feat(#4668): add StateWriteIntent type surface and opaque-transform guard recognition (ADR-4629 C1) (#4676)
* feat(#4668): add StateWriteIntent type surface and opaque-transform guard recognition (ADR-4629 C1) Child C1 of epic #4629 — migration-order step (1) of ADR-4629: the guard/type scaffolding, with NO behavior change and no caller migrated. 1. StateWriteIntent (src/state-transition.cts) extends StateTransaction with the ADR-4629 section 8.1 concepts: field/section assertions marked required vs best-effort, plus a declared mutation scope (narrow | broad). Frozen like its base. createStateWriteIntent builds one from an existing transaction. Nothing in production constructs it yet — section 8.1's caller-side rule is Required in Phase 2; C2 (the verifying executor) and C3+ (caller migration) consume it. 2. findOpaqueStateTransforms (scripts/lint-state-write-path-drift.cjs) recognizes a residual readModifyWriteStateMd(path, (content) => ...) write whose transform is an inline anonymous arrow/function — the opaque shape section 8.1 replaces with a declared StateWriteIntent. readModifyWriteStateMd goes THROUGH the seam (it is not a raw-write bypass, Axis 2's concern), but its opaque body transform is neither verified (section 8.2) nor bounded (section 8.3). This ships recognition as a CAPABILITY: exported and unit-tested (positive control on a seeded fixture) but DELIBERATELY NOT wired into collect()'s failing scan. Wiring it now would turn the ~16 residual callers red at once, and ADR-3473 section 8.6 retired the ratchet that would otherwise absorb them. C2 wires it terminal as the verifying executor lands and callers migrate under ADR-3408 section 6 phasing. No behavior change: the guard is green on the tree (detection not wired), every state verb's output is unchanged, and the relevant suites (1763 tests) plus lint:ci pass. Regression tests are failing-first: positive/negative controls for the guard capability and a shape test for the type, plus a pin that collect() has no opaque-transform findings (C1 must not enforce; that is C2). Closes #4668 * chore(#4668): backfill changeset pr field to the real PR number (#4676) pr: 0 is rejected by parseFragment as invalid_pr (it is not a valid placeholder); the fragment must carry the real PR number, which fixes both changeset-lint and docs-lint (fail_invalid_fragment / fail_malformed_fragment). --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -4826,3 +4826,46 @@ describe('stateReplaceProgressPercent — anchored bold form leaves prose lookal
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// C1 of epic #4629 (ADR-4629 §8.1) — the StateWriteIntent type surface.
|
||||
// StateWriteIntent EXTENDS StateTransaction, adding the §8.1 concepts: field/
|
||||
// section assertions marked required-vs-best-effort, plus a declared mutation
|
||||
// scope (narrow | broad). C1 ships the type + constructor (no caller migrated —
|
||||
// §8.1's caller-side rule is Required — Phase 2); C2+ consume it. Frozen like its
|
||||
// base so a transaction cannot be mutated after construction.
|
||||
describe('C1 (ADR-4629 §8.1): StateWriteIntent type surface', () => {
|
||||
const { createStateWriteIntent } = require('../gsd-core/bin/lib/state-transition.cjs');
|
||||
|
||||
test('createStateWriteIntent extends a StateTransaction with assertions + declared scope', () => {
|
||||
const base = openStateTransaction({ snapshot: {} });
|
||||
const intent = createStateWriteIntent(base, {
|
||||
assertions: [
|
||||
{ field: 'Progress', requirement: 'required' },
|
||||
{ field: 'Notes', requirement: 'best-effort' },
|
||||
],
|
||||
scope: 'narrow',
|
||||
});
|
||||
// still a StateTransaction (IS-A: every base field carried through)
|
||||
assert.strictEqual(intent.kind, 'open');
|
||||
assert.strictEqual(typeof intent.snapshot, 'object');
|
||||
assert.strictEqual(intent.resync, false);
|
||||
// plus the §8.1 additions
|
||||
assert.deepStrictEqual(
|
||||
intent.assertions.map((a) => [a.field, a.requirement]),
|
||||
[['Progress', 'required'], ['Notes', 'best-effort']],
|
||||
);
|
||||
assert.strictEqual(intent.scope, 'narrow');
|
||||
assert.ok(Object.isFrozen(intent), 'StateWriteIntent must be frozen like StateTransaction');
|
||||
});
|
||||
|
||||
test('createStateWriteIntent defaults scope to narrow and assertions to [] when omitted', () => {
|
||||
const intent = createStateWriteIntent(openStateTransaction({ snapshot: {} }), {});
|
||||
assert.deepStrictEqual(intent.assertions, []);
|
||||
assert.strictEqual(intent.scope, 'narrow');
|
||||
});
|
||||
|
||||
test('createStateWriteIntent rejects an absent base transaction (construction failure, per §8.6 posture)', () => {
|
||||
assert.throws(() => createStateWriteIntent(null, { scope: 'narrow' }), /transaction/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -634,3 +634,55 @@ describe('F3 — a guard that cannot fail is not a guard: the real CLI entry poi
|
||||
assert.ok(findings.every((f) => f.reason === REASON.COMPOSITION_BYPASS));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// C1 of epic #4629 (ADR-4629 §8.1, migration step 1) — the OPAQUE-TRANSFORM
|
||||
// recognition CAPABILITY. `findOpaqueStateTransforms` recognizes a residual
|
||||
// `readModifyWriteStateMd(path, (content) => …)` write whose transform is an
|
||||
// inline anonymous arrow / function expression (the shape §8.1 replaces with a
|
||||
// declared StateWriteIntent). C1 ships the capability proven on a seeded fixture;
|
||||
// it is deliberately NOT wired into collect()'s failing scan (that is C2 —
|
||||
// §8.2/§8.3 Required — Phase 2), so the ~16 existing residual callers do not turn
|
||||
// lint:ci red and no ratchet is introduced (ADR-3408 §6 phased migration).
|
||||
describe('C1 (ADR-4629 §8.1): findOpaqueStateTransforms — opaque readModifyWriteStateMd recognition', () => {
|
||||
test('positive control: an inline arrow transform (the #4551 residue shape) is flagged', () => {
|
||||
const text = [
|
||||
'stateUpdated = readModifyWriteStateMd(',
|
||||
' statePath,',
|
||||
' (stateContent) => {',
|
||||
" return stateContent.replace(/None/g, '');",
|
||||
' },',
|
||||
' cwd,',
|
||||
');',
|
||||
].join('\n');
|
||||
const out = guard.findOpaqueStateTransforms('src/some-residual-writer.cts', text);
|
||||
assert.strictEqual(out.length, 1);
|
||||
assert.strictEqual(out[0].reason, REASON.OPAQUE_STATE_TRANSFORM);
|
||||
assert.strictEqual(out[0].axis, 'opaque-transform');
|
||||
});
|
||||
|
||||
test('positive control: a function-expression transform is also flagged (Postel: over-report-safe)', () => {
|
||||
const text = 'readModifyWriteStateMd(p, function (c) { return c; }, cwd);';
|
||||
assert.strictEqual(guard.findOpaqueStateTransforms('src/x.cts', text).length, 1);
|
||||
});
|
||||
|
||||
test('negative control: a declared-intent (identifier) 2nd arg is NOT flagged', () => {
|
||||
const text = 'readModifyWriteStateMd(p, writeIntent, cwd);';
|
||||
assert.strictEqual(guard.findOpaqueStateTransforms('src/x.cts', text).length, 0);
|
||||
});
|
||||
|
||||
test('negative control: a non-readModifyWriteStateMd call with an arrow is NOT flagged', () => {
|
||||
const text = 'someOther(p, (c) => c, cwd);';
|
||||
assert.strictEqual(guard.findOpaqueStateTransforms('src/x.cts', text).length, 0);
|
||||
});
|
||||
|
||||
test('no behavior change: the capability is NOT wired into the failing scan (collect has no opaque-transform findings)', () => {
|
||||
// C1 ships recognition; C2 wires enforcement. Wiring it now would red the ~16 residual callers.
|
||||
const { findings } = guard.collect();
|
||||
assert.strictEqual(
|
||||
findings.some((f) => f.reason === REASON.OPAQUE_STATE_TRANSFORM),
|
||||
false,
|
||||
'C1 must not wire opaque-transform into collect() (that is C2, §8.2/§8.3 Required — Phase 2)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user