* feat(verify-phase): honest verifier — abstain (insufficient_spec) on non-inferable backstop truths (#1154) Carry the edge-probe's existing `backstop` (non-inferable) tier through the plan-phase projection as a structured flat-scalar marker instead of a prose parenthetical, and make verify-phase abstain -> human_needed (never silent-pass) on a backstop truth it cannot confirm with explicit evidence. Truth-axis mirror of #644's prohibition judgment-tier (ADR-550 D4). Engine (deterministic, CI-tested per ADR-550 D5 — never the LLM verdict): - src/probe-core.cts: truthStatement/truthVerification normalizers, projectTruths (conservative serializer), dispositionForUnverifiableTruth (backstop+no-evidence -> unverified/flagged/insufficient_spec; backstop+evidence -> green; inferable -> green, the over-abstention guard). - src/roadmap.cts: coerceTruthToString now reads `statement` first so an object-form backstop truth is surfaced, not dropped (Hyrum backward-compat for truth-readers). Workflow/agent/docs: plan-phase emits the structured marker (flat scalar, ADR-550 #1278); verify-phase + gsd-verifier add the abstain arm; new references/honest-verifier.md; FEATURES/COMMANDS document insufficient_spec; ADR-550 amended (truth-axis D4 mirror). Decisions adopted (trek-e review): insufficient_spec feeds existing human_needed with a distinguishable reason (no new VERIFIER_STATUS); changeset Changed; round-trip parity test; abstain-on-unconfirmed-backstop regression test red-first. Implementation notes (deviations from the issue's proposed file list, verified live): - frontmatter.cts needs no change — its flat parser already round-trips object-form truths. - verify.cts needs no change — it grades artifacts/key_links structurally; truths are LLM-graded at the workflow layer, so consumption lives there + the deterministic helper. - No CJS<->SDK hand-sync — the SDK seam was retired (ADR-0174); src/*.cts is sole source. Regenerated artifacts: golden-install-parity fixtures, INVENTORY-MANIFEST, size baselines. * chore(#1154): add changeset (Changed) for honest verifier User-facing changelog fragment for #1738. Typed `Changed` (not `Added`) per trek-e review condition 3 — the verify behavior shifts for backstop-bearing specs (a confident silent `passed` becomes `human_needed`), which is user-visible even though the schema marker is additive. * docs(#1154): score-formula also excludes abstained insufficient_spec truths (review nit-1) trek-e review nit: the verify-phase score sentence said PRESENT_BEHAVIOR_UNVERIFIED truths were "the only ones excluded" from verified_truths. Post-#1154 an abstained `insufficient_spec` backstop truth is also excluded (it is not ✓ VERIFIED and routes to human_needed). Behavior was already correct; this tightens the wording. Regenerated golden-install-parity fixtures + workflow-size baseline for the touched verify-phase.md. (Nit-2 — a dedicated insufficient_spec_items frontmatter list — is intentionally not taken: the current design is ADR-550-D4-conformant, the abstain cause rides as a distinguishable report reason, and adding it would exceed the approved scope.) --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -513,3 +513,111 @@ describe('probe-core: projectProhibitions descriptor projection (CHK-02)', () =>
|
||||
assert.ok(!('check_rule' in projected[0]), 'descriptor-less item gains no check_rule');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Honest verifier (#1154): truth-axis abstention — the verify-time MIRROR of #644's
|
||||
// prohibition judgment-tier (ADR-550 D4), applied to the edge `backstop` truth tier (D7a).
|
||||
// Per ADR-550 D5 the deterministic, CI-testable surface is the disposition helper + the
|
||||
// projection ROUND-TRIP — NEVER the LLM verdict (a test asserting the model's judgment is
|
||||
// vacuous and rejected). The abstain-on-unconfirmed-backstop case is the REGRESSION
|
||||
// (trek-e review condition 5) that must fail RED on `next` before the fix.
|
||||
const FM_SCRIPT = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'frontmatter.cjs');
|
||||
const fm = require(FM_SCRIPT);
|
||||
|
||||
// Serialize projected truths into a plan-frontmatter `must_haves.truths` block exactly as
|
||||
// plan-phase emits it — a backstop truth as a flat-scalar object (ADR-550 #1278: flat scalars,
|
||||
// NEVER a nested object), a plain inferable truth as a bare string.
|
||||
function renderTruthsBlock(projected) {
|
||||
const lines = ['---', 'must_haves:', ' truths:'];
|
||||
for (const t of projected) {
|
||||
if (typeof t === 'string') {
|
||||
lines.push(` - ${t}`);
|
||||
} else {
|
||||
lines.push(` - statement: ${t.statement}`);
|
||||
if (t.verification) lines.push(` verification: ${t.verification}`);
|
||||
}
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
describe('probe-core: truthStatement / truthVerification normalizers (#1154, Hyrum backward-compat)', () => {
|
||||
test('truthStatement extracts text from a plain-string truth and an object-form truth identically', () => {
|
||||
assert.equal(pc.truthStatement('Overlapping intervals are merged'), 'Overlapping intervals are merged');
|
||||
assert.equal(
|
||||
pc.truthStatement({ statement: 'Adjacent intervals merge', verification: 'backstop' }),
|
||||
'Adjacent intervals merge',
|
||||
);
|
||||
});
|
||||
|
||||
test('truthVerification returns the tier for an object-form truth and null for a plain string (no spurious tier)', () => {
|
||||
assert.equal(pc.truthVerification({ statement: 'Adjacent intervals merge', verification: 'backstop' }), 'backstop');
|
||||
assert.equal(pc.truthVerification('Overlapping intervals are merged'), null);
|
||||
assert.equal(pc.truthVerification({ statement: 'x', verification: 'explicit' }), 'explicit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('probe-core: dispositionForUnverifiableTruth (#1154, ADR-550 D4 truth-axis mirror)', () => {
|
||||
test('abstain-on-unconfirmed-backstop (condition 5, REGRESSION): a backstop truth with no explicit evidence disposes insufficient_spec/unverified/flagged — never green', () => {
|
||||
const d = pc.dispositionForUnverifiableTruth(
|
||||
{ statement: 'Adjacent/touching intervals [1,2],[2,3] merge', verification: 'backstop' },
|
||||
{ evidence: [] },
|
||||
);
|
||||
assert.equal(d.status, 'unverified', 'a backstop truth with no explicit evidence is unverified');
|
||||
assert.equal(d.flagged, true, 'and flagged — never a silent pass (ADR-550 D4)');
|
||||
assert.equal(d.tier, 'backstop', 'the backstop tier is echoed unchanged');
|
||||
assert.equal(
|
||||
d.reason,
|
||||
'insufficient_spec',
|
||||
'carries the distinguishable insufficient_spec reason code so human_needed is not conflated with ordinary manual-UAT',
|
||||
);
|
||||
});
|
||||
|
||||
test('pass-on-wired-backstop: a backstop truth WITH explicit evidence (a wired held-out/property test) disposes green — abstention is for the unconfirmable only', () => {
|
||||
const d = pc.dispositionForUnverifiableTruth(
|
||||
{ statement: 'Adjacent/touching intervals [1,2],[2,3] merge', verification: 'backstop' },
|
||||
{ evidence: [{ test: 'tests/intervals.property.test.cjs', passed: true }] },
|
||||
);
|
||||
assert.equal(d.status, 'green');
|
||||
assert.equal(d.flagged, false);
|
||||
assert.equal(d.tier, 'backstop');
|
||||
});
|
||||
|
||||
test('over-abstention guard (AC#3): a plain inferable truth is NEVER routed to abstention', () => {
|
||||
const d = pc.dispositionForUnverifiableTruth('Overlapping intervals are merged', { evidence: [] });
|
||||
assert.equal(d.status, 'green', 'a plain inferable truth is graded normally, never abstained');
|
||||
assert.equal(d.flagged, false);
|
||||
});
|
||||
|
||||
test('over-abstention guard (AC#3): an explicit-tier truth NEVER abstains even with no evidence (only backstop triggers it)', () => {
|
||||
const d = pc.dispositionForUnverifiableTruth({ statement: 'Symbol X is wired', verification: 'explicit' }, { evidence: [] });
|
||||
assert.equal(d.status, 'green', 'an explicit (inferable) truth never abstains');
|
||||
assert.equal(d.flagged, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('probe-core: projectTruths (#1154, conservative serializer — Postel) + round-trip parity (condition 4, ADR-550 D5b)', () => {
|
||||
test('projectTruths emits the flat-scalar backstop marker and collapses inferable truths to plain strings', () => {
|
||||
const projected = pc.projectTruths([
|
||||
{ statement: 'Adjacent intervals merge', verification: 'backstop' },
|
||||
'Overlapping intervals are merged',
|
||||
{ statement: 'Symbol X is wired', verification: 'explicit' },
|
||||
]);
|
||||
assert.deepEqual(projected, [
|
||||
{ statement: 'Adjacent intervals merge', verification: 'backstop' },
|
||||
'Overlapping intervals are merged',
|
||||
'Symbol X is wired',
|
||||
], 'only a backstop truth carries a structured marker; explicit/inferable truths stay bare strings (no spurious markers)');
|
||||
});
|
||||
|
||||
test('round-trip parity (SPEC backstop edge → must_haves.truths marker → read-back): the marker survives as a structured field, plain truths byte-identically', () => {
|
||||
const projected = pc.projectTruths([
|
||||
{ statement: 'Adjacent intervals merge', verification: 'backstop' },
|
||||
'Overlapping intervals are merged',
|
||||
]);
|
||||
const parsed = fm.parseMustHavesBlock(renderTruthsBlock(projected), 'truths');
|
||||
assert.equal(pc.truthVerification(parsed[0]), 'backstop', 'the backstop marker survives the round-trip as a structured field, not prose (#1110 fragility avoided)');
|
||||
assert.equal(pc.truthStatement(parsed[0]), 'Adjacent intervals merge');
|
||||
assert.equal(pc.truthStatement(parsed[1]), 'Overlapping intervals are merged');
|
||||
assert.equal(pc.truthVerification(parsed[1]), null, 'a plain truth round-trips with no marker (Hyrum byte-identity backward-compat)');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user