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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
40 changes: 38 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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=<python repo>/tests/fixtures/conformance npm run fixtures:sync

3. Commit only the resulting changes.

### Verification

After syncing:

npm test
FIXTURES_SOURCE=<same python fixtures path> 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:
Expand Down
15 changes: 7 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 11 additions & 3 deletions scripts/fixtures-check.sh
Original file line number Diff line number Diff line change
@@ -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

Expand Down
14 changes: 11 additions & 3 deletions scripts/fixtures-sync.sh
Original file line number Diff line number Diff line change
@@ -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

Expand Down
13 changes: 1 addition & 12 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion tests/engine_state_immutability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion tests/harness/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface NamedFixture<T> {
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<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
Expand Down
2 changes: 1 addition & 1 deletion tests/step-fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion tests/transcript-fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down