diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c49f858..7f4d271 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,9 @@ jobs: build-and-test: runs-on: ubuntu-latest timeout-minutes: 15 + env: + PY_FIXTURE_REF: dba101b662410c0ae0c85842cd78d8adb55f184b + PY_FIXTURE_CHECKOUT: ${{ runner.temp }}/context-compiler-source steps: - name: Checkout @@ -27,12 +30,22 @@ jobs: run: npm ci - name: Checkout Python fixture source - # TS 0.5.2 fixtures include cases added after Python v0.5.2; pin to v0.6.0 - # to keep fixture drift checks aligned with the current TS fixture corpus. - run: git clone --depth 1 --branch v0.6.0 https://github.com/rlippmann/context-compiler ../context-compiler + # Pin to an explicit Python commit SHA for deterministic fixture drift checks. + run: | + echo "Python fixture ref: ${PY_FIXTURE_REF}" + echo "Python checkout path: ${PY_FIXTURE_CHECKOUT}" + git init "${PY_FIXTURE_CHECKOUT}" + git -C "${PY_FIXTURE_CHECKOUT}" remote add origin https://github.com/rlippmann/context-compiler + git -C "${PY_FIXTURE_CHECKOUT}" fetch --depth 1 origin "${PY_FIXTURE_REF}" + git -C "${PY_FIXTURE_CHECKOUT}" checkout --detach FETCH_HEAD + echo "Resolved fixture source: ${PY_FIXTURE_CHECKOUT}/tests/fixtures/conformance" - name: Check fixture drift - run: npm run fixtures:check + env: + FIXTURES_SOURCE: ${{ runner.temp }}/context-compiler-source/tests/fixtures/conformance + run: | + echo "Using FIXTURES_SOURCE=${FIXTURES_SOURCE}" + npm run fixtures:check - name: Build run: npm run build diff --git a/AGENTS.md b/AGENTS.md index eca2d20..4f32f52 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ Behavior is defined by the upstream Python project: The following are authoritative: - Directive grammar specification -- Fixture corpus under `tests/fixtures/v2` +- Fixture corpus under `tests/fixtures/conformance` - Python engine behavior as exercised by those fixtures If behavior differs, the TypeScript implementation is incorrect. @@ -78,10 +78,46 @@ A change is correct only if all fixtures pass. Do not modify fixtures to make tests pass. -Fixture updates are allowed only when syncing from the authoritative Python source for the targeted compatibility line. +Fixture updates must follow the process defined in 'Conformance fixtures' below. If synced fixtures introduce failures, fix TypeScript behavior rather than editing fixture expectations. +## Conformance fixtures + +Conformance fixtures are sourced from the Python repository and define the canonical cross-port contract. + +### Rules + +- Do not hand-edit fixture JSON files. +- Do not modify fixture JSON to make TS tests pass. +- TS implementation must conform to the fixtures, not the reverse. +- Fixture JSON files are read-only artifacts in this repository. + +### Updating fixtures + +When fixtures need to be updated: + +1. Obtain the Python source of truth (pinned commit or tag). +2. Run the sync script with an explicit source: + + FIXTURES_SOURCE=/tests/fixtures/conformance npm run fixtures:sync + +3. Commit only the resulting changes. + +### Verification + +After syncing: + +npm test +FIXTURES_SOURCE= npm run fixtures:check + +### Constraints + +- FIXTURES_SOURCE must be explicitly provided; no implicit local fallback is allowed. +- Do not copy/paste or manually edit fixture contents, even for single-file fixes. +- If a fixture appears incorrect, fix it in the Python repo first, then re-sync. +- Manual fixture JSON edits should be rejected in review. + ## Test Coverage Expectations Before opening a PR, consider: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b945285..b08df59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,19 +68,18 @@ All tests must pass before submitting changes. Python fixtures are the source of truth. -Default source path: +Conformance fixture policy: -- `../context-compiler/tests/fixtures/v2` +- Do not hand-edit fixture JSON files under `tests/fixtures/conformance`. +- Update conformance fixtures only via `npm run fixtures:sync` with explicit `FIXTURES_SOURCE` pointing to the Python source of truth. +- If fixture updates introduce test failures, update TypeScript implementation to conform to fixtures. Commands: -- `npm run fixtures:sync` to copy fixtures from Python into `tests/fixtures/v2` -- `npm run fixtures:check` to detect drift between local fixtures and Python fixtures +- `FIXTURES_SOURCE=/path/to/context-compiler/tests/fixtures/conformance npm run fixtures:sync` to copy fixtures from Python into `tests/fixtures/conformance` +- `FIXTURES_SOURCE=/path/to/context-compiler/tests/fixtures/conformance npm run fixtures:check` to detect drift between local fixtures and Python fixtures -Optional override: - -- Set `FIXTURES_SOURCE` to use a different source path, for example: - - `FIXTURES_SOURCE=/path/to/context-compiler/tests/fixtures/v2 npm run fixtures:check` +`FIXTURES_SOURCE` is required for both commands. ## Pull Requests diff --git a/scripts/fixtures-check.sh b/scripts/fixtures-check.sh index 8f3da2d..b845883 100755 --- a/scripts/fixtures-check.sh +++ b/scripts/fixtures-check.sh @@ -1,12 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -SOURCE_DIR="${FIXTURES_SOURCE:-../context-compiler/tests/fixtures/v2}" -TARGET_DIR="tests/fixtures/v2" +if [[ -z "${FIXTURES_SOURCE:-}" ]]; then + echo "[fixtures:check] FIXTURES_SOURCE is required." >&2 + echo "[fixtures:check] Example: FIXTURES_SOURCE=/path/to/context-compiler/tests/fixtures/conformance npm run fixtures:check" >&2 + exit 1 +fi + +SOURCE_DIR="$FIXTURES_SOURCE" +TARGET_DIR="tests/fixtures/conformance" + +echo "[fixtures:check] Using source fixture directory: $SOURCE_DIR" if [[ ! -d "$SOURCE_DIR" ]]; then echo "[fixtures:check] Source fixture directory not found: $SOURCE_DIR" >&2 - echo "[fixtures:check] Set FIXTURES_SOURCE to override the default source path." >&2 + echo "[fixtures:check] Set FIXTURES_SOURCE to a valid Python fixture source path." >&2 exit 1 fi diff --git a/scripts/fixtures-sync.sh b/scripts/fixtures-sync.sh index b7c9cf9..80e242f 100755 --- a/scripts/fixtures-sync.sh +++ b/scripts/fixtures-sync.sh @@ -1,12 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -SOURCE_DIR="${FIXTURES_SOURCE:-../context-compiler/tests/fixtures/v2}" -TARGET_DIR="tests/fixtures/v2" +if [[ -z "${FIXTURES_SOURCE:-}" ]]; then + echo "[fixtures:sync] FIXTURES_SOURCE is required." >&2 + echo "[fixtures:sync] Example: FIXTURES_SOURCE=/path/to/context-compiler/tests/fixtures/conformance npm run fixtures:sync" >&2 + exit 1 +fi + +SOURCE_DIR="$FIXTURES_SOURCE" +TARGET_DIR="tests/fixtures/conformance" + +echo "[fixtures:sync] Using source fixture directory: $SOURCE_DIR" if [[ ! -d "$SOURCE_DIR" ]]; then echo "[fixtures:sync] Source fixture directory not found: $SOURCE_DIR" >&2 - echo "[fixtures:sync] Set FIXTURES_SOURCE to override the default source path." >&2 + echo "[fixtures:sync] Set FIXTURES_SOURCE to a valid Python fixture source path." >&2 exit 1 fi diff --git a/src/engine.ts b/src/engine.ts index 3356934..4507981 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -235,18 +235,7 @@ class EngineImpl implements Engine { const oldState = this._state.policies[oldKey]; const newState = this._state.policies[newKey]; if (!Object.prototype.hasOwnProperty.call(this._state.policies, oldKey)) { - const promptLines = [ - `No exact policy found for "${action.old_item}".`, - 'Replacement requires an exact policy match.' - ]; - const diagnosticHints = diagnosticPolicyContainsHints(this._state.policies, action.old_item); - if (diagnosticHints !== '') { - promptLines.push(`Existing policies containing that text: ${diagnosticHints}.`); - promptLines.push(`Confirm to use "${action.new_item}" and keep ${diagnosticHints}?`); - } else { - promptLines.push(`Confirm to use "${action.new_item}" and keep existing policies?`); - } - const prompt = promptLines.join('\n'); + const prompt = `Did you mean to use "${action.new_item}" instead?`; this._pendingReplacement = { kind: 'use_only', new_item: action.new_item }; this._pendingPrompt = prompt; return clarify(prompt); diff --git a/tests/engine_state_immutability.test.ts b/tests/engine_state_immutability.test.ts index 84042b3..04d2a74 100644 --- a/tests/engine_state_immutability.test.ts +++ b/tests/engine_state_immutability.test.ts @@ -105,7 +105,7 @@ describe('engine error-path regressions', () => { expectClarifyNoMutation( missingSource, 'use kubectl instead of docker', - 'No exact policy found for "docker".\nReplacement requires an exact policy match.\nConfirm to use "kubectl" and keep existing policies?' + 'Did you mean to use "kubectl" instead?' ); const oldIsProhibit = createEngine({ state: { premise: null, policies: { docker: 'prohibit' }, version: 2 } }); diff --git a/tests/fixtures/v2/step/001_set_premise_update.json b/tests/fixtures/conformance/step/001_set_premise_update.json similarity index 100% rename from tests/fixtures/v2/step/001_set_premise_update.json rename to tests/fixtures/conformance/step/001_set_premise_update.json diff --git a/tests/fixtures/v2/step/002_use_item_normalization.json b/tests/fixtures/conformance/step/002_use_item_normalization.json similarity index 100% rename from tests/fixtures/v2/step/002_use_item_normalization.json rename to tests/fixtures/conformance/step/002_use_item_normalization.json diff --git a/tests/fixtures/v2/step/003_conflict_prohibit_clarify.json b/tests/fixtures/conformance/step/003_conflict_prohibit_clarify.json similarity index 100% rename from tests/fixtures/v2/step/003_conflict_prohibit_clarify.json rename to tests/fixtures/conformance/step/003_conflict_prohibit_clarify.json diff --git a/tests/fixtures/v2/step/004_remove_policy_missing_idempotent_update.json b/tests/fixtures/conformance/step/004_remove_policy_missing_idempotent_update.json similarity index 100% rename from tests/fixtures/v2/step/004_remove_policy_missing_idempotent_update.json rename to tests/fixtures/conformance/step/004_remove_policy_missing_idempotent_update.json diff --git a/tests/fixtures/v2/step/005_exact_prefix_passthrough_leading_space.json b/tests/fixtures/conformance/step/005_exact_prefix_passthrough_leading_space.json similarity index 100% rename from tests/fixtures/v2/step/005_exact_prefix_passthrough_leading_space.json rename to tests/fixtures/conformance/step/005_exact_prefix_passthrough_leading_space.json diff --git a/tests/fixtures/v2/step/006_near_miss_set_premise_to.json b/tests/fixtures/conformance/step/006_near_miss_set_premise_to.json similarity index 100% rename from tests/fixtures/v2/step/006_near_miss_set_premise_to.json rename to tests/fixtures/conformance/step/006_near_miss_set_premise_to.json diff --git a/tests/fixtures/v2/step/007_near_miss_change_premise_missing_to.json b/tests/fixtures/conformance/step/007_near_miss_change_premise_missing_to.json similarity index 100% rename from tests/fixtures/v2/step/007_near_miss_change_premise_missing_to.json rename to tests/fixtures/conformance/step/007_near_miss_change_premise_missing_to.json diff --git a/tests/fixtures/v2/step/008_replace_missing_source_clarify_prompt.json b/tests/fixtures/conformance/step/008_replace_missing_source_clarify_prompt.json similarity index 69% rename from tests/fixtures/v2/step/008_replace_missing_source_clarify_prompt.json rename to tests/fixtures/conformance/step/008_replace_missing_source_clarify_prompt.json index 4b87a0b..d5f476d 100644 --- a/tests/fixtures/v2/step/008_replace_missing_source_clarify_prompt.json +++ b/tests/fixtures/conformance/step/008_replace_missing_source_clarify_prompt.json @@ -10,7 +10,7 @@ "expected": { "decision": { "kind": "clarify", - "prompt_to_user": "No exact policy found for \"docker\".\nReplacement requires an exact policy match.\nConfirm to use \"kubectl\" and keep existing policies?", + "prompt_to_user": "Did you mean to use \"kubectl\" instead?", "state": null }, "state": { diff --git a/tests/fixtures/v2/step/009_pending_affirmative_normalized_token.json b/tests/fixtures/conformance/step/009_pending_affirmative_normalized_token.json similarity index 100% rename from tests/fixtures/v2/step/009_pending_affirmative_normalized_token.json rename to tests/fixtures/conformance/step/009_pending_affirmative_normalized_token.json diff --git a/tests/fixtures/v2/step/010_pending_negative_normalized_token.json b/tests/fixtures/conformance/step/010_pending_negative_normalized_token.json similarity index 100% rename from tests/fixtures/v2/step/010_pending_negative_normalized_token.json rename to tests/fixtures/conformance/step/010_pending_negative_normalized_token.json diff --git a/tests/fixtures/v2/step/011_pending_unmatched_reuses_prompt.json b/tests/fixtures/conformance/step/011_pending_unmatched_reuses_prompt.json similarity index 100% rename from tests/fixtures/v2/step/011_pending_unmatched_reuses_prompt.json rename to tests/fixtures/conformance/step/011_pending_unmatched_reuses_prompt.json diff --git a/tests/fixtures/v2/step/012_clear_premise_populated_update.json b/tests/fixtures/conformance/step/012_clear_premise_populated_update.json similarity index 100% rename from tests/fixtures/v2/step/012_clear_premise_populated_update.json rename to tests/fixtures/conformance/step/012_clear_premise_populated_update.json diff --git a/tests/fixtures/v2/step/013_clear_premise_already_null_update.json b/tests/fixtures/conformance/step/013_clear_premise_already_null_update.json similarity index 100% rename from tests/fixtures/v2/step/013_clear_premise_already_null_update.json rename to tests/fixtures/conformance/step/013_clear_premise_already_null_update.json diff --git a/tests/fixtures/v2/step/014_reset_policies_populated_update.json b/tests/fixtures/conformance/step/014_reset_policies_populated_update.json similarity index 100% rename from tests/fixtures/v2/step/014_reset_policies_populated_update.json rename to tests/fixtures/conformance/step/014_reset_policies_populated_update.json diff --git a/tests/fixtures/v2/step/015_reset_policies_already_empty_update.json b/tests/fixtures/conformance/step/015_reset_policies_already_empty_update.json similarity index 100% rename from tests/fixtures/v2/step/015_reset_policies_already_empty_update.json rename to tests/fixtures/conformance/step/015_reset_policies_already_empty_update.json diff --git a/tests/fixtures/v2/step/016_clear_state_populated_update.json b/tests/fixtures/conformance/step/016_clear_state_populated_update.json similarity index 100% rename from tests/fixtures/v2/step/016_clear_state_populated_update.json rename to tests/fixtures/conformance/step/016_clear_state_populated_update.json diff --git a/tests/fixtures/v2/step/017_clear_state_already_empty_update.json b/tests/fixtures/conformance/step/017_clear_state_already_empty_update.json similarity index 100% rename from tests/fixtures/v2/step/017_clear_state_already_empty_update.json rename to tests/fixtures/conformance/step/017_clear_state_already_empty_update.json diff --git a/tests/fixtures/v2/transcript/001_user_only_replay_state.json b/tests/fixtures/conformance/transcript/001_user_only_replay_state.json similarity index 100% rename from tests/fixtures/v2/transcript/001_user_only_replay_state.json rename to tests/fixtures/conformance/transcript/001_user_only_replay_state.json diff --git a/tests/fixtures/v2/transcript/002_non_string_user_content_ignored.json b/tests/fixtures/conformance/transcript/002_non_string_user_content_ignored.json similarity index 100% rename from tests/fixtures/v2/transcript/002_non_string_user_content_ignored.json rename to tests/fixtures/conformance/transcript/002_non_string_user_content_ignored.json diff --git a/tests/fixtures/v2/transcript/003_stops_at_first_clarify_later_yes.json b/tests/fixtures/conformance/transcript/003_stops_at_first_clarify_later_yes.json similarity index 100% rename from tests/fixtures/v2/transcript/003_stops_at_first_clarify_later_yes.json rename to tests/fixtures/conformance/transcript/003_stops_at_first_clarify_later_yes.json diff --git a/tests/fixtures/v2/transcript/004_stops_at_first_clarify_later_no.json b/tests/fixtures/conformance/transcript/004_stops_at_first_clarify_later_no.json similarity index 100% rename from tests/fixtures/v2/transcript/004_stops_at_first_clarify_later_no.json rename to tests/fixtures/conformance/transcript/004_stops_at_first_clarify_later_no.json diff --git a/tests/harness/fixtures.ts b/tests/harness/fixtures.ts index 6d58368..94e73dc 100644 --- a/tests/harness/fixtures.ts +++ b/tests/harness/fixtures.ts @@ -40,7 +40,7 @@ export interface NamedFixture { payload: T; } -const FIXTURE_ROOT = resolve(process.cwd(), 'tests', 'fixtures', 'v2'); +const FIXTURE_ROOT = resolve(process.cwd(), 'tests', 'fixtures', 'conformance'); async function listJsonFilesRecursive(dir: string): Promise { const entries = await readdir(dir, { withFileTypes: true }); diff --git a/tests/step-fixtures.test.ts b/tests/step-fixtures.test.ts index 0d01128..3024a96 100644 --- a/tests/step-fixtures.test.ts +++ b/tests/step-fixtures.test.ts @@ -4,7 +4,7 @@ import { loadStepFixtures } from './harness/fixtures.js'; const fixtures = await loadStepFixtures(); -describe('step fixtures (v2)', () => { +describe('step fixtures (conformance)', () => { for (const fixture of fixtures) { it(fixture.name, () => { expect(fixture.payload.kind).toBe('step'); diff --git a/tests/transcript-fixtures.test.ts b/tests/transcript-fixtures.test.ts index 7f9b64d..2ab476e 100644 --- a/tests/transcript-fixtures.test.ts +++ b/tests/transcript-fixtures.test.ts @@ -4,7 +4,7 @@ import { loadTranscriptFixtures } from './harness/fixtures.js'; const fixtures = await loadTranscriptFixtures(); -describe('transcript fixtures (v2)', () => { +describe('transcript fixtures (conformance)', () => { for (const fixture of fixtures) { it(fixture.name, () => { expect(fixture.payload.kind).toBe('transcript');