* 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:
@@ -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)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user