feat(spec-phase): surface zero-classification edge-probe requirements as unclassified candidates (#1110) (#1117)

Edge-probe now surfaces a zero-classification requirement (non-empty prose, no
shape cue matched, no `shapes` override) as a single soft `unclassified — review
manually` candidate instead of silently dropping it — the exact blind spot the
probe exists to catch. Dismissible like any edge; the `shapes: []` opt-out stays
silent; `TAXONOMY` (the closed 8 categories) is unchanged. Under `--auto` the
candidate is left `unresolved`, never auto-`backstop` (a missing shape is not
evidence an edge exists).

Closes #1110
This commit is contained in:
Rezolv
2026-06-14 21:44:21 -04:00
committed by GitHub
parent 395fb519e7
commit 3556450b0d
9 changed files with 111 additions and 4 deletions

View File

@@ -219,6 +219,57 @@ describe('edge-probe: proposeEdges — empty-shapes override (RR-06)', () => {
});
});
describe('edge-probe: proposeEdges — unclassified candidate for prose-zero-cue (#1110)', () => {
// A requirement with non-empty prose that matches NO shape cue must not be silently
// dropped (zero edges, no signal). It now surfaces ONE soft "unclassified — review
// manually" candidate instead. The explicit `shapes: []` opt-out stays silent.
test('prose with no shape cue surfaces a single unclassified candidate', () => {
const edges = ep.proposeEdges({ id: 'R1', text: 'Display the company logo' });
assert.equal(edges.length, 1, 'prose-zero-cue must surface exactly one unclassified candidate');
assert.deepEqual(edges[0], {
requirement_id: 'R1',
category: 'unclassified',
status: 'unresolved',
verification: null,
resolution: null,
reason: null,
probe: 'unclassified — review manually',
});
});
test('UNCLASSIFIED_CATEGORY is a valid item category but NOT a taxonomy category', () => {
assert.equal(ep.UNCLASSIFIED_CATEGORY, 'unclassified');
assert.ok(ep.EDGE_VALIDATORS.categories.includes('unclassified'), 'analyzeCoverage must accept the unclassified item category');
assert.ok(!ep.TAXONOMY.some((c) => c.id === 'unclassified'), 'unclassified must NOT pollute the closed 8-category taxonomy');
});
test('explicit shapes: [] stays silent (deliberate opt-out — NOT unclassified)', () => {
const edges = ep.proposeEdges({ id: 'R1', text: 'Display the company logo', shapes: [] });
assert.deepEqual(edges, []);
});
test('prose that DOES classify proposes real edges, never an unclassified candidate', () => {
const edges = ep.proposeEdges({ id: 'R1', text: 'Round a number to N decimal places' });
assert.ok(edges.length > 0);
assert.ok(!edges.some((e) => e.category === 'unclassified'), 'a classifiable requirement must not emit unclassified');
});
test('analyzeCoverage surfaces the unclassified candidate as unresolved (no throw)', () => {
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.items[0].category, 'unclassified');
});
test('an unclassified candidate can be dismissed with a reason (edge-probe parity)', () => {
const report = ep.analyzeCoverage(
[{ id: 'R1', text: 'Display the company logo' }],
[{ requirement_id: 'R1', category: 'unclassified', status: 'dismissed', reason: 'genuinely edge-free — static asset' }],
);
assert.equal(report.items[0].status, 'dismissed');
});
});
describe('edge-probe: proposeEdges — invalid authored shapes fail closed (re-review #3 High)', () => {
// A non-empty but INVALID shapes array must NOT silently suppress every probe.
// shapes:['numeric'] (typo for the locked 'numeric-range') previously passed

View File

@@ -72,7 +72,7 @@
"ship.md": 24388,
"sketch-wrap-up.md": 14223,
"sketch.md": 19960,
"spec-phase.md": 27589,
"spec-phase.md": 28438,
"spike-wrap-up.md": 15092,
"spike.md": 24517,
"stats.md": 6718,