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:
@@ -519,6 +519,82 @@ export function rebuildStateTransaction(init: StateTransactionInit): StateTransa
|
||||
return createStateTransaction('rebuild', init, 'rebuildStateTransaction');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// StateWriteIntent — ADR-4629 §8.1 (epic #4629, child C1, migration step 1)
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
// ADR-1769 Decision 2 scoped the state-transition model to 10 transitions and
|
||||
// REJECTED covering all 16 writers; the residual writers still ride an opaque
|
||||
// `transformFn: (content: string) => string` (`readModifyWriteStateMd`,
|
||||
// src/state.cts). The write seam preserves FRONTMATTER, but the opaque body
|
||||
// transform is neither verified (did every intended assertion land? §8.2) nor
|
||||
// bounded (did anything OUTSIDE the declared scope change? §8.3) — the residue
|
||||
// behind the write-path bugs epic #4629 absorbs.
|
||||
//
|
||||
// StateWriteIntent is the declared replacement: which field/section assertions
|
||||
// the write must land (required vs best-effort) and the mutation scope it may
|
||||
// touch (narrow | broad). It EXTENDS StateTransaction so an intent IS-A
|
||||
// transaction everywhere the write seam already expects one. C1 ships the TYPE +
|
||||
// constructor only — §8.1's caller-side rule ("no residual caller supplies an
|
||||
// anonymous transform") is statused *Required — Phase 2*, so nothing constructs
|
||||
// this in production yet; C2 (the verifying executor) and C3+ (caller migration)
|
||||
// consume it.
|
||||
|
||||
export type StateAssertionRequirement = 'required' | 'best-effort';
|
||||
export type StateMutationScope = 'narrow' | 'broad';
|
||||
|
||||
/** One declared post-state assertion: a frontmatter field or a body section. */
|
||||
export type StateFieldAssertion = {
|
||||
readonly field: string;
|
||||
readonly requirement: StateAssertionRequirement;
|
||||
};
|
||||
|
||||
export type StateWriteIntentInit = {
|
||||
readonly assertions?: ReadonlyArray<StateFieldAssertion>;
|
||||
readonly scope?: StateMutationScope;
|
||||
};
|
||||
|
||||
/**
|
||||
* ADR-4629 §8.1: a StateTransaction PLUS the declared write intent — the
|
||||
* assertions verified against the re-read file (§8.2) and the mutation scope the
|
||||
* write may not exceed (§8.3). Both are Phase-2 consumers; the type exists now so
|
||||
* Phase 2 has a surface to build on.
|
||||
*/
|
||||
export type StateWriteIntent = StateTransaction & {
|
||||
readonly assertions: ReadonlyArray<StateFieldAssertion>;
|
||||
readonly scope: StateMutationScope;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend an existing StateTransaction into a StateWriteIntent. The base
|
||||
* transaction is REQUIRED — an absent base is a construction failure, mirroring
|
||||
* `createStateTransaction`'s ADR-3473 §8.6 posture (do not tolerate null). `scope`
|
||||
* defaults to the conservative `'narrow'`; `assertions` defaults to none. Frozen
|
||||
* so an intent, like a transaction, cannot be mutated after construction.
|
||||
*/
|
||||
export function createStateWriteIntent(
|
||||
transaction: StateTransaction,
|
||||
init: StateWriteIntentInit = {},
|
||||
): StateWriteIntent {
|
||||
if (transaction === null || typeof transaction !== 'object' || Array.isArray(transaction)) {
|
||||
const err = new Error(
|
||||
'createStateWriteIntent: a base StateTransaction is required (build it with ' +
|
||||
'openStateTransaction / rebuildStateTransaction first). Per ADR-4629 §8.1, an absent ' +
|
||||
'transaction is a construction failure — do not tolerate null.',
|
||||
) as Error & { code: string };
|
||||
err.code = 'STATE_WRITE_INTENT_TRANSACTION_REQUIRED';
|
||||
throw err;
|
||||
}
|
||||
const assertions: ReadonlyArray<StateFieldAssertion> = Object.freeze(
|
||||
(init.assertions ?? []).map((a) => Object.freeze({ field: a.field, requirement: a.requirement })),
|
||||
);
|
||||
return Object.freeze({
|
||||
...transaction,
|
||||
assertions,
|
||||
scope: init.scope ?? 'narrow',
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// applyStatePreservation — table-driven post-sync preservation (ADR-1769 #1796)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user