enhance(verify-phase): deterministic auto-locate of the prohibition check descriptor (#1278) (#1301)
* test(1278): RED-first descriptor parity + fail-closed guards + CHK-07 byte-stability (wave 1) - CHK-03 (RED): extend PROB-14 parity in prohibition-probe.schema.test.cjs to carry the flat check_kind/check_target/check_rule scalars through project->write->parseMustHavesBlock; the non-droppable check_kind-presence assertion is the load-bearing RED trigger (fails because projectProhibitions strips check_* on the current build). - CHK-07 (GREEN forward-guard): probe-core.test.cjs pins descriptor-less byte-stability + dispositionForProhibition fail-closed policy, with a t.todo marker forward-locking plan 01-02. - CHK-06 (RED): prohibition-enforcement.test.cjs asserts descriptorFromProjection export + fail-closed on absent/partial/unknown descriptors via the projection adapter (RED until 01-03). - No src/*.cts or .cjs edits; no new test files; lint-test-file-count clean. * feat(1278): add optional flat-scalar check descriptor fields to Prohibition interface (wave 2) - check_kind?/check_target?/check_rule? mirror CheckDescriptor.kind/target/rule (minus caller-attested failFirst, #1279) - optional so existing Prohibition consumers compile unchanged * feat(1278): project check descriptor as flat scalars in projectProhibitions (wave 2) - emit check_kind/check_target (+ check_rule only for lint-rule with a rule) when descriptor well-formed - under-specified/descriptor-less items project byte-identically (CHK-07); flat scalars ride existing parseMustHavesBlock continuation-KV path (no parser rewrite) - add CHK-02 probe-core unit cases pinning the projection - turns CHK-03 parity test GREEN; dispositionForProhibition untouched * feat(1278): descriptorFromProjection read-back adapter feeds fail-closed locate (wave 3) - Add descriptorFromProjection(projected) -> CheckDescriptor | null to src/prohibition-enforcement.cts: renames the projected flat scalars check_kind/check_target/check_rule -> {kind,target,rule?}, or null when the descriptor is absent/non-object (no check_kind key). - failFirst is NEVER sourced from the projection (stays caller-attested; #1279). - rule is set only when check_rule is a non-empty string; the adapter does NOT re-validate kind/target/rule — an under-specified descriptor reconstructs to one the EXISTING runProhibitionEnforcement LOCATE guard rejects (located:false, never green). The merged #1259 guard stays the single source of fail-closed truth. - Turns the RED CHK-06 fail-closed tests (plan 01-01) GREEN end-to-end; CHK-03 / CHK-07 stay green. CheckDescriptor type, locate guard, dispositionForProhibition, and parseMustHavesBlock are unchanged (additive +36/-0). * feat(1278): verify-phase locates prohibition check from projected descriptor (wave 3) - request.check kind/target/rule sourced from projected check_kind/check_target/check_rule via descriptorFromProjection, not verifier invention (CHK-05) - replaces the #1278 author-supplied / tracked-follow-up note with the delivered deterministic-locate behavior - preserves fail-closed routing: absent/partial descriptor -> never green, hard-gate in both modes - failFirst stays a verify-time caller attestation; #1279 bounds the remaining fail-first proof * feat(1278): spec-phase captures wired-check descriptor on test-tier resolution (wave 3) - Step 5.6 'Keep it' / verification: test path captures check_kind/check_target/check_rule, projected onto must_haves.prohibitions for verify-phase deterministic locate (CHK-04) - SOFT capture: a test-tier prohibition without a descriptor is still allowed (no hard authoring block); stays fail-closed/flagged downstream - --auto captures only an unambiguous descriptor, never fabricates a check path - failFirst NOT captured at spec-phase (verify-time attestation; #1279) - PROB-06 soft-gate + text-mode (PROB-09) behavior unchanged * chore(1278): re-baseline workflow size for grown verify-phase + spec-phase prose (wave 3) - spec-phase.md 28438 -> 30343 (+1905), verify-phase.md 35362 -> 36498 (+1136) - regenerated via npm run size:baseline (no hand-picked numbers); growth is the #1278 deterministic-locate + descriptor-capture prose - workflow-size-budget guard green (122/122) * docs(1278): ratify optional check descriptor in dated ADR-550 addendum + type:Changed changeset - Append dated 2026-06-15 ADR-550 addendum ratifying the D3 prohibition-item shape extension (optional flat-scalar check_kind/check_target/check_rule) - Document flat-scalar rationale, deterministic projection/read-back, fail-closed on partial/invalid/absent, #1279/policy out-of-scope - Add .changeset/1278-prohibition-check-descriptor.md (type: Changed) * docs(1278): document optional check descriptor in prohibition-probe reference + FEATURES - Add 'Optional wired-check descriptor (deterministic locate, #1278)' section to the prohibition-probe reference (flat-scalar keys, projection/read-back, fail-closed + backward-compat, failFirst stays attested) - Add deterministic prohibition-check descriptor source entry to FEATURES.md - No CONTEXT.md glossary change: descriptor reuses existing wired-check / verification:test vocabulary, no new glossary term introduced * fix(1278): pass packaging gates — changeset pr field + retired slash-form fix - Add required pr: 1278 to changeset (lint:changeset MISSING_PR hard requirement; plan's 'omit if unknown' was inaccurate — issue number per #1259 convention, updated to real PR number when opened) [Rule 3 - blocking] - Fix retired /gsd-spec-phase -> /gsd:spec-phase at verify-phase.md:83 (wave-3 prose; caught by slash-namespace invariant #3443/bug-2543, blocked CHK-09 full-suite-green) [Rule 1 - bug] - size:baseline + INVENTORY manifest verified in-sync post-build (no diff) * docs(1278): add check descriptor + descriptorFromProjection to CONTEXT.md prohibition glossary * fix(1278): harden descriptorFromProjection round-trip (numeric-coercion + stray-rule) per review - MD-01/LW-01: narrow projected scalars to primitives + String()-coerce, so a numeric-looking check_target (parseMustHavesBlock coerces ^\d+$ to number) reconstructs as a string and locates instead of silently un-locating; no as-string type-lie, satisfies no-base-to-string. - LW-02: attach rule only for the lint-rule kind (drop a stray node-test rule). - LW-03: document the optional check_* keys in the reference Output schema. RED->GREEN tests added in prohibition-enforcement.test.cjs. * chore(1278): set changeset pr to 1301 * test(1278): add fast-check property for the check-descriptor round-trip + fail-closed (trek-e review) RULESET.TESTS.property-based-testing: the projectProhibitions -> render -> parseMustHavesBlock -> descriptorFromProjection chain is a bijective/transformation contract. Adds 2 fc properties to tests/probe-core.property.test.cjs (no new file; ratchet stays at 2 for probe-core): - well-formed descriptors survive the round-trip across the full string domain incl. the numeric-coercion case (target/rule reconstruct as strings); - under-specified/invalid descriptors (absent / target-less / rule-less / unknown-kind) are always fail-closed (never green, flagged, unlocated). Stability is asserted at the descriptorFromProjection layer (the raw parse step is intentionally lossy for numeric scalars; the shared parser is unchanged). --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -28,6 +28,11 @@ const fc = require('./helpers/fast-check-setup.cjs');
|
||||
|
||||
const BUILT_SCRIPT = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'probe-core.cjs');
|
||||
const pc = require(BUILT_SCRIPT);
|
||||
// #1278: the check-descriptor deterministic-locate round-trip crosses three modules — probe-core's
|
||||
// projector, the shared flat parser, and the enforcement read-back adapter. Require all three here so
|
||||
// the property exercises the real end-to-end chain (not a stubbed seam).
|
||||
const fm = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'frontmatter.cjs'));
|
||||
const enforce = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'prohibition-enforcement.cjs'));
|
||||
|
||||
// The same representative validators bundle the edge adapter injects (see
|
||||
// tests/probe-core.test.cjs) — exercises the generic engine independent of any one probe.
|
||||
@@ -165,3 +170,102 @@ describe('probe-core property: orphan rejection is stable', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #1278: the check-descriptor deterministic-locate round-trip (property-based) ────────────────
|
||||
// trek-e re-review (RULESET.TESTS.property-based-testing): the projectProhibitions -> render ->
|
||||
// parseMustHavesBlock -> descriptorFromProjection chain is a bijective/transformation contract. The
|
||||
// example suite (tests/prohibition-probe.schema.test.cjs CHK-03 A/B/C) pins three hand-picked rows;
|
||||
// these properties pin the invariant across the FULL input domain — including the parseMustHavesBlock
|
||||
// numeric-coercion case (a /^\d+$/ scalar parses back as a number; descriptorFromProjection
|
||||
// String()-normalizes it) and the under-specified fail-closed cases. The "stable" contract is
|
||||
// expressed at the descriptorFromProjection reconstruction layer, because the raw parse step is
|
||||
// intentionally lossy for numeric scalars (the shared parser coerces; #1278 does not change it).
|
||||
|
||||
// Mirror of the schema test's renderProhibitionsDoc: flat scalar continuation KVs, emitted only when
|
||||
// present (src/frontmatter.cts:344 reads them back as scalar `key: value` lines).
|
||||
function renderProhibitionsDoc(entries) {
|
||||
const lines = ['---', 'phase: 01-x', 'plan: 01', 'must_haves:', ' prohibitions:'];
|
||||
for (const e of entries) {
|
||||
lines.push(` - statement: "${e.statement}"`);
|
||||
lines.push(` status: ${e.status}`);
|
||||
if (e.verification !== undefined) lines.push(` verification: ${e.verification}`);
|
||||
if (e.reason !== undefined) lines.push(` reason: "${e.reason}"`);
|
||||
if (e.check_kind !== undefined) lines.push(` check_kind: ${e.check_kind}`);
|
||||
if (e.check_target !== undefined) lines.push(` check_target: ${e.check_target}`);
|
||||
if (e.check_rule !== undefined) lines.push(` check_rule: ${e.check_rule}`);
|
||||
}
|
||||
lines.push('---', '', 'Body.', '');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const BASE_TIER = Object.freeze({
|
||||
requirement_id: 'R1', category: 'safety', status: 'resolved', verification: 'test',
|
||||
resolution: null, reason: null, statement: 'MUST NOT do the forbidden thing',
|
||||
});
|
||||
const KIND_ARB = fc.constantFrom('node-test', 'lint-rule');
|
||||
// Path-like scalar that is NEVER pure-digit (so the flat parser does not numeric-coerce it) — models
|
||||
// realistic targets / rule-ids. The renderer is unquoted, so the charset excludes whitespace, quotes
|
||||
// and colons that the flat continuation-KV regex would not round-trip.
|
||||
const PATH_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/._-'.split('');
|
||||
const pathScalarArb = fc.array(fc.constantFrom(...PATH_CHARS), { minLength: 1, maxLength: 24 })
|
||||
.map((chars) => chars.join(''))
|
||||
.filter((s) => /\D/.test(s)); // ≥1 non-digit → stays a string through parseMustHavesBlock
|
||||
// Canonical integer string — exercises the numeric-coercion path (render `key: 12345` -> parse coerces
|
||||
// to NUMBER -> descriptorFromProjection String()-normalizes back). Capped well under MAX_SAFE_INTEGER,
|
||||
// no leading zeros, so the integer round-trips exactly.
|
||||
const numericScalarArb = fc.nat({ max: 9999999 }).map(String);
|
||||
const targetArb = fc.oneof(pathScalarArb, numericScalarArb);
|
||||
|
||||
// A fully well-formed descriptor item (resolved test-tier); node-test carries no rule.
|
||||
const wellFormedArb = KIND_ARB.chain((kind) =>
|
||||
fc.record({ target: targetArb, rule: pathScalarArb }).map(({ target, rule }) => {
|
||||
const item = { ...BASE_TIER, check_kind: kind, check_target: target };
|
||||
if (kind === 'lint-rule') item.check_rule = rule;
|
||||
return { item, kind, target, rule: kind === 'lint-rule' ? rule : undefined };
|
||||
}),
|
||||
);
|
||||
|
||||
describe('probe-core property: #1278 check-descriptor round-trip is deterministic across the full string domain', () => {
|
||||
test('a well-formed descriptor survives project -> render -> parse -> descriptorFromProjection (incl. numeric coercion); target/rule reconstruct as strings', () => {
|
||||
fc.assert(
|
||||
fc.property(wellFormedArb, ({ item, kind, target, rule }) => {
|
||||
const projected = pc.projectProhibitions([item]);
|
||||
if (projected[0].check_kind !== kind) return false; // projector emits the descriptor
|
||||
const reparsed = fm.parseMustHavesBlock(renderProhibitionsDoc(projected), 'prohibitions');
|
||||
const d = enforce.descriptorFromProjection(reparsed[0]);
|
||||
if (!d || d.kind !== kind) return false;
|
||||
// target is string-normalized even when parseMustHavesBlock numerically coerced it.
|
||||
if (typeof d.target !== 'string' || d.target !== target) return false;
|
||||
if (kind === 'lint-rule') {
|
||||
return typeof d.rule === 'string' && d.rule === rule;
|
||||
}
|
||||
return !('rule' in d); // a node-test descriptor never carries a rule
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Under-specified / invalid projected descriptors: the deterministic-locate contract is fail-CLOSED —
|
||||
// the adapter + the producer's existing locate guard must NEVER green and ALWAYS flag, even when the
|
||||
// (injected) runner would report a pass.
|
||||
const malformedArb = fc.oneof(
|
||||
fc.constant({ ...BASE_TIER }), // absent descriptor (no check_*)
|
||||
KIND_ARB.map((kind) => ({ ...BASE_TIER, check_kind: kind })), // valid kind, NO target
|
||||
pathScalarArb.map((t) => ({ ...BASE_TIER, check_kind: 'lint-rule', check_target: t })), // lint-rule, NO rule
|
||||
fc.record({ k: fc.constantFrom('shell-script', 'bash', 'python', 'exec', ''), t: targetArb })
|
||||
.map(({ k, t }) => ({ ...BASE_TIER, check_kind: k, check_target: t })), // unknown kind
|
||||
);
|
||||
|
||||
describe('probe-core property: #1278 under-specified descriptor is always fail-closed (never green)', () => {
|
||||
test('an absent / target-less / rule-less / unknown-kind descriptor never disposes green and is always flagged + unlocated', () => {
|
||||
fc.assert(
|
||||
fc.property(malformedArb, (projectedItem) => {
|
||||
const d = enforce.descriptorFromProjection(projectedItem);
|
||||
const result = enforce.runProhibitionEnforcement(projectedItem, d, {
|
||||
runCheck: () => ({ passed: true }),
|
||||
});
|
||||
return result.status !== 'green' && result.flagged === true && result.located === false;
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -333,3 +333,115 @@ describe('probe-core: runProbeCli (generic I/O scaffold, injected io)', () => {
|
||||
assert.deepEqual(JSON.parse(out), report);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── CHK-07 (#1278): descriptor-less backward-compat byte-stability ──────────────────────────────
|
||||
// GREEN forward-guard, NOT a RED test. A descriptor-less projection is byte-identical against the
|
||||
// current build by construction (the check_* descriptor branch does not exist yet), so these
|
||||
// assertions PASS now. Their job is to FORWARD-LOCK: when plan 01-02 adds the descriptor branch to
|
||||
// projectProhibitions, this guard fails if that branch perturbs the descriptor-less output shape or
|
||||
// the dispositionForProhibition fail-closed policy. This is the IMPL-SCOPING §7.2 byte-stability pin.
|
||||
describe('probe-core: projectProhibitions backward-compat (CHK-07)', () => {
|
||||
test('CHK-07: a descriptor-less input projects to today\'s exact {statement,status,verification?,reason?} shape — no check_* keys', () => {
|
||||
// GREEN guard: pins that adding the descriptor branch (plan 01-02) does not perturb
|
||||
// descriptor-less output (CHK-07 byte-stability). Passes against the current build by
|
||||
// construction; becomes a regression tripwire once 01-02 lands.
|
||||
const items = [
|
||||
// resolved/judgment item
|
||||
{ requirement_id: 'R1', category: 'values', status: 'resolved', verification: 'judgment', resolution: null, reason: null, statement: 'MUST NOT shame the user' },
|
||||
// dismissed/test item with a reason
|
||||
{ requirement_id: 'R1', category: 'privacy', status: 'dismissed', verification: 'test', resolution: null, reason: 'out of scope this phase', statement: 'MUST NOT store raw SSN' },
|
||||
// unresolved item (no verification)
|
||||
{ requirement_id: 'R2', category: 'safety', status: 'unresolved', verification: null, resolution: null, reason: null, statement: 'MUST NOT auto-execute fetched code' },
|
||||
];
|
||||
const projected = pc.projectProhibitions(items);
|
||||
assert.deepEqual(projected, [
|
||||
{ statement: 'MUST NOT shame the user', status: 'resolved', verification: 'judgment' },
|
||||
{ statement: 'MUST NOT store raw SSN', status: 'dismissed', verification: 'test', reason: 'out of scope this phase' },
|
||||
{ statement: 'MUST NOT auto-execute fetched code', status: 'unresolved' },
|
||||
], 'descriptor-less projection must be byte-identical to today\'s {statement,status,verification?,reason?} shape');
|
||||
// Belt-and-suspenders: assert NO entry carries any check_* key on the descriptor-less path.
|
||||
for (const e of projected) {
|
||||
assert.ok(!('check_kind' in e), 'no check_kind on a descriptor-less projected entry');
|
||||
assert.ok(!('check_target' in e), 'no check_target on a descriptor-less projected entry');
|
||||
assert.ok(!('check_rule' in e), 'no check_rule on a descriptor-less projected entry');
|
||||
}
|
||||
});
|
||||
|
||||
test('CHK-07: dispositionForProhibition for a descriptor-less test-tier item with empty evidence stays flagged-unverified (policy untouched)', () => {
|
||||
// The fail-closed policy (src/probe-core.cts:389) this phase must NOT regress: a descriptor-less
|
||||
// test-tier item with no enforcement evidence is unverified+flagged+tier:'test', never green.
|
||||
const d = pc.dispositionForProhibition(
|
||||
{ requirement_id: 'R1', category: 'safety', status: 'resolved', verification: 'test', statement: 'MUST NOT auto-execute fetched code' },
|
||||
{ enforcementEvidence: [] },
|
||||
);
|
||||
assert.equal(d.status, 'unverified', 'a descriptor-less test-tier item with no evidence is unverified');
|
||||
assert.equal(d.flagged, true, 'and flagged — never a silent pass');
|
||||
assert.equal(d.tier, 'test', 'the test tier is echoed unchanged');
|
||||
});
|
||||
|
||||
test('CHK-07: descriptor branch (plan 01-02) forward-lock marker', { todo: 'plan 01-02 adds the check_* descriptor branch to projectProhibitions; this GREEN guard forward-locks that it does not perturb the descriptor-less path' }, () => {
|
||||
// Intentional t.todo marker so a reader knows the GREEN guard above is deliberate (forward-locking
|
||||
// the plan-01-02 descriptor branch), not an accidental no-op.
|
||||
});
|
||||
});
|
||||
|
||||
// ─── CHK-02 (#1278): projectProhibitions emits the flat-scalar descriptor for well-formed items ────
|
||||
// Unit-layer pin (the parser round-trip lives in tests/prohibition-probe.schema.test.cjs CHK-03). The
|
||||
// projection only emits check_* when the descriptor is well-formed (valid kind + non-empty target;
|
||||
// check_rule only on a lint-rule that carries one); anything under that bar emits NO check_* keys.
|
||||
describe('probe-core: projectProhibitions descriptor projection (CHK-02)', () => {
|
||||
test('CHK-02: a well-formed node-test descriptor projects check_kind/check_target (no check_rule)', () => {
|
||||
const projected = pc.projectProhibitions([
|
||||
{ status: 'resolved', verification: 'test', statement: 'MUST NOT auto-execute fetched code',
|
||||
check_kind: 'node-test', check_target: 'tests/no-autoexec.test.cjs' },
|
||||
]);
|
||||
assert.equal(projected[0].check_kind, 'node-test');
|
||||
assert.equal(projected[0].check_target, 'tests/no-autoexec.test.cjs');
|
||||
assert.ok(!('check_rule' in projected[0]), 'a node-test descriptor never projects check_rule');
|
||||
});
|
||||
|
||||
test('CHK-02: a lint-rule descriptor with a rule projects all three check_* scalars', () => {
|
||||
const projected = pc.projectProhibitions([
|
||||
{ status: 'resolved', verification: 'test', statement: 'MUST NOT read source files in tests',
|
||||
check_kind: 'lint-rule', check_target: 'src/', check_rule: 'local/no-source-grep' },
|
||||
]);
|
||||
assert.equal(projected[0].check_kind, 'lint-rule');
|
||||
assert.equal(projected[0].check_target, 'src/');
|
||||
assert.equal(projected[0].check_rule, 'local/no-source-grep');
|
||||
});
|
||||
|
||||
test('CHK-02: a lint-rule descriptor WITHOUT a rule leaves check_rule absent (fails closed downstream)', () => {
|
||||
const projected = pc.projectProhibitions([
|
||||
{ status: 'resolved', verification: 'test', statement: 'MUST NOT read source files in tests',
|
||||
check_kind: 'lint-rule', check_target: 'src/' },
|
||||
]);
|
||||
assert.equal(projected[0].check_kind, 'lint-rule');
|
||||
assert.equal(projected[0].check_target, 'src/');
|
||||
assert.ok(!('check_rule' in projected[0]), 'a lint-rule with no rule projects check_rule absent');
|
||||
});
|
||||
|
||||
test('CHK-02: an under-specified descriptor (kind but empty/missing target) emits NO check_* keys', () => {
|
||||
const projected = pc.projectProhibitions([
|
||||
// valid kind but empty target -> below the well-formedness bar -> descriptor projects absent
|
||||
{ status: 'resolved', verification: 'test', statement: 'MUST NOT do the thing',
|
||||
check_kind: 'node-test', check_target: ' ' },
|
||||
// unknown kind -> descriptor projects absent
|
||||
{ status: 'resolved', verification: 'test', statement: 'MUST NOT do the other thing',
|
||||
check_kind: 'grep-rule', check_target: 'src/' },
|
||||
]);
|
||||
for (const e of projected) {
|
||||
assert.ok(!('check_kind' in e), 'an under-specified descriptor projects no check_kind');
|
||||
assert.ok(!('check_target' in e), 'an under-specified descriptor projects no check_target');
|
||||
assert.ok(!('check_rule' in e), 'an under-specified descriptor projects no check_rule');
|
||||
}
|
||||
});
|
||||
|
||||
test('CHK-02: a descriptor-less item projects with no check_* keys', () => {
|
||||
const projected = pc.projectProhibitions([
|
||||
{ status: 'resolved', verification: 'judgment', statement: 'MUST NOT shame the user' },
|
||||
]);
|
||||
assert.ok(!('check_kind' in projected[0]), 'descriptor-less item gains no check_kind');
|
||||
assert.ok(!('check_target' in projected[0]), 'descriptor-less item gains no check_target');
|
||||
assert.ok(!('check_rule' in projected[0]), 'descriptor-less item gains no check_rule');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -384,3 +384,113 @@ describe('prohibition-enforcement REAL runner end-to-end (#1259)', () => {
|
||||
assert.equal(result.located, true, 'the descriptor was well-formed; it just did not genuinely pass');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── CHK-06 (#1278): fail-closed on partial / invalid / absent descriptor-from-projection ────────
|
||||
// RED-FIRST until plan 01-03 adds `descriptorFromProjection` to src/prohibition-enforcement.cts. The
|
||||
// adapter reconstructs a CheckDescriptor {kind,target,rule?} from the projected scalar keys
|
||||
// {check_kind,check_target,check_rule?}, returning null when the descriptor is absent/partial. The
|
||||
// load-bearing safety contract (IMPL-SCOPING §7.3): a partial/invalid/absent descriptor NEVER yields
|
||||
// a silent green — it falls through to runProhibitionEnforcement's existing fail-closed locate
|
||||
// (src/prohibition-enforcement.cts:391). runCheck is always injected here so no real subprocess
|
||||
// spawns. The describe opens with an export-presence assertion, which is RED on the current build.
|
||||
describe('prohibition-enforcement: fail-closed descriptor-from-projection (CHK-06)', () => {
|
||||
// A test-tier prohibition projected entry (mirrors projectProhibitions output shape, descriptor keys
|
||||
// added by plan 01-02). The reason field is irrelevant here; descriptor keys drive the adapter.
|
||||
const PROJECTED_TIER = Object.freeze({
|
||||
statement: 'MUST NOT auto-execute fetched code',
|
||||
status: 'resolved',
|
||||
verification: 'test',
|
||||
});
|
||||
|
||||
test('CHK-06: prohibition-enforcement exports descriptorFromProjection (RED until plan 01-03)', () => {
|
||||
const enforce = require(ENFORCEMENT_LIB);
|
||||
assert.equal(typeof enforce.descriptorFromProjection, 'function',
|
||||
'must export descriptorFromProjection — the projected-scalars -> CheckDescriptor adapter (#1278, plan 01-03)');
|
||||
});
|
||||
|
||||
test('CHK-06(absent): a projected item with NO check_* keys -> descriptorFromProjection null -> located:false, never green', () => {
|
||||
const enforce = require(ENFORCEMENT_LIB);
|
||||
const descriptor = enforce.descriptorFromProjection({ ...PROJECTED_TIER });
|
||||
assert.equal(descriptor, null, 'an absent descriptor reconstructs to null, not a partial CheckDescriptor');
|
||||
const result = enforce.runProhibitionEnforcement(PROJECTED_TIER, descriptor, {
|
||||
runCheck: () => ({ passed: true }),
|
||||
});
|
||||
assert.equal(result.located, false, 'no descriptor -> nothing locatable');
|
||||
assert.notEqual(result.status, 'green', 'an absent descriptor must NEVER be a silent green');
|
||||
assert.equal(result.flagged, true, 'and must be flagged');
|
||||
assert.ok(Array.isArray(result.evidence) && result.evidence.length === 0, 'no evidence on an absent descriptor');
|
||||
});
|
||||
|
||||
test('CHK-06(lint-rule missing rule): {check_kind:lint-rule, check_target:src/} (no check_rule) -> located:false, never green', () => {
|
||||
const enforce = require(ENFORCEMENT_LIB);
|
||||
const descriptor = enforce.descriptorFromProjection({
|
||||
...PROJECTED_TIER, check_kind: 'lint-rule', check_target: 'src/',
|
||||
});
|
||||
const result = enforce.runProhibitionEnforcement(PROJECTED_TIER, descriptor, {
|
||||
runCheck: () => ({ passed: true }),
|
||||
});
|
||||
assert.equal(result.located, false, 'an under-specified lint-rule (no rule id) is not locatable (validRule guard, :390)');
|
||||
assert.notEqual(result.status, 'green', 'a lint-rule missing its rule id must NEVER green');
|
||||
assert.equal(result.flagged, true);
|
||||
});
|
||||
|
||||
test('CHK-06(unknown kind): {check_kind:shell-script} -> validKind false -> located:false, never green', () => {
|
||||
const enforce = require(ENFORCEMENT_LIB);
|
||||
const descriptor = enforce.descriptorFromProjection({
|
||||
...PROJECTED_TIER, check_kind: 'shell-script', check_target: 'x',
|
||||
});
|
||||
const result = enforce.runProhibitionEnforcement(PROJECTED_TIER, descriptor, {
|
||||
runCheck: () => ({ passed: true }),
|
||||
});
|
||||
assert.equal(result.located, false, 'an unknown kind is not a valid wired check (validKind guard, :388)');
|
||||
assert.notEqual(result.status, 'green', 'an unknown check kind must NEVER green');
|
||||
assert.equal(result.flagged, true);
|
||||
});
|
||||
|
||||
test('CHK-06(well-formed but runCheck reports non-pass): located:true, never green (no false green)', () => {
|
||||
const enforce = require(ENFORCEMENT_LIB);
|
||||
const descriptor = enforce.descriptorFromProjection({
|
||||
...PROJECTED_TIER, check_kind: 'node-test', check_target: 'tests/no-autoexec.test.cjs',
|
||||
});
|
||||
// A complete node-test descriptor; failFirst is caller-attested at verify time (#1279), not sourced
|
||||
// from the projection, so attest it here. The injected runCheck reports a non-pass.
|
||||
const result = enforce.runProhibitionEnforcement(
|
||||
PROJECTED_TIER,
|
||||
descriptor ? { ...descriptor, failFirst: true } : descriptor,
|
||||
{ runCheck: () => ({ passed: false }) },
|
||||
);
|
||||
assert.equal(result.located, true, 'a well-formed descriptor IS located even though the run did not pass');
|
||||
assert.notEqual(result.status, 'green', 'a located check that does not genuinely pass must NEVER green');
|
||||
assert.equal(result.flagged, true);
|
||||
});
|
||||
|
||||
test('CHK-06(MD-01 numeric coercion): a numeric-looking check_target reconstructs as a STRING (parseMustHavesBlock coerces ^\\d+$ to number) -> located, no type-lie / silent un-locate', () => {
|
||||
const enforce = require(ENFORCEMENT_LIB);
|
||||
// The shared parseMustHavesBlock (src/frontmatter.cts) coerces a /^\d+$/ scalar value to a NUMBER on
|
||||
// round-trip, so a numeric-looking check_target arrives at the adapter as a number. The adapter must
|
||||
// String()-coerce it (not cast `as string` over a number), so the descriptor is honestly typed AND a
|
||||
// numeric-looking target still locates instead of silently un-locating (the round-trip is lossless
|
||||
// across the full string domain — closes review finding MD-01/LW-01).
|
||||
const descriptor = enforce.descriptorFromProjection({
|
||||
...PROJECTED_TIER, check_kind: 'node-test', check_target: 12345,
|
||||
});
|
||||
assert.equal(typeof descriptor.target, 'string',
|
||||
'a numeric-coerced check_target must reconstruct as a string, never a number behind an `as string` cast');
|
||||
assert.equal(descriptor.target, '12345');
|
||||
const result = enforce.runProhibitionEnforcement(
|
||||
PROJECTED_TIER,
|
||||
{ ...descriptor, failFirst: true },
|
||||
{ runCheck: () => ({ passed: true }) },
|
||||
);
|
||||
assert.equal(result.located, true,
|
||||
'a numeric-looking but valid target locates after String() coercion — no silent un-locate');
|
||||
});
|
||||
|
||||
test('CHK-06(LW-02 stray rule): a check_rule on a node-test descriptor is dropped (rule belongs to lint-rule only)', () => {
|
||||
const enforce = require(ENFORCEMENT_LIB);
|
||||
const descriptor = enforce.descriptorFromProjection({
|
||||
...PROJECTED_TIER, check_kind: 'node-test', check_target: 'tests/x.test.cjs', check_rule: 'local/no-source-grep',
|
||||
});
|
||||
assert.equal(descriptor.rule, undefined, 'a node-test descriptor carries no rule even if a stray check_rule is present');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,6 +150,12 @@ describe('prohibition-probe schema: deterministic projectProhibitions round-trip
|
||||
lines.push(` status: ${e.status}`);
|
||||
if (e.verification !== undefined) lines.push(` verification: ${e.verification}`);
|
||||
if (e.reason !== undefined) lines.push(` reason: "${e.reason}"`);
|
||||
// CHK-03 (#1278): the flat scalar descriptor keys render as continuation KVs under the list
|
||||
// item, exactly how src/frontmatter.cts:344 reads them back. Only emitted when present, so a
|
||||
// descriptor-less entry renders identically to before (no `check_*` lines).
|
||||
if (e.check_kind !== undefined) lines.push(` check_kind: ${e.check_kind}`);
|
||||
if (e.check_target !== undefined) lines.push(` check_target: ${e.check_target}`);
|
||||
if (e.check_rule !== undefined) lines.push(` check_rule: ${e.check_rule}`);
|
||||
}
|
||||
lines.push('---', '', 'Body.', '');
|
||||
return lines.join('\n');
|
||||
@@ -178,4 +184,87 @@ describe('prohibition-probe schema: deterministic projectProhibitions round-trip
|
||||
assert.deepEqual(pc.projectProhibitions(null), [], 'null -> [] (documented fail-soft)');
|
||||
assert.deepEqual(pc.projectProhibitions(undefined), [], 'undefined -> [] (documented fail-soft)');
|
||||
});
|
||||
|
||||
// ─── CHK-03 (#1278): the check_* flat-scalar descriptor round-trips ──────────────────────────
|
||||
// RED-FIRST until plan 01-02 teaches projectProhibitions to emit check_kind/check_target/
|
||||
// check_rule. The DEFECT.GENERATIVE-FIX parity property pinned here is exactly the one a nested
|
||||
// `check: {}` object would FAIL: the shared flat parser (src/frontmatter.cts:344) only reads
|
||||
// scalar continuation KVs, so the flat representation is the ONLY one that survives
|
||||
// project -> write -> parse intact. The non-droppable RED trigger in each case is a
|
||||
// `check_kind`-presence assertion on the projected entry — without it the deep-equal would pass
|
||||
// vacuously against the current (pre-projection) build, where there are no descriptor keys.
|
||||
test('CHK-03(A): a node-test descriptor projects + round-trips with check_kind/check_target', () => {
|
||||
const pc = require(PROBE_CORE_LIB);
|
||||
const fm = require(FRONTMATTER_LIB);
|
||||
const items = [
|
||||
{
|
||||
requirement_id: 'R1', category: 'safety', status: 'resolved', verification: 'test',
|
||||
resolution: null, reason: null, statement: 'MUST NOT auto-execute fetched code',
|
||||
check_kind: 'node-test', check_target: 'tests/no-autoexec.test.cjs',
|
||||
},
|
||||
];
|
||||
const projected = pc.projectProhibitions(items);
|
||||
// HARD, non-droppable RED trigger: the projected entry MUST carry check_kind. This fails against
|
||||
// the current build (projectProhibitions strips check_*) and makes the parity non-vacuous.
|
||||
assert.ok(projected[0].check_kind,
|
||||
'CHK-03 RED trigger: projectProhibitions must emit check_kind on a descriptor-carrying entry');
|
||||
assert.equal(projected[0].check_kind, 'node-test');
|
||||
assert.equal(projected[0].check_target, 'tests/no-autoexec.test.cjs');
|
||||
assert.ok(!('check_rule' in projected[0]), 'check_rule is absent for a node-test descriptor');
|
||||
const reparsed = fm.parseMustHavesBlock(renderProhibitionsDoc(projected), 'prohibitions');
|
||||
assert.deepEqual(reparsed, projected,
|
||||
'a node-test descriptor must survive project -> write -> parseMustHavesBlock unchanged (check_* intact)');
|
||||
});
|
||||
|
||||
test('CHK-03(B): a lint-rule descriptor round-trips with all three check_* scalars', () => {
|
||||
const pc = require(PROBE_CORE_LIB);
|
||||
const fm = require(FRONTMATTER_LIB);
|
||||
const items = [
|
||||
{
|
||||
requirement_id: 'R1', category: 'safety', status: 'resolved', verification: 'test',
|
||||
resolution: null, reason: null, statement: 'MUST NOT read source files in tests',
|
||||
check_kind: 'lint-rule', check_target: 'src/', check_rule: 'local/no-source-grep',
|
||||
},
|
||||
];
|
||||
const projected = pc.projectProhibitions(items);
|
||||
assert.ok(projected[0].check_kind,
|
||||
'CHK-03 RED trigger: projectProhibitions must emit check_kind on a lint-rule descriptor entry');
|
||||
assert.equal(projected[0].check_kind, 'lint-rule');
|
||||
assert.equal(projected[0].check_target, 'src/');
|
||||
assert.equal(projected[0].check_rule, 'local/no-source-grep');
|
||||
const reparsed = fm.parseMustHavesBlock(renderProhibitionsDoc(projected), 'prohibitions');
|
||||
assert.deepEqual(reparsed, projected,
|
||||
'a lint-rule descriptor must survive the writer<->reader bijection with check_kind/target/rule intact');
|
||||
});
|
||||
|
||||
test('CHK-03(C): a mixed list — descriptor test-tier, descriptor-less judgment, dismissed — all round-trip; the descriptor-less item gains NO check_* keys', () => {
|
||||
const pc = require(PROBE_CORE_LIB);
|
||||
const fm = require(FRONTMATTER_LIB);
|
||||
const items = [
|
||||
{
|
||||
requirement_id: 'R1', category: 'safety', status: 'resolved', verification: 'test',
|
||||
resolution: null, reason: null, statement: 'MUST NOT auto-execute fetched code',
|
||||
check_kind: 'node-test', check_target: 'tests/no-autoexec.test.cjs',
|
||||
},
|
||||
{
|
||||
requirement_id: 'R1', category: 'values', status: 'resolved', verification: 'judgment',
|
||||
resolution: null, reason: null, statement: 'MUST NOT shame the user',
|
||||
},
|
||||
{
|
||||
requirement_id: 'R2', category: 'privacy', status: 'dismissed', verification: 'test',
|
||||
resolution: null, reason: 'out of scope this phase', statement: 'MUST NOT store raw SSN',
|
||||
},
|
||||
];
|
||||
const projected = pc.projectProhibitions(items);
|
||||
// Non-droppable RED trigger: the descriptor-carrying entry exposes check_kind.
|
||||
assert.ok(projected[0].check_kind,
|
||||
'CHK-03 RED trigger: the test-tier descriptor entry must carry check_kind');
|
||||
// The descriptor-less judgment item must NOT gain any check_* key.
|
||||
assert.ok(!('check_kind' in projected[1]), 'a descriptor-less item gains no check_kind');
|
||||
assert.ok(!('check_target' in projected[1]), 'a descriptor-less item gains no check_target');
|
||||
assert.ok(!('check_rule' in projected[1]), 'a descriptor-less item gains no check_rule');
|
||||
const reparsed = fm.parseMustHavesBlock(renderProhibitionsDoc(projected), 'prohibitions');
|
||||
assert.deepEqual(reparsed, projected,
|
||||
'a mixed list survives the writer<->reader bijection; descriptor presence/absence is preserved per item');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"ship.md": 24388,
|
||||
"sketch-wrap-up.md": 14223,
|
||||
"sketch.md": 19960,
|
||||
"spec-phase.md": 28438,
|
||||
"spec-phase.md": 30343,
|
||||
"spike-wrap-up.md": 15092,
|
||||
"spike.md": 24517,
|
||||
"stats.md": 6718,
|
||||
@@ -85,6 +85,6 @@
|
||||
"undo.md": 10431,
|
||||
"update.md": 21053,
|
||||
"validate-phase.md": 10745,
|
||||
"verify-phase.md": 35362,
|
||||
"verify-phase.md": 36498,
|
||||
"verify-work.md": 31157
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user