fix(#4656): expose coverage.unclassified and widen the zero-applicable guards (#4800)

* fix(#4656): expose coverage.unclassified and widen the zero-applicable guards

* fix(#4656): regenerate golden coverage fixtures and update the rollup pin

Emitted-Drift-Ack-Growth: spec-phase.md — #4656: guard widened to the all-unclassified case, doc claim corrected
Emitted-Drift-Ack-Growth: ui-phase.md — #4656: guard widened identically

* fix(#4656): sync edge-probe doc blocks and coverage pins with the new field

* fix(#4656): key the mandatory confirmation on the widened guard

* docs(#4656): backfill changeset PR number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-16 10:33:32 -04:00
committed by GitHub
parent caecaec62e
commit 3014775a3f
14 changed files with 515 additions and 53 deletions

View File

@@ -10,7 +10,7 @@
process.env.GSD_TEST_MODE = '1';
const { test } = require('node:test');
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
@@ -560,3 +560,37 @@ test('#3717: the edge-probe reference documents the text_en field', () => {
'the edge-probe reference `## Inputs` contract must document the optional text_en field and its text_en ?? text fallback',
);
});
describe('#4656: the zero-applicable guard is widened to the all-unclassified case', () => {
// allow-test-rule: source-text-is-the-product (#4656) — spec-phase.md/ui-phase.md
// guard text is the shipped workflow contract.
const read = (f) => fs.readFileSync(path.join(__dirname, '..', 'gsd-core', 'workflows', f), 'utf-8');
test('spec-phase extracts coverage.unclassified and the guard fires on either condition', () => {
const content = read('spec-phase.md');
assert.ok(
content.includes('coverage.unclassified'),
'step 5.5 must extract the unclassified sibling count the #4656 rollup exposes',
);
assert.ok(
content.includes('[ "$UNCLASSIFIED" = "$APPLICABLE" ]'),
'the guard must fire when every requirement is unclassified (#4656), not only at applicable:0',
);
assert.ok(
content.includes('If the guard above fired'),
'the mandatory AskUserQuestion confirmation must key on the widened guard, not only on applicable:0 (#4656)',
);
});
test('ui-phase carries the same widened guard', () => {
const content = read('ui-phase.md');
assert.ok(
content.includes('coverage.unclassified') && content.includes('[ "$UNCLASSIFIED" = "$APPLICABLE" ]'),
'ui-phase.md:385 guard must be widened identically (#4656)',
);
assert.ok(
content.includes('If the guard above fired'),
'ui-phase confirmation prose must key on the widened guard too (#4656)',
);
});
});

View File

@@ -124,7 +124,7 @@ describe('edge-probe: analyzeCoverage', () => {
const reqs = [{ id: 'R1', text: 'Merge a list of overlapping intervals' }];
test('with no resolutions, every applicable edge is unresolved (byVerification zeroed)', () => {
const rep = ep.analyzeCoverage(reqs, []);
assert.deepEqual(rep.coverage, { applicable: 3, resolved: 0, unresolved: 3, byVerification: { explicit: 0, backstop: 0 } });
assert.deepEqual(rep.coverage, { applicable: 3, resolved: 0, unresolved: 3, unclassified: 0, byVerification: { explicit: 0, backstop: 0 } });
});
test('merges a resolved/explicit resolution and counts it resolved', () => {
const rep = ep.analyzeCoverage(reqs, [
@@ -153,7 +153,7 @@ describe('edge-probe: CLI (built artifact)', () => {
const nodeResult = runNode([BUILT_SCRIPT, reqPath], { timeoutMs: PROBE_TIMEOUT_MS });
throwIfFailed(nodeResult, `node ${BUILT_SCRIPT} ${reqPath}`);
const rep = JSON.parse(nodeResult.stdout);
assert.deepEqual(rep.coverage, { applicable: 2, resolved: 0, unresolved: 2, byVerification: { explicit: 0, backstop: 0 } });
assert.deepEqual(rep.coverage, { applicable: 2, resolved: 0, unresolved: 2, unclassified: 0, byVerification: { explicit: 0, backstop: 0 } });
});
test('with no args exits with status 2 (assert on exit code, not stderr prose)', () => {
const result = runNode([BUILT_SCRIPT], { timeoutMs: PROBE_TIMEOUT_MS });
@@ -194,7 +194,7 @@ describe('edge-probe: CLI JSON.parse error handling (RR-10)', () => {
const r = runNode([BUILT_SCRIPT, reqPath], { timeoutMs: PROBE_TIMEOUT_MS });
assert.equal(r.exitCode, 0);
const rep = JSON.parse(r.stdout);
assert.deepEqual(rep.coverage, { applicable: 2, resolved: 0, unresolved: 2, byVerification: { explicit: 0, backstop: 0 } });
assert.deepEqual(rep.coverage, { applicable: 2, resolved: 0, unresolved: 2, unclassified: 0, byVerification: { explicit: 0, backstop: 0 } });
} finally {
cleanup(dir);
}
@@ -255,6 +255,7 @@ describe('edge-probe: proposeEdges — unclassified candidate for prose-zero-cue
const report = ep.analyzeCoverage([{ id: 'R1', text: 'Display the company logo' }]);
assert.equal(report.coverage.applicable, 1);
assert.equal(report.coverage.unresolved, 1);
assert.equal(report.coverage.unclassified, 1, '#4656: the unclassified sibling count must expose the soft-signal row');
assert.equal(report.items[0].category, 'unclassified');
});

View File

@@ -137,7 +137,7 @@ describe('probe-core: analyzeCoverage (merge · rollup · byVerification)', () =
test('no resolutions → every item unresolved; resolved 0; byVerification zeroed per tier', () => {
const rep = pc.analyzeCoverage(UNRESOLVED_ITEMS, [], VALIDATORS);
assert.deepEqual(rep.coverage, {
applicable: 3, resolved: 0, unresolved: 3, byVerification: { explicit: 0, backstop: 0 },
applicable: 3, resolved: 0, unresolved: 3, unclassified: 0, byVerification: { explicit: 0, backstop: 0 },
});
});
test('merges a resolved/explicit resolution and counts byVerification.explicit', () => {
@@ -688,3 +688,58 @@ describe('probe-core: projectTruths (#1154, conservative serializer — Postel)
assert.equal(pc.truthVerification(parsed[1]), null, 'a plain truth round-trips with no marker (Hyrum byte-identity backward-compat)');
});
});
describe('probe-core: coverage.unclassified sibling count (#4656)', () => {
// #1110's soft-signal rows are legitimate rollup members, so `applicable`
// counts them and the documented zero-applicable guards could never fire on
// an all-unclassified spec. The fix exposes the count as a SIBLING field,
// leaving `applicable` and the documented `resolved = applicable -
// unresolved` identity count-preserving.
const UNCLASSIFIED_VALIDATORS = {
categories: ['idempotency', 'ordering', 'unclassified'],
verification: ['explicit', 'backstop'],
requiredFieldsByVerification: { explicit: ['resolution'], backstop: ['resolution'] },
};
test('the issue defect case: all requirements unclassified reports unclassified === applicable', () => {
const items = [
item('unclassified', { requirement_id: 'CONN-01' }),
item('unclassified', { requirement_id: 'CONN-02' }),
item('unclassified', { requirement_id: 'PJL-01' }),
];
const rep = pc.analyzeCoverage(items, [], UNCLASSIFIED_VALIDATORS);
assert.equal(rep.coverage.applicable, 3);
assert.equal(rep.coverage.unclassified, 3, 'the all-unclassified case must be distinguishable from a spec with genuine edge surface');
assert.equal(rep.coverage.resolved, 0);
assert.equal(rep.coverage.unresolved, 3);
});
test('a classified spec reports unclassified 0 (control 1 — text_en populated)', () => {
const items = [
item('idempotency', { requirement_id: 'CONN-01' }),
item('ordering', { requirement_id: 'CONN-02' }),
];
const rep = pc.analyzeCoverage(items, [], UNCLASSIFIED_VALIDATORS);
assert.equal(rep.coverage.applicable, 2);
assert.equal(rep.coverage.unclassified, 0);
});
test('an edge-free requirement (no items) reports applicable 0 and unclassified 0 (control 2)', () => {
const rep = pc.analyzeCoverage([], [], UNCLASSIFIED_VALIDATORS);
assert.equal(rep.coverage.applicable, 0);
assert.equal(rep.coverage.unclassified, 0);
});
test('mixed spec: unclassified is a strict subset of applicable; the counting identity is preserved', () => {
const items = [
item('idempotency', { requirement_id: 'CONN-01' }),
item('unclassified', { requirement_id: 'CONN-02' }),
];
const rep = pc.analyzeCoverage(items, [], UNCLASSIFIED_VALIDATORS);
assert.equal(rep.coverage.applicable, 2);
assert.equal(rep.coverage.unclassified, 1);
assert.ok(rep.coverage.unclassified <= rep.coverage.applicable);
assert.equal(rep.coverage.resolved, rep.coverage.applicable - rep.coverage.unresolved);
});
});