Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Built-in primitive definitions carry a `kind` classification:

The exported `BUILTIN_PORTABLE_PRIMITIVES` registry contains package-owned portable primitives plus the explicit host-owned bridge IDs that generated runtimes may call when the host supplies support modules. It does not carry product-shaped transitional compatibility primitives.

`payload.project` is the generic projection primitive for exact dot-path selected-output wrappers. See [Projection primitives](docs/projection-primitives.md) for its ownership boundary.

## Public API

- `load_command_package_ir(path, schema_path=None)` validates IR against the package-owned schema.
Expand Down
20 changes: 20 additions & 0 deletions docs/projection-primitives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Projection primitives

`payload.project` is a host-neutral primitive for exact payload projection.

Command Generation owns only the mechanics:

- read a source value from the operation value map;
- split declared selector strings into exact dot paths;
- resolve object fields and list indexes;
- return a selected-output wrapper with `values`, `missing`, and `available_selectors`.

Host packages own the semantics:

- payload construction;
- selector names and command names;
- view policy, ordering, labels, and text;
- whether a missing selector is acceptable;
- any user-facing interpretation of the projected values.

The primitive intentionally does not evaluate expressions, execute embedded language snippets, infer selectors from prose, or encode host package vocabulary.
75 changes: 75 additions & 0 deletions src/command_generation/primitive_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ def execute_primitive(
return _toml_table_counts(values=values, arguments=arguments, context=context)
if primitive == "payload.assemble":
return _assemble_payload(values=values, arguments=arguments)
if primitive == "payload.project":
return _project_payload(values=values, arguments=arguments)
if primitive == "output.emit":
return _emit_output(values=values, arguments=arguments)
if primitive == "python.function.call":
Expand Down Expand Up @@ -509,6 +511,46 @@ def _emit_output(
return "\n".join(lines).rstrip() + "\n"


def _project_payload(*, values: dict[str, Any], arguments: dict[str, Any]) -> dict[str, Any]:
source_name = str(arguments.get("source") or "result")
source_command = str(arguments.get("source_command") or values.get("operation_id") or "")
selected_output_kind = str(arguments.get("selected_output_kind") or "command-generation/selected-output/v1")
if source_name not in values:
raise PrimitiveExecutionError(f"payload.project source value is missing: {source_name!r}")
payload = values[source_name]
selectors = _projection_selectors(values=values, arguments=arguments)
if not selectors:
return _plain_output_result(payload)
selected: dict[str, Any] = {
"kind": selected_output_kind,
"source_command": source_command,
"values": {},
}
missing: list[str] = []
projected_values = cast(dict[str, Any], selected["values"])
for selector in selectors:
found, value = _field_by_path(payload, selector)
if found:
projected_values[selector] = _plain_output_result(value)
else:
missing.append(selector)
if missing:
selected["missing"] = missing
selected["selector_rule"] = "Comma-separated dot paths select exact JSON fields; unknown fields are reported in missing."
selected["available_selectors"] = _available_selectors_for_payload(payload)
return selected


def _projection_selectors(*, values: dict[str, Any], arguments: dict[str, Any]) -> list[str]:
raw_selectors = arguments.get("selectors")
if raw_selectors is None:
select_value_name = str(arguments.get("select_value") or "select")
raw_selectors = values.get(select_value_name)
if isinstance(raw_selectors, Sequence) and not isinstance(raw_selectors, (str, bytes, bytearray)):
return [str(item).strip() for item in raw_selectors if str(item).strip()]
return [token.strip() for token in str(raw_selectors or "").split(",") if token.strip()]


def _plain_output_result(result: Any) -> Any:
if isinstance(result, Path):
return str(result)
Expand Down Expand Up @@ -702,6 +744,39 @@ def _resolve_dotted_value(payload: Mapping[str, Any], dotted_path: str) -> Any:
return current


def _field_by_path(payload: Any, dotted_path: str) -> tuple[bool, Any]:
if not dotted_path:
return False, None
current: Any = payload
for part in dotted_path.split("."):
if isinstance(current, Mapping) and part in current:
current = current[part]
continue
if isinstance(current, Sequence) and not isinstance(current, (str, bytes, bytearray)):
try:
current = current[int(part)]
continue
except (ValueError, IndexError):
return False, None
return False, None
return True, current


def _available_selectors_for_payload(payload: Any, prefix: str = "") -> list[str]:
selectors: list[str] = []
if isinstance(payload, Mapping):
for key in sorted(str(item) for item in payload):
path = f"{prefix}.{key}" if prefix else key
selectors.append(path)
selectors.extend(_available_selectors_for_payload(payload.get(key), path))
elif isinstance(payload, Sequence) and not isinstance(payload, (str, bytes, bytearray)):
for index, item in enumerate(payload[:10]):
path = f"{prefix}.{index}" if prefix else str(index)
selectors.append(path)
selectors.extend(_available_selectors_for_payload(item, path))
return selectors


def _resolve_inside(root: Path, relative: str) -> Path:
candidate = (root / relative).resolve()
_ensure_inside(root, candidate)
Expand Down
6 changes: 6 additions & 0 deletions src/command_generation/primitive_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ def to_jsonable(self) -> list[dict[str, Any]]:
"description": "Assemble generic file, skill, template, or package-file-list payloads.",
"target_support": {"python": "implemented", "typescript": "implemented"},
},
{
"id": "payload.project",
"kind": "portable",
"description": "Project exact dot-path selectors from a payload into a generic selected-output wrapper.",
"target_support": {"python": "implemented", "typescript": "implemented"},
},
{
"id": "output.emit",
"kind": "portable",
Expand Down
70 changes: 70 additions & 0 deletions src/command_generation/targets/typescript.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,75 @@ class RuntimeError extends Error {{}}
return current;
}}

function fieldByPath(root, dottedPath) {{
if (!dottedPath) return [false, null];
let current = root;
for (const part of String(dottedPath).split('.')) {{
if (isObject(current) && Object.prototype.hasOwnProperty.call(current, part)) {{
current = current[part];
continue;
}}
if (Array.isArray(current)) {{
const index = Number(part);
if (Number.isInteger(index) && index >= 0 && index < current.length) {{
current = current[index];
continue;
}}
}}
return [false, null];
}}
return [true, current];
}}

function selectorTokens(value) {{
if (Array.isArray(value)) return value.map(String).map((item) => item.trim()).filter(Boolean);
return String(value ?? '').split(',').map((item) => item.trim()).filter(Boolean);
}}

function availableSelectorsForPayload(payload, prefix = '') {{
const selectors = [];
if (isObject(payload)) {{
for (const key of Object.keys(payload).map(String).sort()) {{
const path = prefix ? `${{prefix}}.${{key}}` : key;
selectors.push(path);
selectors.push(...availableSelectorsForPayload(payload[key], path));
}}
}} else if (Array.isArray(payload)) {{
for (const [index, item] of payload.slice(0, 10).entries()) {{
const path = prefix ? `${{prefix}}.${{index}}` : String(index);
selectors.push(path);
selectors.push(...availableSelectorsForPayload(item, path));
}}
}}
return selectors;
}}

function projectPayload(values, args) {{
const sourceName = String(args.source ?? 'result');
if (!Object.prototype.hasOwnProperty.call(values, sourceName)) throw new RuntimeError(`payload.project source value is missing: ${{sourceName}}`);
const payload = values[sourceName];
const selectValueName = String(args.select_value ?? 'select');
const selectors = selectorTokens(args.selectors ?? values[selectValueName]);
if (selectors.length === 0) return payload;
const selected = {{
kind: String(args.selected_output_kind ?? 'command-generation/selected-output/v1'),
source_command: String(args.source_command ?? values.operation_id ?? ''),
values: {{}}
}};
const missing = [];
for (const selector of selectors) {{
const [found, value] = fieldByPath(payload, selector);
if (found) selected.values[selector] = value;
else missing.push(selector);
}}
if (missing.length) {{
selected.missing = missing;
selected.selector_rule = 'Comma-separated dot paths select exact JSON fields; unknown fields are reported in missing.';
selected.available_selectors = availableSelectorsForPayload(payload);
}}
return selected;
}}

function assemblePayload(values, args) {{
const fields = args.fields ?? {{}};
if (fields.template !== undefined) return resolveTemplate(fields.template, values);
Expand Down Expand Up @@ -523,6 +592,7 @@ class RuntimeError extends Error {{}}
if (primitive === 'filesystem.glob') return globFiles(valueRoot(args, values), String(args.pattern ?? '')).map((relative_path) => ({{ relative_path }}));
if (primitive === 'json.parse') return JSON.parse(String(values[String(args.source ?? 'registry_text')]));
if (primitive === 'payload.assemble') return assemblePayload(values, args);
if (primitive === 'payload.project') return projectPayload(values, args);
if (primitive === 'output.emit') return emitOutput(values);
return executeHostPrimitive(primitive, values, args, operationId);
}}
Expand Down
9 changes: 9 additions & 0 deletions tests/primitive_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ def main() -> int:
assert skill_payload["actions"][0]["path"] == "review"
assert skill_payload["actions"][0]["source"] == "review"

selected_payload = execute_primitive(
"payload.project",
values={"result": skill_payload, "select": "actions.0.path,message,missing"},
arguments={"source_command": "fixture.skills"},
context=context,
)
assert selected_payload["values"] == {"actions.0.path": "review", "message": "Skills"}
assert selected_payload["missing"] == ["missing"]

emitted_json = execute_primitive(
"output.emit",
values={"result": skill_payload, "format": "json"},
Expand Down
79 changes: 79 additions & 0 deletions tests/test_primitive_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,52 @@ def test_payload_assemble_supports_template_field_selectors(primitive_context: P
assert payload == {"status": "present", "nested": {"note_count": 3, "required_count": 1}}


def test_payload_project_selects_exact_paths_and_reports_missing(primitive_context: PrimitiveContext) -> None:
result = execute_primitive(
"payload.project",
values={
"operation_id": "fixture.show",
"select": "items.0.name,summary.count,missing.value",
"result": {
"summary": {"count": 2},
"items": [{"name": "alpha"}, {"name": "beta"}],
},
},
context=primitive_context,
)

assert result["kind"] == "command-generation/selected-output/v1"
assert result["source_command"] == "fixture.show"
assert result["values"] == {"items.0.name": "alpha", "summary.count": 2}
assert result["missing"] == ["missing.value"]
assert "items.1.name" in result["available_selectors"]


def test_payload_project_can_use_declared_selector_list(primitive_context: PrimitiveContext) -> None:
result = execute_primitive(
"payload.project",
values={
"payload": {
"status": "ready",
"details": {"owner": "fixture"},
}
},
arguments={
"source": "payload",
"source_command": "fixture.status",
"selectors": ["status", "details.owner"],
"selected_output_kind": "fixture/selected-output/v1",
},
context=primitive_context,
)

assert result == {
"kind": "fixture/selected-output/v1",
"source_command": "fixture.status",
"values": {"status": "ready", "details.owner": "fixture"},
}


def test_operation_fragments_compose_reusable_step_groups(primitive_context: PrimitiveContext) -> None:
operation = {
"id": "fixture.report",
Expand Down Expand Up @@ -294,6 +340,39 @@ def test_operation_fragments_compose_reusable_step_groups(primitive_context: Pri
assert json.loads(values["emitted"]) == {"status": "ok"}


def test_run_operation_steps_can_project_payload_fields(primitive_context: PrimitiveContext) -> None:
operation = {
"id": "fixture.project",
"ir_plan": {
"steps": [
{
"id": "make_result",
"uses": "fixture.make-result",
"outputs": ["result"],
},
{
"id": "project",
"uses": "payload.project",
"arguments": {
"selectors": ["summary.status"],
"source_command": "fixture.project",
},
"outputs": ["selected"],
},
]
},
}

values = run_operation_steps(
operation,
initial_values={},
context=primitive_context,
handlers={"fixture.make-result": lambda values, arguments, context: {"summary": {"status": "ready"}}},
)

assert values["selected"]["values"] == {"summary.status": "ready"}


def test_operation_fragments_reject_cycles(primitive_context: PrimitiveContext) -> None:
operation = {
"id": "fixture.report",
Expand Down
2 changes: 2 additions & 0 deletions tests/test_public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2028,6 +2028,7 @@ def test_primitive_registry_round_trips_host_metadata() -> None:

def test_builtin_registry_declares_portable_primitives() -> None:
assert "filesystem.read" in BUILTIN_PORTABLE_PRIMITIVES.ids()
assert "payload.project" in BUILTIN_PORTABLE_PRIMITIVES.ids()
assert "output.emit" in BUILTIN_PORTABLE_PRIMITIVES.ids()


Expand All @@ -2037,6 +2038,7 @@ def test_builtin_registry_classifies_primitive_ownership_boundaries() -> None:
assert {item["kind"] for item in definitions.values()} <= {"portable", "host-owned"}
assert definitions["filesystem.read"]["kind"] == "portable"
assert definitions["json.parse"]["kind"] == "portable"
assert definitions["payload.project"]["kind"] == "portable"
assert definitions["python.function.call"]["kind"] == "host-owned"
assert definitions["typescript.domain.execute"]["kind"] == "host-owned"

Expand Down
Loading