From 8d3bfb57cc3d9ef66e25a03285a249ced3d6d0fe Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 03:03:11 -0400 Subject: [PATCH 1/7] test: consume updated Python 0.9 conformance contract --- tests/api_parity.test.ts | 9 +- tests/apply-directive-fixtures.test.ts | 4 +- tests/fixtures/.source-commit | 2 +- tests/fixtures/README.md | 13 --- .../conformance/api/public-api-v2.json | 57 +++++-------- .../conformance/api/public-grammar-v1.json | 42 +++------ .../conformance/apply-directive/README.md | 8 +- ...ve_structural_use_item_conflict_error.json | 57 +++++++++++++ ..._directive_structural_use_item_update.json | 31 +++++++ ...use_item_replacement_prefix_rejected.json} | 5 +- ...ruct_canonical_directive_replace_use.json} | 4 +- ...nvalid_clear_premise_suffix_rejected.json} | 6 +- ...e_invalid_clear_state_suffix_rejected.json | 16 ++++ ...nvalid_reset_policies_suffix_rejected.json | 16 ++++ ...ective_constructor_operands_isolation.json | 60 +++++++++++++ .../conformance/mutation-isolation/README.md | 4 +- ...n_isolation_engine_policies_property.json} | 0 ...on_isolation_engine_premise_property.json} | 0 ...> mutation_isolation_update_decision.json} | 0 ...alized_policy_key_rejected_atomically.json | 1 - ...sanitized_premise_rejected_atomically.json | 1 - ...son_import_json_invalid_json_rejected.json | 1 - ...t_json_invalid_policies_type_rejected.json | 1 - ...rt_json_invalid_policy_value_rejected.json | 1 - ...rt_json_invalid_premise_type_rejected.json | 1 - ..._json_missing_policies_field_rejected.json | 1 - ..._json_import_json_non_object_rejected.json | 1 - ..._policy_collision_rejected_atomically.json | 1 - ..._policy_collision_rejected_atomically.json | 1 - ...ort_json_unsupported_version_rejected.json | 1 - .../workflow_apply_returned_repair.json | 85 +++++++++++++++++++ ...port_import_fixed_point_three_cycles.json} | 4 +- .../workflow_mixed_conflict_remove_retry.json | 83 ++++++++++++++++++ ...rt_then_yes_no_no_continuation_state.json} | 4 +- ...tep_apply_equivalence_change_premise.json} | 4 +- ...step_apply_equivalence_clear_premise.json} | 4 +- ...w_step_apply_equivalence_clear_state.json} | 4 +- ...step_apply_equivalence_prohibit_item.json} | 4 +- ...step_apply_equivalence_remove_policy.json} | 4 +- ...w_step_apply_equivalence_replace_use.json} | 4 +- ...tep_apply_equivalence_reset_policies.json} | 4 +- ...w_step_apply_equivalence_set_premise.json} | 4 +- ...flow_step_apply_equivalence_use_item.json} | 4 +- tests/grammar-api-contract.test.ts | 8 +- tests/grammar-fixtures.test.ts | 19 +++++ tests/harness/fixtures.ts | 34 ++++++-- tests/mutation-isolation-fixtures.test.ts | 12 ++- ...ures.test.ts => workflow-fixtures.test.ts} | 27 +++--- 48 files changed, 502 insertions(+), 155 deletions(-) create mode 100644 tests/fixtures/conformance/apply-directive/apply_directive_structural_use_item_conflict_error.json create mode 100644 tests/fixtures/conformance/apply-directive/apply_directive_structural_use_item_update.json rename tests/fixtures/conformance/grammar/{grammar_render_invalid_use_item_replacement_prefix_rejected.json => grammar_construct_canonical_directive_invalid_use_item_replacement_prefix_rejected.json} (62%) rename tests/fixtures/conformance/grammar/{grammar_render_replace_use.json => grammar_construct_canonical_directive_replace_use.json} (70%) rename tests/fixtures/conformance/grammar/{grammar_decompose_invalid_compound_use_and_prohibit_rejected.json => grammar_decompose_invalid_clear_premise_suffix_rejected.json} (58%) create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_invalid_clear_state_suffix_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_invalid_reset_policies_suffix_rejected.json create mode 100644 tests/fixtures/conformance/mutation-isolation/014_canonical_directive_constructor_operands_isolation.json rename tests/fixtures/conformance/mutation-isolation/{008_engine_policies_property_isolation.json => mutation_isolation_engine_policies_property.json} (100%) rename tests/fixtures/conformance/mutation-isolation/{009_engine_premise_property_isolation.json => mutation_isolation_engine_premise_property.json} (100%) rename tests/fixtures/conformance/mutation-isolation/{003_update_decision_state_isolation.json => mutation_isolation_update_decision.json} (100%) create mode 100644 tests/fixtures/conformance/workflow/workflow_apply_returned_repair.json rename tests/fixtures/conformance/{controller/controller_export_import_fixed_point_three_cycles.json => workflow/workflow_export_import_fixed_point_three_cycles.json} (92%) create mode 100644 tests/fixtures/conformance/workflow/workflow_mixed_conflict_remove_retry.json rename tests/fixtures/conformance/{controller/controller_replace_error_import_then_yes_no_no_reserved_state.json => workflow/workflow_replace_error_import_then_yes_no_no_continuation_state.json} (93%) rename tests/fixtures/conformance/{controller/controller_step_apply_equivalence_change_premise.json => workflow/workflow_step_apply_equivalence_change_premise.json} (93%) rename tests/fixtures/conformance/{controller/controller_step_apply_equivalence_clear_premise.json => workflow/workflow_step_apply_equivalence_clear_premise.json} (93%) rename tests/fixtures/conformance/{controller/controller_step_apply_equivalence_clear_state.json => workflow/workflow_step_apply_equivalence_clear_state.json} (93%) rename tests/fixtures/conformance/{controller/controller_step_apply_equivalence_prohibit_item.json => workflow/workflow_step_apply_equivalence_prohibit_item.json} (93%) rename tests/fixtures/conformance/{controller/controller_step_apply_equivalence_remove_policy.json => workflow/workflow_step_apply_equivalence_remove_policy.json} (92%) rename tests/fixtures/conformance/{controller/controller_step_apply_equivalence_replace_use.json => workflow/workflow_step_apply_equivalence_replace_use.json} (93%) rename tests/fixtures/conformance/{controller/controller_step_apply_equivalence_reset_policies.json => workflow/workflow_step_apply_equivalence_reset_policies.json} (92%) rename tests/fixtures/conformance/{controller/controller_step_apply_equivalence_set_premise.json => workflow/workflow_step_apply_equivalence_set_premise.json} (93%) rename tests/fixtures/conformance/{controller/controller_step_apply_equivalence_use_item.json => workflow/workflow_step_apply_equivalence_use_item.json} (93%) rename tests/{controller-fixtures.test.ts => workflow-fixtures.test.ts} (68%) diff --git a/tests/api_parity.test.ts b/tests/api_parity.test.ts index fa3b4b6..59178fb 100644 --- a/tests/api_parity.test.ts +++ b/tests/api_parity.test.ts @@ -53,6 +53,7 @@ type EngineMemberSpec = { probes?: Array<{ args: unknown[]; raises?: { type: string }; + rejects?: boolean; }>; }; @@ -401,7 +402,7 @@ describe('public API parity contract (conformance fixture)', () => { return values.map(materializeProbeValue); })(); const construct = () => Reflect.construct(cc.SemanticErrorDecision, args); - if (probe.raises != null) { + if (probe.raises != null || probe.rejects === true) { expect(construct, `SemanticErrorDecision construction probe ${index} should raise`).toThrowError(TypeError); continue; } @@ -434,8 +435,8 @@ describe('public API parity contract (conformance fixture)', () => { return Reflect.construct(cc.Engine, constructorArgs); }; - if (probe.raises != null) { - if (probe.raises.type === 'TypeError') { + if (probe.raises != null || probe.rejects === true) { + if (probe.raises?.type === 'TypeError') { expect(construct, `Engine construction probe ${index} should raise TypeError`).toThrowError(TypeError); } else { expect(construct, `Engine construction probe ${index} should raise`).toThrow(); @@ -529,7 +530,7 @@ describe('public API parity contract (conformance fixture)', () => { for (const [index, probe] of (memberSpec.probes ?? []).entries()) { const invoke = () => (engine as unknown as Record unknown>)[memberName](...probe.args); - if (probe.raises != null) { + if (probe.raises != null || probe.rejects === true) { expect(invoke, `${memberName} probe ${index} should raise`).toThrow(); } else { expect(invoke, `${memberName} probe ${index} should not raise`).not.toThrow(); diff --git a/tests/apply-directive-fixtures.test.ts b/tests/apply-directive-fixtures.test.ts index cc7ec27..72d7385 100644 --- a/tests/apply-directive-fixtures.test.ts +++ b/tests/apply-directive-fixtures.test.ts @@ -15,7 +15,9 @@ describe('apply-directive fixtures (conformance)', () => { 'function' ); - const directive = decompose_directive(fixture.payload.action.text); + const directive = fixture.payload.action.directive === undefined + ? decompose_directive(fixture.payload.action.text as string) + : new CanonicalDirective(fixture.payload.action.directive.kind, fixture.payload.action.directive.operands as Record); expect(directive).toBeInstanceOf(CanonicalDirective); const decision = (applyDirective as (value: CanonicalDirective) => unknown).call(engine, directive); expect(decision).toEqual(fixture.payload.expected.decision); diff --git a/tests/fixtures/.source-commit b/tests/fixtures/.source-commit index 7569023..505b958 100644 --- a/tests/fixtures/.source-commit +++ b/tests/fixtures/.source-commit @@ -1 +1 @@ -ee49df7fdcd8965392da31d4ead37c7514d53d29 +01cac9c154eac33675cbd9a207eec085fde58d06 diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index e8cdb16..8b08bf3 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -45,18 +45,6 @@ Portable serialization contract coverage for `engine.export_json()` and `engine.import_json(...)`, including canonical export payload shape and deterministic validation/error boundaries. -## Controller fixtures - -For [`conformance/controller/`](conformance/controller/): - -Portable controller contract coverage for: - -* `step(engine, user_input)` result envelope and state snapshot -* `preview(engine, user_input)` result envelope, `would_mutate`, and non-mutation of live engine state -* `state_diff(state_before, state_after)` deterministic structural diff output - -These fixtures keep a minimal, language-neutral contract matrix for controller APIs. - ## Source of truth Fixtures reflect current Python behavior and tests. @@ -89,5 +77,4 @@ See the TypeScript fixture runners in this repository for execution details: * [`step-fixtures.test.ts`](../step-fixtures.test.ts) * [`state-json-fixtures.test.ts`](../state-json-fixtures.test.ts) -* [`controller-fixtures.test.ts`](../controller-fixtures.test.ts) * [`structured-regression-fixtures.test.ts`](../structured-regression-fixtures.test.ts) diff --git a/tests/fixtures/conformance/api/public-api-v2.json b/tests/fixtures/conformance/api/public-api-v2.json index a3a4dd2..d396aed 100644 --- a/tests/fixtures/conformance/api/public-api-v2.json +++ b/tests/fixtures/conformance/api/public-api-v2.json @@ -37,6 +37,7 @@ "_NO_DIRECTIVE" ], "forbidden_engine_members": [ + "apply_transcript", "export_checkpoint", "export_checkpoint_json", "has_pending_clarification", @@ -99,17 +100,13 @@ "args": [ "unexpected" ], - "raises": { - "type": "TypeError" - } + "rejects": true }, { "kwargs": { "value": "unexpected" }, - "raises": { - "type": "TypeError" - } + "rejects": true } ] }, @@ -179,9 +176,7 @@ }, { "args": [], - "raises": { - "type": "TypeError" - } + "rejects": true }, { "kwargs": { @@ -193,9 +188,7 @@ }, "unexpected": true }, - "raises": { - "type": "TypeError" - } + "rejects": true } ] }, @@ -252,17 +245,13 @@ }, { "args": [], - "raises": { - "type": "TypeError" - } + "rejects": true }, { "kwargs": { "value": true }, - "raises": { - "type": "TypeError" - } + "rejects": true } ] }, @@ -293,17 +282,13 @@ "args": [ "unexpected" ], - "raises": { - "type": "TypeError" - } + "rejects": true }, { "kwargs": { "state": {} }, - "raises": { - "type": "TypeError" - } + "rejects": true } ] }, @@ -351,33 +336,25 @@ "args": [ "use docker" ], - "raises": { - "type": "AttributeError" - } + "rejects": true }, { "args": [ null ], - "raises": { - "type": "AttributeError" - } + "rejects": true }, { "args": [ 1 ], - "raises": { - "type": "AttributeError" - } + "rejects": true }, { "args": [ {} ], - "raises": { - "type": "AttributeError" - } + "rejects": true } ] }, @@ -394,10 +371,14 @@ } }, "policies": { - "kind": "property" + "kind": "property", + "readable": true, + "writable": false }, "premise": { - "kind": "property" + "kind": "property", + "readable": true, + "writable": false }, "step": { "kind": "method", diff --git a/tests/fixtures/conformance/api/public-grammar-v1.json b/tests/fixtures/conformance/api/public-grammar-v1.json index 0385f77..a3f9cab 100644 --- a/tests/fixtures/conformance/api/public-grammar-v1.json +++ b/tests/fixtures/conformance/api/public-grammar-v1.json @@ -80,7 +80,7 @@ }, { "args": [], - "raises": {"type": "TypeError"} + "rejects": true }, { "kwargs": { @@ -89,7 +89,7 @@ "operand_names": ["item"], "unexpected": true }, - "raises": {"type": "TypeError"} + "rejects": true } ] }, @@ -172,18 +172,14 @@ "item": "docker" } }, - "raises": { - "type": "ValueError" - } + "rejects": true }, { "kwargs": { "kind": "use_item", "operands": {} }, - "raises": { - "type": "ValueError" - } + "rejects": true }, { "kwargs": { @@ -192,9 +188,7 @@ "item": "docker" } }, - "raises": { - "type": "ValueError" - } + "rejects": true }, { "kwargs": { @@ -203,9 +197,7 @@ "value": 123 } }, - "raises": { - "type": "ValueError" - } + "rejects": true }, { "kwargs": { @@ -214,9 +206,7 @@ "value": "to concise replies" } }, - "raises": { - "type": "ValueError" - } + "rejects": true }, { "kwargs": { @@ -226,9 +216,7 @@ "old_item": "docker" } }, - "raises": { - "type": "ValueError" - } + "rejects": true }, { "kwargs": { @@ -237,9 +225,7 @@ "item": "instead of" } }, - "raises": { - "type": "ValueError" - } + "rejects": true }, { "kwargs": { @@ -248,13 +234,11 @@ "item": "docker prohibit peanuts" } }, - "raises": { - "type": "ValueError" - } + "rejects": true }, { "args": [], - "raises": {"type": "TypeError"} + "rejects": true }, { "kwargs": { @@ -262,7 +246,7 @@ "operands": {"item": "docker"}, "unexpected": true }, - "raises": {"type": "TypeError"} + "rejects": true } ] }, @@ -300,7 +284,7 @@ }, { "kwargs": {"unexpected": true}, - "raises": {"type": "TypeError"} + "rejects": true } ] }, diff --git a/tests/fixtures/conformance/apply-directive/README.md b/tests/fixtures/conformance/apply-directive/README.md index 72889c6..d386fbd 100644 --- a/tests/fixtures/conformance/apply-directive/README.md +++ b/tests/fixtures/conformance/apply-directive/README.md @@ -27,12 +27,14 @@ Each fixture is a JSON object with: ### `action` -The current portable action form is: +The portable action forms are: * `{"fn":"apply_directive","text":"...canonical directive text..."}` + decomposes the text before applying the resulting directive. +* `{"fn":"apply_directive","directive":{"kind":"...","operands":{...}}}` + constructs a public `CanonicalDirective` directly and passes it to + `engine.apply_directive(...)` without parsing text first. -The Python source-of-truth runner validates that `text` decomposes to a -canonical directive before calling `engine.apply_directive(...)`. Ports may construct the equivalent canonical directive object using their own public grammar surface. diff --git a/tests/fixtures/conformance/apply-directive/apply_directive_structural_use_item_conflict_error.json b/tests/fixtures/conformance/apply-directive/apply_directive_structural_use_item_conflict_error.json new file mode 100644 index 0000000..ce36575 --- /dev/null +++ b/tests/fixtures/conformance/apply-directive/apply_directive_structural_use_item_conflict_error.json @@ -0,0 +1,57 @@ +{ + "id": "apply_directive_structural_use_item_conflict_error", + "kind": "apply_directive", + "initial_state": { + "premise": null, + "policies": { + "docker": "prohibit" + }, + "version": 2 + }, + "action": { + "fn": "apply_directive", + "directive": { + "kind": "use_item", + "operands": { + "item": "docker" + } + } + }, + "expected": { + "decision": { + "kind": "error", + "failure": "item_prohibited", + "directive": { + "kind": "use_item", + "text": "use docker", + "operands": { + "item": "docker" + } + }, + "repairs": [ + { + "kind": "remove_policy", + "text": "remove policy docker", + "operands": { + "item": "docker" + } + }, + { + "kind": "use_item", + "text": "use docker", + "operands": { + "item": "docker" + } + } + ], + "message": "\"docker\" is currently prohibited.\nRemove or replace it before using it." + }, + "state": { + "premise": null, + "policies": { + "docker": "prohibit" + }, + "version": 2 + } + } +} diff --git a/tests/fixtures/conformance/apply-directive/apply_directive_structural_use_item_update.json b/tests/fixtures/conformance/apply-directive/apply_directive_structural_use_item_update.json new file mode 100644 index 0000000..5793fca --- /dev/null +++ b/tests/fixtures/conformance/apply-directive/apply_directive_structural_use_item_update.json @@ -0,0 +1,31 @@ +{ + "id": "apply_directive_structural_use_item_update", + "kind": "apply_directive", + "initial_state": { + "premise": null, + "policies": {}, + "version": 2 + }, + "action": { + "fn": "apply_directive", + "directive": { + "kind": "use_item", + "operands": { + "item": "docker" + } + } + }, + "expected": { + "decision": { + "kind": "update", + "changed": true + }, + "state": { + "premise": null, + "policies": { + "docker": "use" + }, + "version": 2 + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_render_invalid_use_item_replacement_prefix_rejected.json b/tests/fixtures/conformance/grammar/grammar_construct_canonical_directive_invalid_use_item_replacement_prefix_rejected.json similarity index 62% rename from tests/fixtures/conformance/grammar/grammar_render_invalid_use_item_replacement_prefix_rejected.json rename to tests/fixtures/conformance/grammar/grammar_construct_canonical_directive_invalid_use_item_replacement_prefix_rejected.json index 3cb11d7..a173bab 100644 --- a/tests/fixtures/conformance/grammar/grammar_render_invalid_use_item_replacement_prefix_rejected.json +++ b/tests/fixtures/conformance/grammar/grammar_construct_canonical_directive_invalid_use_item_replacement_prefix_rejected.json @@ -1,8 +1,8 @@ { - "id": "grammar_render_invalid_use_item_replacement_prefix_rejected", + "id": "grammar_construct_canonical_directive_invalid_use_item_replacement_prefix_rejected", "kind": "grammar", "action": { - "fn": "render_directive", + "fn": "construct_canonical_directive", "kind": "use_item", "operands": { "item": "instead of docker" @@ -10,7 +10,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "canonical use_item directive" } } diff --git a/tests/fixtures/conformance/grammar/grammar_render_replace_use.json b/tests/fixtures/conformance/grammar/grammar_construct_canonical_directive_replace_use.json similarity index 70% rename from tests/fixtures/conformance/grammar/grammar_render_replace_use.json rename to tests/fixtures/conformance/grammar/grammar_construct_canonical_directive_replace_use.json index c0059ca..c1c0607 100644 --- a/tests/fixtures/conformance/grammar/grammar_render_replace_use.json +++ b/tests/fixtures/conformance/grammar/grammar_construct_canonical_directive_replace_use.json @@ -1,8 +1,8 @@ { - "id": "grammar_render_replace_use", + "id": "grammar_construct_canonical_directive_replace_use", "kind": "grammar", "action": { - "fn": "render_directive", + "fn": "construct_canonical_directive", "kind": "replace_use", "operands": { "new_item": "podman", diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_compound_use_and_prohibit_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_clear_premise_suffix_rejected.json similarity index 58% rename from tests/fixtures/conformance/grammar/grammar_decompose_invalid_compound_use_and_prohibit_rejected.json rename to tests/fixtures/conformance/grammar/grammar_decompose_invalid_clear_premise_suffix_rejected.json index f71adf1..b65809b 100644 --- a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_compound_use_and_prohibit_rejected.json +++ b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_clear_premise_suffix_rejected.json @@ -1,14 +1,14 @@ { - "id": "grammar_decompose_invalid_compound_use_and_prohibit_rejected", + "id": "grammar_decompose_invalid_clear_premise_suffix_rejected", "kind": "grammar", "action": { "fn": "decompose_directive", - "text": "use docker and prohibit peanuts" + "text": "clear premise now" }, "expected": { "directive": { "kind": "invalid_directive_syntax", - "failure": "compound_directive", + "failure": "malformed_directive", "directive_kind": null, "missing_operand": null } diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_clear_state_suffix_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_clear_state_suffix_rejected.json new file mode 100644 index 0000000..7407879 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_clear_state_suffix_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_invalid_clear_state_suffix_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "clear state now" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "malformed_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_reset_policies_suffix_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_reset_policies_suffix_rejected.json new file mode 100644 index 0000000..13e4350 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_reset_policies_suffix_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_invalid_reset_policies_suffix_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "reset policies now" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "malformed_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/mutation-isolation/014_canonical_directive_constructor_operands_isolation.json b/tests/fixtures/conformance/mutation-isolation/014_canonical_directive_constructor_operands_isolation.json new file mode 100644 index 0000000..04540af --- /dev/null +++ b/tests/fixtures/conformance/mutation-isolation/014_canonical_directive_constructor_operands_isolation.json @@ -0,0 +1,60 @@ +{ + "id": "014_canonical_directive_constructor_operands_isolation", + "kind": "mutation_isolation", + "initial_state": { + "premise": null, + "policies": {}, + "version": 2 + }, + "operation": { + "fn": "canonical_directive.constructor_operands", + "kind": "use_item", + "operands": { + "item": "docker" + }, + "source_handle": "source_operands", + "result_handle": "directive" + }, + "handles": { + "source_operands": { + "kind": "caller_owned_constructor_input" + }, + "directive": { + "kind": "constructed_directive" + } + }, + "mutations": [ + { + "target_handle": "source_operands", + "path": [ + "item" + ], + "op": "set", + "value": "podman" + } + ], + "expected": { + "authoritative_state": { + "premise": null, + "policies": {}, + "version": 2 + }, + "caller_owned_observations": { + "directive_operands_after_source_mutation": { + "target_handle": "directive", + "path": [ + "operands", + "item" + ], + "value": "docker" + }, + "directive_text_after_source_mutation": { + "target_handle": "directive", + "path": [ + "text" + ], + "value": "use docker" + } + } + } +} diff --git a/tests/fixtures/conformance/mutation-isolation/README.md b/tests/fixtures/conformance/mutation-isolation/README.md index 4a32a22..82ffde1 100644 --- a/tests/fixtures/conformance/mutation-isolation/README.md +++ b/tests/fixtures/conformance/mutation-isolation/README.md @@ -22,7 +22,8 @@ are immutable. The contract requires: For the grammar-object fixtures, exposed `CanonicalDirective.operands` and the covered `DirectiveMetadata` fields must preserve mutation isolation: a caller mutation either is rejected or does not change the exposed value observed after -the mutation. +the mutation. Constructed directives also capture their operand values +independently from the caller-owned mapping used to construct them. ## Fixture shape @@ -49,6 +50,7 @@ Examples: * `engine.policies` * `engine.premise` * `canonical_directive.operands` +* `canonical_directive.constructor_operands` * `directive_metadata` For the current corpus, `operation` uses a closed per-function field set. Unknown operation fields are invalid. diff --git a/tests/fixtures/conformance/mutation-isolation/008_engine_policies_property_isolation.json b/tests/fixtures/conformance/mutation-isolation/mutation_isolation_engine_policies_property.json similarity index 100% rename from tests/fixtures/conformance/mutation-isolation/008_engine_policies_property_isolation.json rename to tests/fixtures/conformance/mutation-isolation/mutation_isolation_engine_policies_property.json diff --git a/tests/fixtures/conformance/mutation-isolation/009_engine_premise_property_isolation.json b/tests/fixtures/conformance/mutation-isolation/mutation_isolation_engine_premise_property.json similarity index 100% rename from tests/fixtures/conformance/mutation-isolation/009_engine_premise_property_isolation.json rename to tests/fixtures/conformance/mutation-isolation/mutation_isolation_engine_premise_property.json diff --git a/tests/fixtures/conformance/mutation-isolation/003_update_decision_state_isolation.json b/tests/fixtures/conformance/mutation-isolation/mutation_isolation_update_decision.json similarity index 100% rename from tests/fixtures/conformance/mutation-isolation/003_update_decision_state_isolation.json rename to tests/fixtures/conformance/mutation-isolation/mutation_isolation_update_decision.json diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_empty_normalized_policy_key_rejected_atomically.json b/tests/fixtures/conformance/state-json/state_json_import_json_empty_normalized_policy_key_rejected_atomically.json index cf0f1ad..60e60a0 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_empty_normalized_policy_key_rejected_atomically.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_empty_normalized_policy_key_rejected_atomically.json @@ -15,7 +15,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid state payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_empty_sanitized_premise_rejected_atomically.json b/tests/fixtures/conformance/state-json/state_json_import_json_empty_sanitized_premise_rejected_atomically.json index 80ca26a..ad61acf 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_empty_sanitized_premise_rejected_atomically.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_empty_sanitized_premise_rejected_atomically.json @@ -16,7 +16,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid state payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_invalid_json_rejected.json b/tests/fixtures/conformance/state-json/state_json_import_json_invalid_json_rejected.json index 8fe097f..77ea34a 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_invalid_json_rejected.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_invalid_json_rejected.json @@ -15,7 +15,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid JSON payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_invalid_policies_type_rejected.json b/tests/fixtures/conformance/state-json/state_json_import_json_invalid_policies_type_rejected.json index b102324..94dcb7a 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_invalid_policies_type_rejected.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_invalid_policies_type_rejected.json @@ -12,7 +12,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid state payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_invalid_policy_value_rejected.json b/tests/fixtures/conformance/state-json/state_json_import_json_invalid_policy_value_rejected.json index 6cb9434..90fd584 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_invalid_policy_value_rejected.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_invalid_policy_value_rejected.json @@ -12,7 +12,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid state payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_invalid_premise_type_rejected.json b/tests/fixtures/conformance/state-json/state_json_import_json_invalid_premise_type_rejected.json index 409d061..2848e16 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_invalid_premise_type_rejected.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_invalid_premise_type_rejected.json @@ -16,7 +16,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid state payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_missing_policies_field_rejected.json b/tests/fixtures/conformance/state-json/state_json_import_json_missing_policies_field_rejected.json index 7655c25..240f573 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_missing_policies_field_rejected.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_missing_policies_field_rejected.json @@ -12,7 +12,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid state payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_non_object_rejected.json b/tests/fixtures/conformance/state-json/state_json_import_json_non_object_rejected.json index 924ebd5..dfe3767 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_non_object_rejected.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_non_object_rejected.json @@ -16,7 +16,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid state payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_normalized_policy_collision_rejected_atomically.json b/tests/fixtures/conformance/state-json/state_json_import_json_normalized_policy_collision_rejected_atomically.json index 49927e9..ead35e3 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_normalized_policy_collision_rejected_atomically.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_normalized_policy_collision_rejected_atomically.json @@ -15,7 +15,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid state payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_unicode_casefold_policy_collision_rejected_atomically.json b/tests/fixtures/conformance/state-json/state_json_import_json_unicode_casefold_policy_collision_rejected_atomically.json index 8821e85..cdd3246 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_unicode_casefold_policy_collision_rejected_atomically.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_unicode_casefold_policy_collision_rejected_atomically.json @@ -15,7 +15,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Invalid state payload" }, "state": { diff --git a/tests/fixtures/conformance/state-json/state_json_import_json_unsupported_version_rejected.json b/tests/fixtures/conformance/state-json/state_json_import_json_unsupported_version_rejected.json index 411dead..c2a3f37 100644 --- a/tests/fixtures/conformance/state-json/state_json_import_json_unsupported_version_rejected.json +++ b/tests/fixtures/conformance/state-json/state_json_import_json_unsupported_version_rejected.json @@ -15,7 +15,6 @@ }, "expected": { "error": { - "type": "ValueError", "message_contains": "Unsupported state version" }, "state": { diff --git a/tests/fixtures/conformance/workflow/workflow_apply_returned_repair.json b/tests/fixtures/conformance/workflow/workflow_apply_returned_repair.json new file mode 100644 index 0000000..5a7711e --- /dev/null +++ b/tests/fixtures/conformance/workflow/workflow_apply_returned_repair.json @@ -0,0 +1,85 @@ +{ + "id": "workflow_apply_returned_repair", + "kind": "workflow", + "initial_state": { + "premise": null, + "policies": {}, + "version": 2 + }, + "operations": [ + { + "fn": "step", + "input": "prohibit docker", + "label": "prohibit_step" + }, + { + "fn": "step", + "input": "use docker", + "label": "conflict" + }, + { + "fn": "apply_repair", + "decision_ref": "conflict", + "repair_index": 0, + "label": "remove_repair" + }, + { + "fn": "apply_repair", + "decision_ref": "conflict", + "repair_index": 1, + "label": "use_repair" + } + ], + "expected": { + "observations": { + "prohibit_step": { + "kind": "update", + "changed": true + }, + "conflict": { + "kind": "error", + "failure": "item_prohibited", + "directive": { + "kind": "use_item", + "text": "use docker", + "operands": { + "item": "docker" + } + }, + "repairs": [ + { + "kind": "remove_policy", + "text": "remove policy docker", + "operands": { + "item": "docker" + } + }, + { + "kind": "use_item", + "text": "use docker", + "operands": { + "item": "docker" + } + } + ], + "message": "\"docker\" is currently prohibited.\nRemove or replace it before using it." + }, + "remove_repair": { + "kind": "update", + "changed": true + }, + "use_repair": { + "kind": "update", + "changed": true + } + }, + "equal": [], + "state": { + "premise": null, + "policies": { + "docker": "use" + }, + "version": 2 + } + } +} diff --git a/tests/fixtures/conformance/controller/controller_export_import_fixed_point_three_cycles.json b/tests/fixtures/conformance/workflow/workflow_export_import_fixed_point_three_cycles.json similarity index 92% rename from tests/fixtures/conformance/controller/controller_export_import_fixed_point_three_cycles.json rename to tests/fixtures/conformance/workflow/workflow_export_import_fixed_point_three_cycles.json index b1f4e9e..e8abcec 100644 --- a/tests/fixtures/conformance/controller/controller_export_import_fixed_point_three_cycles.json +++ b/tests/fixtures/conformance/workflow/workflow_export_import_fixed_point_three_cycles.json @@ -1,6 +1,6 @@ { - "id": "controller_export_import_fixed_point_three_cycles", - "kind": "controller", + "id": "workflow_export_import_fixed_point_three_cycles", + "kind": "workflow", "initial_state": { "premise": "Use concise output", "policies": { diff --git a/tests/fixtures/conformance/workflow/workflow_mixed_conflict_remove_retry.json b/tests/fixtures/conformance/workflow/workflow_mixed_conflict_remove_retry.json new file mode 100644 index 0000000..9487974 --- /dev/null +++ b/tests/fixtures/conformance/workflow/workflow_mixed_conflict_remove_retry.json @@ -0,0 +1,83 @@ +{ + "id": "workflow_mixed_conflict_remove_retry", + "kind": "workflow", + "initial_state": { + "premise": null, + "policies": {}, + "version": 2 + }, + "operations": [ + { + "fn": "step", + "input": "use docker", + "label": "use_step" + }, + { + "fn": "apply_directive", + "text": "prohibit docker", + "label": "conflict_apply" + }, + { + "fn": "step", + "input": "remove policy docker", + "label": "remove_step" + }, + { + "fn": "apply_directive", + "text": "use docker", + "label": "retry_apply" + } + ], + "expected": { + "observations": { + "use_step": { + "kind": "update", + "changed": true + }, + "conflict_apply": { + "kind": "error", + "failure": "item_already_in_use", + "directive": { + "kind": "prohibit_item", + "text": "prohibit docker", + "operands": { + "item": "docker" + } + }, + "repairs": [ + { + "kind": "remove_policy", + "text": "remove policy docker", + "operands": { + "item": "docker" + } + }, + { + "kind": "prohibit_item", + "text": "prohibit docker", + "operands": { + "item": "docker" + } + } + ], + "message": "\"docker\" is currently in use.\nRemove or replace it before prohibiting it." + }, + "remove_step": { + "kind": "update", + "changed": true + }, + "retry_apply": { + "kind": "update", + "changed": true + } + }, + "equal": [], + "state": { + "premise": null, + "policies": { + "docker": "use" + }, + "version": 2 + } + } +} diff --git a/tests/fixtures/conformance/controller/controller_replace_error_import_then_yes_no_no_reserved_state.json b/tests/fixtures/conformance/workflow/workflow_replace_error_import_then_yes_no_no_continuation_state.json similarity index 93% rename from tests/fixtures/conformance/controller/controller_replace_error_import_then_yes_no_no_reserved_state.json rename to tests/fixtures/conformance/workflow/workflow_replace_error_import_then_yes_no_no_continuation_state.json index b1f5006..7e0fa6c 100644 --- a/tests/fixtures/conformance/controller/controller_replace_error_import_then_yes_no_no_reserved_state.json +++ b/tests/fixtures/conformance/workflow/workflow_replace_error_import_then_yes_no_no_continuation_state.json @@ -1,6 +1,6 @@ { - "id": "controller_replace_error_import_then_yes_no_no_reserved_state", - "kind": "controller", + "id": "workflow_replace_error_import_then_yes_no_no_continuation_state", + "kind": "workflow", "initial_state": { "premise": null, "policies": {}, diff --git a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_change_premise.json b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_change_premise.json similarity index 93% rename from tests/fixtures/conformance/controller/controller_step_apply_equivalence_change_premise.json rename to tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_change_premise.json index 882f55d..b68282d 100644 --- a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_change_premise.json +++ b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_change_premise.json @@ -1,6 +1,6 @@ { - "id": "controller_step_apply_equivalence_change_premise", - "kind": "controller", + "id": "workflow_step_apply_equivalence_change_premise", + "kind": "workflow", "initial_state": { "premise": "verbose replies", "policies": {}, diff --git a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_clear_premise.json b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_clear_premise.json similarity index 93% rename from tests/fixtures/conformance/controller/controller_step_apply_equivalence_clear_premise.json rename to tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_clear_premise.json index 5194aee..90962e6 100644 --- a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_clear_premise.json +++ b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_clear_premise.json @@ -1,6 +1,6 @@ { - "id": "controller_step_apply_equivalence_clear_premise", - "kind": "controller", + "id": "workflow_step_apply_equivalence_clear_premise", + "kind": "workflow", "initial_state": { "premise": "baseline", "policies": { diff --git a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_clear_state.json b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_clear_state.json similarity index 93% rename from tests/fixtures/conformance/controller/controller_step_apply_equivalence_clear_state.json rename to tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_clear_state.json index b8dc562..efbb680 100644 --- a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_clear_state.json +++ b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_clear_state.json @@ -1,6 +1,6 @@ { - "id": "controller_step_apply_equivalence_clear_state", - "kind": "controller", + "id": "workflow_step_apply_equivalence_clear_state", + "kind": "workflow", "initial_state": { "premise": "baseline", "policies": { diff --git a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_prohibit_item.json b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_prohibit_item.json similarity index 93% rename from tests/fixtures/conformance/controller/controller_step_apply_equivalence_prohibit_item.json rename to tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_prohibit_item.json index cee2ca0..cef00f9 100644 --- a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_prohibit_item.json +++ b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_prohibit_item.json @@ -1,6 +1,6 @@ { - "id": "controller_step_apply_equivalence_prohibit_item", - "kind": "controller", + "id": "workflow_step_apply_equivalence_prohibit_item", + "kind": "workflow", "initial_state": { "premise": null, "policies": {}, diff --git a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_remove_policy.json b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_remove_policy.json similarity index 92% rename from tests/fixtures/conformance/controller/controller_step_apply_equivalence_remove_policy.json rename to tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_remove_policy.json index d54df2d..7dbaf4d 100644 --- a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_remove_policy.json +++ b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_remove_policy.json @@ -1,6 +1,6 @@ { - "id": "controller_step_apply_equivalence_remove_policy", - "kind": "controller", + "id": "workflow_step_apply_equivalence_remove_policy", + "kind": "workflow", "initial_state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_replace_use.json b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_replace_use.json similarity index 93% rename from tests/fixtures/conformance/controller/controller_step_apply_equivalence_replace_use.json rename to tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_replace_use.json index 655e8ef..a08fb34 100644 --- a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_replace_use.json +++ b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_replace_use.json @@ -1,6 +1,6 @@ { - "id": "controller_step_apply_equivalence_replace_use", - "kind": "controller", + "id": "workflow_step_apply_equivalence_replace_use", + "kind": "workflow", "initial_state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_reset_policies.json b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_reset_policies.json similarity index 92% rename from tests/fixtures/conformance/controller/controller_step_apply_equivalence_reset_policies.json rename to tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_reset_policies.json index ba98831..c6f3339 100644 --- a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_reset_policies.json +++ b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_reset_policies.json @@ -1,6 +1,6 @@ { - "id": "controller_step_apply_equivalence_reset_policies", - "kind": "controller", + "id": "workflow_step_apply_equivalence_reset_policies", + "kind": "workflow", "initial_state": { "premise": "baseline", "policies": { diff --git a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_set_premise.json b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_set_premise.json similarity index 93% rename from tests/fixtures/conformance/controller/controller_step_apply_equivalence_set_premise.json rename to tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_set_premise.json index 0678ea8..3348e60 100644 --- a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_set_premise.json +++ b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_set_premise.json @@ -1,6 +1,6 @@ { - "id": "controller_step_apply_equivalence_set_premise", - "kind": "controller", + "id": "workflow_step_apply_equivalence_set_premise", + "kind": "workflow", "initial_state": { "premise": null, "policies": {}, diff --git a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_use_item.json b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_use_item.json similarity index 93% rename from tests/fixtures/conformance/controller/controller_step_apply_equivalence_use_item.json rename to tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_use_item.json index eab3782..17bd189 100644 --- a/tests/fixtures/conformance/controller/controller_step_apply_equivalence_use_item.json +++ b/tests/fixtures/conformance/workflow/workflow_step_apply_equivalence_use_item.json @@ -1,6 +1,6 @@ { - "id": "controller_step_apply_equivalence_use_item", - "kind": "controller", + "id": "workflow_step_apply_equivalence_use_item", + "kind": "workflow", "initial_state": { "premise": null, "policies": {}, diff --git a/tests/grammar-api-contract.test.ts b/tests/grammar-api-contract.test.ts index 18581b1..d3f0325 100644 --- a/tests/grammar-api-contract.test.ts +++ b/tests/grammar-api-contract.test.ts @@ -81,7 +81,7 @@ describe('public grammar API parity contract (conformance fixture)', () => { .construction_probes as Array>; for (const probe of probes) { const construct = () => constructGrammar('CanonicalDirective', probe); - if (probe.raises != null) { + if (probe.raises != null || probe.rejects === true) { expect(construct).toThrowError(); continue; } @@ -104,7 +104,7 @@ describe('public grammar API parity contract (conformance fixture)', () => { .construction_probes as Array>; for (const probe of probes) { const construct = () => constructGrammar('DirectiveMetadata', probe); - if (probe.raises != null) { + if (probe.raises != null || probe.rejects === true) { expect(construct).toThrowError(TypeError); continue; } @@ -122,7 +122,7 @@ describe('public grammar API parity contract (conformance fixture)', () => { it('matches declared public grammar object fields', () => { for (const [name, member] of Object.entries(contract.exports.members)) { if (member.public_fields === undefined) continue; - const probe = (member.construction_probes ?? []).find((candidate) => candidate.raises == null); + const probe = (member.construction_probes ?? []).find((candidate) => candidate.raises == null && candidate.rejects !== true); expect(probe, `${name} requires a successful construction probe`).toBeDefined(); if (probe === undefined) continue; const actual = constructGrammar(name, probe) as object; @@ -182,7 +182,7 @@ describe('public grammar API parity contract (conformance fixture)', () => { ? [] : [kwargs.failure, kwargs.directive_kind, kwargs.missing_operand, ...(Object.keys(kwargs).includes('unexpected') ? [true] : [])]; const construct = () => new grammar.InvalidDirectiveSyntax(...args as [string?, grammar.DirectiveKindValue?, string?]); - if (probe.raises != null) { + if (probe.raises != null || probe.rejects === true) { expect(construct).toThrowError(TypeError); continue; } diff --git a/tests/grammar-fixtures.test.ts b/tests/grammar-fixtures.test.ts index 4b4ed4b..9f3b361 100644 --- a/tests/grammar-fixtures.test.ts +++ b/tests/grammar-fixtures.test.ts @@ -10,6 +10,25 @@ describe('grammar fixtures (conformance)', () => { it(fixture.name, () => { const publicGrammar = grammar as unknown as Record; const fn = fixture.payload.action.fn === 'render_directive' ? render_directive : publicGrammar[fixture.payload.action.fn]; + if (fixture.payload.action.fn === 'construct_canonical_directive') { + let result: unknown; + try { + result = new grammar.CanonicalDirective( + fixture.payload.action.kind as string, + fixture.payload.action.operands as Record + ); + } catch (error) { + if (fixture.payload.expected.error == null) throw error; + expect(String(error)).toContain(fixture.payload.expected.error.message_contains); + return; + } + expect(fixture.payload.expected.error, `${fixture.name}: expected an error`).toBeUndefined(); + expect(result).toMatchObject({ + kind: fixture.payload.expected.directive_kind, + text: fixture.payload.expected.text + }); + return; + } expect(typeof fn, `${fixture.name}: missing grammar export '${fixture.payload.action.fn}'`).toBe('function'); if (fixture.payload.action.fn === 'decompose_directive') { diff --git a/tests/harness/fixtures.ts b/tests/harness/fixtures.ts index e4b8899..c61b8fe 100644 --- a/tests/harness/fixtures.ts +++ b/tests/harness/fixtures.ts @@ -28,7 +28,7 @@ export interface StateJsonFixtureCase { payload?: string; state: Record; error?: { - type: string; + type?: string; message_contains: string; }; }; @@ -53,18 +53,38 @@ export interface ControllerFixtureCase { }; } +export interface WorkflowFixtureCase { + id: string; + kind: 'workflow'; + initial_state: Record; + operations: Array<{ + fn: 'step' | 'apply_directive' | 'apply_repair' | 'export_json' | 'import_json'; + input?: string; + text?: string; + payload?: string; + payload_ref?: string; + decision_ref?: string; + repair_index?: number; + label?: string; + }>; + expected: { observations: Record; equal: string[][]; state: Record }; +} + export interface GrammarFixtureCase { id: string; kind: 'grammar'; action: { - fn: 'decompose_directive' | 'render_directive'; + fn: 'decompose_directive' | 'render_directive' | 'construct_canonical_directive'; text?: string; kind?: string; operands?: Record; }; expected: { directive?: Record | null; - error?: { type: string; message_contains: string }; + directive_kind?: string; + text?: string; + operands?: Record; + error?: { type?: string; message_contains: string }; }; } @@ -72,7 +92,7 @@ export interface ApplyDirectiveFixtureCase { id: string; kind: 'apply_directive'; initial_state: Record; - action: { fn: 'apply_directive'; text: string }; + action: { fn: 'apply_directive'; text?: string; directive?: { kind: string; operands: Record } }; expected: { decision: Record; state: Record; @@ -85,7 +105,7 @@ export interface MutationIsolationFixtureCase { initial_state: Record; prelude?: string[]; operation: { - fn: 'engine.step' | 'engine.policies' | 'engine.premise' | 'canonical_directive.operands' | 'directive_metadata'; + fn: 'engine.step' | 'engine.policies' | 'engine.premise' | 'canonical_directive.operands' | 'canonical_directive.constructor_operands' | 'directive_metadata'; input?: string; kind?: string; operands?: Record; @@ -185,4 +205,8 @@ export async function loadMutationIsolationFixtures(): Promise('mutation-isolation'); } +export async function loadWorkflowFixtures(): Promise[]> { + return loadFixtureFiles('workflow'); +} + export { FIXTURE_ROOT }; diff --git a/tests/mutation-isolation-fixtures.test.ts b/tests/mutation-isolation-fixtures.test.ts index 073e4de..9f8edbf 100644 --- a/tests/mutation-isolation-fixtures.test.ts +++ b/tests/mutation-isolation-fixtures.test.ts @@ -30,6 +30,7 @@ describe('mutation-isolation fixtures (conformance)', () => { engine.import_json(JSON.stringify(fixture.payload.initial_state)); const operation = fixture.payload.operation; let result: unknown; + const handleValues: Record = {}; for (const priorInput of fixture.payload.prelude ?? []) { engine.step(priorInput); @@ -48,6 +49,10 @@ describe('mutation-isolation fixtures (conformance)', () => { } else if (operation.fn === 'canonical_directive.operands') { const directive = new CanonicalDirective(operation.kind as string, operation.operands as Record); result = directive.operands; + } else if (operation.fn === 'canonical_directive.constructor_operands') { + const sourceOperands = { ...(operation.operands as Record) }; + handleValues.source_operands = sourceOperands; + result = new CanonicalDirective(operation.kind as string, sourceOperands); } else if (operation.fn === 'directive_metadata') { result = new DirectiveMetadata( operation.kind as 'use_item', @@ -65,9 +70,12 @@ describe('mutation-isolation fixtures (conformance)', () => { operation.fn === 'canonical_directive.operands' || operation.fn === 'directive_metadata'; for (const mutation of fixture.payload.mutations) { - expect(mutation.target_handle).toBe(operation.result_handle); expect(mutation.op).toBe('set'); - const mutate = () => setPath(result, mutation.path, mutation.value); + const target = mutation.target_handle === operation.result_handle + ? result + : handleValues[mutation.target_handle]; + expect(target, `${fixture.name}: missing mutation target '${mutation.target_handle}'`).toBeDefined(); + const mutate = () => setPath(target, mutation.path, mutation.value); if (immutableResult) { expect(mutate, `${fixture.name}: mutation should be rejected`).toThrow(); } else { diff --git a/tests/controller-fixtures.test.ts b/tests/workflow-fixtures.test.ts similarity index 68% rename from tests/controller-fixtures.test.ts rename to tests/workflow-fixtures.test.ts index e07c17c..f778b80 100644 --- a/tests/controller-fixtures.test.ts +++ b/tests/workflow-fixtures.test.ts @@ -1,15 +1,13 @@ import { describe, expect, it } from 'vitest'; import { Engine } from '../src/engine.js'; import { CanonicalDirective, decompose_directive } from '../src/grammar.js'; -import { loadControllerFixtures } from './harness/fixtures.js'; +import { loadWorkflowFixtures } from './harness/fixtures.js'; -const fixtures = await loadControllerFixtures(); +const fixtures = await loadWorkflowFixtures(); -describe('controller fixtures (conformance)', () => { +describe('workflow fixtures (conformance)', () => { for (const fixture of fixtures) { it(fixture.name, () => { - expect(fixture.payload.kind).toBe('controller'); - const engine = new Engine(); engine.import_json(JSON.stringify(fixture.payload.initial_state)); const observations: Record = {}; @@ -22,13 +20,16 @@ describe('controller fixtures (conformance)', () => { result = engine.step(operation.input as string); break; case 'apply_directive': { - const applyDirective = (engine as unknown as Record).apply_directive; - expect(typeof applyDirective, `${fixture.name}: Engine.apply_directive is required by this fixture`).toBe( - 'function' - ); const directive = decompose_directive(operation.text as string); expect(directive, `${fixture.name}: apply_directive input must be canonical`).toBeInstanceOf(CanonicalDirective); - result = (applyDirective as (value: CanonicalDirective) => unknown).call(engine, directive); + result = engine.apply_directive(directive as CanonicalDirective); + break; + } + case 'apply_repair': { + const decision = observations[operation.decision_ref as string] as { repairs: CanonicalDirective[] }; + const repair = decision?.repairs?.[operation.repair_index as number]; + expect(repair, `${fixture.name}: repair reference is missing`).toBeInstanceOf(CanonicalDirective); + result = engine.apply_directive(repair); break; } case 'export_json': @@ -41,13 +42,13 @@ describe('controller fixtures (conformance)', () => { result = undefined; break; } + default: + throw new Error(`${fixture.name}: unsupported workflow operation '${operation.fn}'`); } if (operation.label != null) { observations[operation.label] = result; - if (operation.fn === 'export_json') { - payloads[operation.label] = result as string; - } + if (operation.fn === 'export_json') payloads[operation.label] = result as string; } } From f02a0c2492696f5d82c3c4bb61a36c1834e24c18 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 03:13:20 -0400 Subject: [PATCH 2/7] fix: use complete Unicode case folding for policy identity --- package-lock.json | 9 +++++++++ package.json | 3 +++ src/engine.ts | 9 +-------- src/unicode.ts | 6 ++++++ src/vendor.d.ts | 12 ++++++++++++ tests/normalization_parity.test.ts | 9 +++++++++ 6 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src/unicode.ts create mode 100644 src/vendor.d.ts diff --git a/package-lock.json b/package-lock.json index c69cd9c..5a2e335 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,20 @@ "name": "@rlippmann/context-compiler", "version": "0.9.0-dev.1", "license": "Apache-2.0", + "dependencies": { + "@ar-nelson/foldcase": "^1.0.1" + }, "devDependencies": { "typescript": "^5.9.3", "vitest": "^3.2.4" } }, + "node_modules/@ar-nelson/foldcase": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@ar-nelson/foldcase/-/foldcase-1.0.1.tgz", + "integrity": "sha512-CHlMkXpG3l3PA/EV5rxRcBm7J3V9oBcpvbo5OgX3TP4ErEGi90DKcVHHKDBJYSiNzP3AkI0IqiaC+DY+EoKdFQ==", + "license": "ISC" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", diff --git a/package.json b/package.json index bd38f4a..ad3795a 100644 --- a/package.json +++ b/package.json @@ -58,5 +58,8 @@ "devDependencies": { "typescript": "^5.9.3", "vitest": "^3.2.4" + }, + "dependencies": { + "@ar-nelson/foldcase": "^1.0.1" } } diff --git a/src/engine.ts b/src/engine.ts index f850b57..7770439 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -7,6 +7,7 @@ import { SemanticFailure, UpdateDecision } from './decision.js'; +import { unicodeCaseFold } from './unicode.js'; export const POLICY_USE = 'use' as const; export const POLICY_PROHIBIT = 'prohibit' as const; @@ -277,14 +278,6 @@ function normalizeItem(value: string): string { return normalized.trim(); } -function unicodeCaseFold(value: string): string { - return value - .toLowerCase() - .replaceAll('ß', 'ss') - .replaceAll('ς', 'σ') - .replaceAll('ſ', 's'); -} - function sortKeysDeep(value: unknown): unknown { if (Array.isArray(value)) { return value.map((v) => sortKeysDeep(v)); diff --git a/src/unicode.ts b/src/unicode.ts new file mode 100644 index 0000000..28c4789 --- /dev/null +++ b/src/unicode.ts @@ -0,0 +1,6 @@ +import foldcase from '@ar-nelson/foldcase'; + +/** Apply Unicode Default Case Folding (the full, potentially expanding form). */ +export function unicodeCaseFold(value: string): string { + return foldcase.full(value); +} diff --git a/src/vendor.d.ts b/src/vendor.d.ts new file mode 100644 index 0000000..bc62303 --- /dev/null +++ b/src/vendor.d.ts @@ -0,0 +1,12 @@ +declare module '@ar-nelson/foldcase' { + interface Foldcase { + (value: string): string; + full(value: string): string; + simple(value: string): string; + charFull(value: string): string; + charSimple(value: string): string; + } + + const foldcase: Foldcase; + export default foldcase; +} diff --git a/tests/normalization_parity.test.ts b/tests/normalization_parity.test.ts index 634f2c5..8de7659 100644 --- a/tests/normalization_parity.test.ts +++ b/tests/normalization_parity.test.ts @@ -10,6 +10,15 @@ describe('normalization parity', () => { expect(JSON.parse(engine.export_json()).policies).toEqual({ 'the docker cli': 'use' }); }); + it('uses full Unicode case folding for policy identity', () => { + const engine = new Engine(); + engine.step('use ǰ'); + const decision = engine.step('use J̌'); + + expect(decision).toMatchObject({ kind: 'update', changed: false }); + expect(JSON.parse(engine.export_json()).policies).toEqual({ 'ǰ': 'use' }); + }); + it('normalizes apostrophes without rewriting distinct operands', () => { const engine = new Engine(); engine.step('use Don’t panic'); From 662ce373f18e044669fb99d8075aa001c6512e14 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 14:12:27 -0400 Subject: [PATCH 3/7] test: consume strengthened Python parity fixtures --- tests/fixtures/.source-commit | 2 +- ...nge_premise_newline_compound_rejected.json | 16 ++++++ ...ose_opaque_change_premise_conjunction.json | 17 +++++++ ..._structural_delimiter_not_replacement.json | 17 +++++++ ...tity_capital_sharp_s_casefold_message.json | 50 +++++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_change_premise_newline_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_unicode_casefold_structural_delimiter_not_replacement.json create mode 100644 tests/fixtures/conformance/step/step_policy_identity_capital_sharp_s_casefold_message.json diff --git a/tests/fixtures/.source-commit b/tests/fixtures/.source-commit index 505b958..0be0f3a 100644 --- a/tests/fixtures/.source-commit +++ b/tests/fixtures/.source-commit @@ -1 +1 @@ -01cac9c154eac33675cbd9a207eec085fde58d06 +fa6075c4c2d8b3de5d93a7b989695222263916ad diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_newline_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_newline_compound_rejected.json new file mode 100644 index 0000000..2a62a69 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_newline_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_change_premise_newline_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "change premise to first\nchange premise to second" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json new file mode 100644 index 0000000..f8d9459 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json @@ -0,0 +1,17 @@ +{ + "id": "grammar_decompose_opaque_change_premise_conjunction", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "change premise to vegetarian and use docker" + }, + "expected": { + "directive": { + "text": "change premise to vegetarian and use docker", + "kind": "change_premise", + "operands": { + "value": "vegetarian and use docker" + } + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_unicode_casefold_structural_delimiter_not_replacement.json b/tests/fixtures/conformance/grammar/grammar_decompose_unicode_casefold_structural_delimiter_not_replacement.json new file mode 100644 index 0000000..2d5655d --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_unicode_casefold_structural_delimiter_not_replacement.json @@ -0,0 +1,17 @@ +{ + "id": "grammar_decompose_unicode_casefold_structural_delimiter_not_replacement", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "use podman inſtead of docker" + }, + "expected": { + "directive": { + "kind": "use_item", + "text": "use podman inſtead of docker", + "operands": { + "item": "podman inſtead of docker" + } + } + } +} diff --git a/tests/fixtures/conformance/step/step_policy_identity_capital_sharp_s_casefold_message.json b/tests/fixtures/conformance/step/step_policy_identity_capital_sharp_s_casefold_message.json new file mode 100644 index 0000000..00d9e03 --- /dev/null +++ b/tests/fixtures/conformance/step/step_policy_identity_capital_sharp_s_casefold_message.json @@ -0,0 +1,50 @@ +{ + "id": "step_policy_identity_capital_sharp_s_casefold_message", + "kind": "step", + "initial_state": { + "premise": null, + "policies": {}, + "version": 2 + }, + "prelude": [ + "use ẞ" + ], + "input": "prohibit SS", + "expected": { + "decision": { + "kind": "error", + "failure": "item_already_in_use", + "directive": { + "kind": "prohibit_item", + "text": "prohibit SS", + "operands": { + "item": "SS" + } + }, + "repairs": [ + { + "kind": "remove_policy", + "text": "remove policy SS", + "operands": { + "item": "SS" + } + }, + { + "kind": "prohibit_item", + "text": "prohibit SS", + "operands": { + "item": "SS" + } + } + ], + "message": "\"ss\" is currently in use.\nRemove or replace it before prohibiting it." + }, + "state": { + "premise": null, + "policies": { + "ss": "use" + }, + "version": 2 + } + } +} From 3e4012933dc9951bf01a808c93caea53589941ac Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 14:12:46 -0400 Subject: [PATCH 4/7] fix: align Unicode and premise grammar parity with Python --- src/decision.ts | 12 ++++-------- src/grammar.ts | 24 ++++++++++++++++++++---- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/decision.ts b/src/decision.ts index 8616612..c1869c5 100644 --- a/src/decision.ts +++ b/src/decision.ts @@ -1,4 +1,5 @@ import { CanonicalDirective } from './grammar.js'; +import { unicodeCaseFold } from './unicode.js'; export const DECISION_ERROR = 'error' as const; export const DECISION_NO_DIRECTIVE = 'no_directive' as const; @@ -89,16 +90,11 @@ function formatFailure(failure: string, directive: CanonicalDirective): string { } function normalizeItemForMessage(value: string): string { - return value + const normalized = value .normalize('NFKC') .replaceAll('’', "'") - .replaceAll('`', "'") - .toLowerCase() - .replaceAll('ß', 'ss') - .replaceAll('ς', 'σ') - .replaceAll('ſ', 's') - .replace(/\s+/g, ' ') - .trim(); + .replaceAll('`', "'"); + return unicodeCaseFold(normalized).replace(/\s+/g, ' ').trim(); } function freezeDirective(directive: CanonicalDirective): CanonicalDirective { diff --git a/src/grammar.ts b/src/grammar.ts index febeb12..4b24832 100644 --- a/src/grammar.ts +++ b/src/grammar.ts @@ -81,7 +81,11 @@ export class CanonicalDirective { const kind = normalizeDirectiveKind(kindInput); const operands = normalizeCanonicalOperands(kind, operandsInput); const rendered = serializeCanonicalDirective(kind, operands); - if (kind !== DirectiveKind.SET_PREMISE && containsMultipleCanonicalDirectives(rendered)) { + const containsCompound = + kind === DirectiveKind.SET_PREMISE || kind === DirectiveKind.CHANGE_PREMISE + ? containsMultiplePremiseDirectives(rendered) + : containsMultipleCanonicalDirectives(rendered); + if (containsCompound) { throw new Error(`Operands do not produce a canonical ${kind} directive.`); } this.kind = kind; @@ -210,6 +214,15 @@ function containsMultipleCanonicalDirectives(text: string): boolean { return false; } +function containsMultiplePremiseDirectives(text: string): boolean { + const first = matchCanonicalStart(text, 0); + if (first == null) return false; + for (let index = first; index < text.length; index += 1) { + if (text[index] === '\n' && matchCanonicalStart(text, index + 1) != null) return true; + } + return false; +} + function startsWithDirectiveFamily(text: string): boolean { return ( matchDirectiveToken(text, 0, 'change premise', true) != null || @@ -243,9 +256,12 @@ function parseReplacement(text: string): CanonicalDirective | null { export function decompose_directive(text: string): CanonicalDirective | InvalidDirectiveSyntax | null { const trimmed = trimAsciiWhitespace(text); if (trimmed === '' || !startsWithDirectiveFamily(trimmed)) return null; - // A set-premise operand is opaque: directive-shaped text inside it is premise - // content, not a second directive. - if (!normalizedForMatching(trimmed).startsWith('set premise ') && containsMultipleCanonicalDirectives(trimmed)) { + const premiseDirective = + matchDirectiveToken(trimmed, 0, 'set premise', true) != null || + matchDirectiveToken(trimmed, 0, 'change premise to', true) != null; + if ( + (premiseDirective ? containsMultiplePremiseDirectives(trimmed) : containsMultipleCanonicalDirectives(trimmed)) + ) { return invalid(DirectiveSyntaxFailure.COMPOUND_DIRECTIVE); } From 2682edca485aa942848105be53072069627ca96d Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 22:59:00 -0400 Subject: [PATCH 5/7] test: sync Python 0.9.1 conformance changes --- tests/fixtures/.source-commit | 2 +- ...ompose_change_premise_compound_rejected.json | 16 ++++++++++++++++ ...se_change_premise_use_compound_rejected.json | 16 ++++++++++++++++ ...ssword_authentication_compound_rejected.json | 16 ++++++++++++++++ ...mpose_opaque_change_premise_conjunction.json | 17 ----------------- ...ar_decompose_opaque_premise_conjunction.json | 17 ----------------- ...ammar_decompose_opaque_premise_prohibit.json | 17 ----------------- .../grammar_decompose_opaque_premise_use.json | 17 ----------------- ...premise_clear_premise_compound_rejected.json | 16 ++++++++++++++++ ...t_premise_clear_state_compound_rejected.json | 16 ++++++++++++++++ ...decompose_set_premise_compound_rejected.json | 16 ++++++++++++++++ ..._set_premise_prohibit_compound_rejected.json | 16 ++++++++++++++++ ...premise_remove_policy_compound_rejected.json | 16 ++++++++++++++++ ...remise_reset_policies_compound_rejected.json | 16 ++++++++++++++++ ...mpose_set_premise_use_compound_rejected.json | 16 ++++++++++++++++ ...premise_users_use_sso_compound_rejected.json | 16 ++++++++++++++++ ...nd_set_premise_and_use_invalid_boundary.json | 5 ++--- 17 files changed, 179 insertions(+), 72 deletions(-) create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_change_premise_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_change_premise_use_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected.json delete mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json delete mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_conjunction.json delete mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_prohibit.json delete mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_use.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_premise_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_state_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_prohibit_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_remove_policy_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_reset_policies_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_use_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_users_use_sso_compound_rejected.json diff --git a/tests/fixtures/.source-commit b/tests/fixtures/.source-commit index 0be0f3a..0e305c8 100644 --- a/tests/fixtures/.source-commit +++ b/tests/fixtures/.source-commit @@ -1 +1 @@ -fa6075c4c2d8b3de5d93a7b989695222263916ad +7ffc19c53ef5c41e4e421f4fd2958655e52737c9 diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_compound_rejected.json new file mode 100644 index 0000000..093baee --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_change_premise_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "change premise to vegetarian and use docker" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_use_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_use_compound_rejected.json new file mode 100644 index 0000000..1d07bbc --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_use_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_change_premise_use_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "change premise to deployment target is staging, then use cautious rollout" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected.json new file mode 100644 index 0000000..d20f2d7 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "change premise to users prohibit password authentication" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json deleted file mode 100644 index f8d9459..0000000 --- a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "grammar_decompose_opaque_change_premise_conjunction", - "kind": "grammar", - "action": { - "fn": "decompose_directive", - "text": "change premise to vegetarian and use docker" - }, - "expected": { - "directive": { - "text": "change premise to vegetarian and use docker", - "kind": "change_premise", - "operands": { - "value": "vegetarian and use docker" - } - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_conjunction.json b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_conjunction.json deleted file mode 100644 index c6da6f1..0000000 --- a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_conjunction.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "grammar_decompose_opaque_premise_conjunction", - "kind": "grammar", - "action": { - "fn": "decompose_directive", - "text": "set premise vegetarian and use docker" - }, - "expected": { - "directive": { - "text": "set premise vegetarian and use docker", - "kind": "set_premise", - "operands": { - "value": "vegetarian and use docker" - } - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_prohibit.json b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_prohibit.json deleted file mode 100644 index 4a9337a..0000000 --- a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_prohibit.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "grammar_decompose_opaque_premise_prohibit", - "kind": "grammar", - "action": { - "fn": "decompose_directive", - "text": "set premise users may prohibit unsafe operations" - }, - "expected": { - "directive": { - "text": "set premise users may prohibit unsafe operations", - "kind": "set_premise", - "operands": { - "value": "users may prohibit unsafe operations" - } - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_use.json b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_use.json deleted file mode 100644 index 27748c3..0000000 --- a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_use.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "grammar_decompose_opaque_premise_use", - "kind": "grammar", - "action": { - "fn": "decompose_directive", - "text": "set premise we must use gaap" - }, - "expected": { - "directive": { - "text": "set premise we must use gaap", - "kind": "set_premise", - "operands": { - "value": "we must use gaap" - } - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_premise_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_premise_compound_rejected.json new file mode 100644 index 0000000..938af49 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_premise_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_clear_premise_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise clear premise before release" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_state_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_state_compound_rejected.json new file mode 100644 index 0000000..ca8eeae --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_state_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_clear_state_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise clear state before starting" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_compound_rejected.json new file mode 100644 index 0000000..4d8f291 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise concise replies and change premise to formal tone" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_prohibit_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_prohibit_compound_rejected.json new file mode 100644 index 0000000..70afeb7 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_prohibit_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_prohibit_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise users may prohibit unsafe operations" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_remove_policy_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_remove_policy_compound_rejected.json new file mode 100644 index 0000000..8966751 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_remove_policy_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_remove_policy_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise cleanup requires remove policy docker" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_reset_policies_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_reset_policies_compound_rejected.json new file mode 100644 index 0000000..557d233 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_reset_policies_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_reset_policies_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise reset policies after migration" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_use_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_use_compound_rejected.json new file mode 100644 index 0000000..7a7b4d6 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_use_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_use_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise we must use gaap" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_users_use_sso_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_users_use_sso_compound_rejected.json new file mode 100644 index 0000000..a4ad59a --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_users_use_sso_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_users_use_sso_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise users use SSO" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json b/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json index 73e61ae..89acb74 100644 --- a/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json @@ -9,11 +9,10 @@ "input": "set premise vegetarian and use docker", "expected": { "decision": { - "kind": "update", - "changed": true + "kind": "no_directive" }, "state": { - "premise": "vegetarian and use docker", + "premise": null, "policies": {}, "version": 2 } From cdbea57482c7d3642348c04e47bf0bd0ecb82468 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 23:01:48 -0400 Subject: [PATCH 6/7] fix: classify premise compound directives consistently --- src/grammar.ts | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/grammar.ts b/src/grammar.ts index 4b24832..081f0e2 100644 --- a/src/grammar.ts +++ b/src/grammar.ts @@ -81,11 +81,7 @@ export class CanonicalDirective { const kind = normalizeDirectiveKind(kindInput); const operands = normalizeCanonicalOperands(kind, operandsInput); const rendered = serializeCanonicalDirective(kind, operands); - const containsCompound = - kind === DirectiveKind.SET_PREMISE || kind === DirectiveKind.CHANGE_PREMISE - ? containsMultiplePremiseDirectives(rendered) - : containsMultipleCanonicalDirectives(rendered); - if (containsCompound) { + if (containsMultipleCanonicalDirectives(rendered)) { throw new Error(`Operands do not produce a canonical ${kind} directive.`); } this.kind = kind; @@ -214,15 +210,6 @@ function containsMultipleCanonicalDirectives(text: string): boolean { return false; } -function containsMultiplePremiseDirectives(text: string): boolean { - const first = matchCanonicalStart(text, 0); - if (first == null) return false; - for (let index = first; index < text.length; index += 1) { - if (text[index] === '\n' && matchCanonicalStart(text, index + 1) != null) return true; - } - return false; -} - function startsWithDirectiveFamily(text: string): boolean { return ( matchDirectiveToken(text, 0, 'change premise', true) != null || @@ -256,12 +243,7 @@ function parseReplacement(text: string): CanonicalDirective | null { export function decompose_directive(text: string): CanonicalDirective | InvalidDirectiveSyntax | null { const trimmed = trimAsciiWhitespace(text); if (trimmed === '' || !startsWithDirectiveFamily(trimmed)) return null; - const premiseDirective = - matchDirectiveToken(trimmed, 0, 'set premise', true) != null || - matchDirectiveToken(trimmed, 0, 'change premise to', true) != null; - if ( - (premiseDirective ? containsMultiplePremiseDirectives(trimmed) : containsMultipleCanonicalDirectives(trimmed)) - ) { + if (containsMultipleCanonicalDirectives(trimmed)) { return invalid(DirectiveSyntaxFailure.COMPOUND_DIRECTIVE); } From c90b6e85fd40a83d73d5c8578fc1119d7c8b31c9 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 23:06:39 -0400 Subject: [PATCH 7/7] test: remove retired render-directive harness support --- tests/grammar-fixtures.test.ts | 3 +-- tests/harness/fixtures.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/grammar-fixtures.test.ts b/tests/grammar-fixtures.test.ts index 9f3b361..d4c367d 100644 --- a/tests/grammar-fixtures.test.ts +++ b/tests/grammar-fixtures.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; import * as grammar from '../src/grammar.js'; -import { render_directive } from '../src/grammar-render.js'; import { loadGrammarFixtures } from './harness/fixtures.js'; const fixtures = await loadGrammarFixtures(); @@ -9,7 +8,7 @@ describe('grammar fixtures (conformance)', () => { for (const fixture of fixtures) { it(fixture.name, () => { const publicGrammar = grammar as unknown as Record; - const fn = fixture.payload.action.fn === 'render_directive' ? render_directive : publicGrammar[fixture.payload.action.fn]; + const fn = publicGrammar[fixture.payload.action.fn]; if (fixture.payload.action.fn === 'construct_canonical_directive') { let result: unknown; try { diff --git a/tests/harness/fixtures.ts b/tests/harness/fixtures.ts index c61b8fe..abf6bac 100644 --- a/tests/harness/fixtures.ts +++ b/tests/harness/fixtures.ts @@ -74,7 +74,7 @@ export interface GrammarFixtureCase { id: string; kind: 'grammar'; action: { - fn: 'decompose_directive' | 'render_directive' | 'construct_canonical_directive'; + fn: 'decompose_directive' | 'construct_canonical_directive'; text?: string; kind?: string; operands?: Record;