* 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:
5
.changeset/lucky-jaguars-wake.md
Normal file
5
.changeset/lucky-jaguars-wake.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4800
|
||||
---
|
||||
**The spec/ui zero-applicable guard now fires on the all-unclassified case** — probe coverage exposes an `unclassified` count beside `applicable`, and spec-phase/ui-phase warn when every requirement classifies to nothing instead of reporting a healthy non-zero total; `applicable` itself is count-preserving. (#4656)
|
||||
@@ -1,7 +1,32 @@
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "boundary", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What happens exactly at each min/max/threshold — and one step either side?" },
|
||||
{ "requirement_id": "R1", "category": "precision", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "boundary",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What happens exactly at each min/max/threshold — and one step either side?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "precision",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 2,
|
||||
"resolved": 0,
|
||||
"unresolved": 2,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "adjacency", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
|
||||
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
|
||||
{ "requirement_id": "R1", "category": "ordering", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When elements compare equal, is output order specified and stable?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "adjacency",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "empty",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What is the result for empty, single-element, or null input?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "ordering",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "When elements compare equal, is output order specified and stable?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 3, "resolved": 0, "unresolved": 3, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 3,
|
||||
"resolved": 0,
|
||||
"unresolved": 3,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
|
||||
{ "requirement_id": "R1", "category": "encoding", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Whose definition of length/equality applies — bytes, code points, grapheme clusters, or normalized form?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "empty",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What is the result for empty, single-element, or null input?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "encoding",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "Whose definition of length/equality applies — bytes, code points, grapheme clusters, or normalized form?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 2,
|
||||
"resolved": 0,
|
||||
"unresolved": 2,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "boundary", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What happens exactly at each min/max/threshold — and one step either side?" },
|
||||
{ "requirement_id": "R1", "category": "precision", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "boundary",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What happens exactly at each min/max/threshold — and one step either side?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "precision",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 2,
|
||||
"resolved": 0,
|
||||
"unresolved": 2,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "adjacency", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
|
||||
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
|
||||
{ "requirement_id": "R1", "category": "ordering", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When elements compare equal, is output order specified and stable?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "adjacency",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "empty",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What is the result for empty, single-element, or null input?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "ordering",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "When elements compare equal, is output order specified and stable?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 3, "resolved": 0, "unresolved": 3, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 3,
|
||||
"resolved": 0,
|
||||
"unresolved": 3,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "adjacency", "status": "resolved", "verification": "explicit", "resolution": "AC#6: touching intervals merge", "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
|
||||
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
|
||||
{ "requirement_id": "R1", "category": "ordering", "status": "dismissed", "verification": null, "resolution": null, "reason": "output is canonically sorted; no tie possible", "probe": "When elements compare equal, is output order specified and stable?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "adjacency",
|
||||
"status": "resolved",
|
||||
"verification": "explicit",
|
||||
"resolution": "AC#6: touching intervals merge",
|
||||
"reason": null,
|
||||
"probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "empty",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What is the result for empty, single-element, or null input?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "ordering",
|
||||
"status": "dismissed",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": "output is canonically sorted; no tie possible",
|
||||
"probe": "When elements compare equal, is output order specified and stable?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 3, "resolved": 2, "unresolved": 1, "byVerification": { "explicit": 1, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 3,
|
||||
"resolved": 2,
|
||||
"unresolved": 1,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 1,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,11 +169,44 @@ resolutions supplied, every applicable edge is `unresolved`:
|
||||
```json edge-probe:02-merge-intervals/expected-coverage.json
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "adjacency", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
|
||||
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
|
||||
{ "requirement_id": "R1", "category": "ordering", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When elements compare equal, is output order specified and stable?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "adjacency",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "empty",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What is the result for empty, single-element, or null input?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "ordering",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "When elements compare equal, is output order specified and stable?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 3, "resolved": 0, "unresolved": 3, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 3,
|
||||
"resolved": 0,
|
||||
"unresolved": 3,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -192,10 +225,35 @@ rule, the requirement classifies as `numeric-range`, which raises `boundary` and
|
||||
```json edge-probe:01-round-half-even/expected-coverage.json
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "boundary", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What happens exactly at each min/max/threshold — and one step either side?" },
|
||||
{ "requirement_id": "R1", "category": "precision", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "boundary",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What happens exactly at each min/max/threshold — and one step either side?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "precision",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 2,
|
||||
"resolved": 0,
|
||||
"unresolved": 2,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -211,10 +269,35 @@ the requirement classifies as `text`, which raises `empty` and `encoding`:
|
||||
```json edge-probe:03-truncate-graphemes/expected-coverage.json
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
|
||||
{ "requirement_id": "R1", "category": "encoding", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Whose definition of length/equality applies — bytes, code points, grapheme clusters, or normalized form?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "empty",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What is the result for empty, single-element, or null input?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "encoding",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "Whose definition of length/equality applies — bytes, code points, grapheme clusters, or normalized form?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 2,
|
||||
"resolved": 0,
|
||||
"unresolved": 2,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -231,10 +314,35 @@ classifies as `numeric-range`, which raises `boundary` and `precision`:
|
||||
```json edge-probe:04-money-rounding/expected-coverage.json
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "boundary", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What happens exactly at each min/max/threshold — and one step either side?" },
|
||||
{ "requirement_id": "R1", "category": "precision", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "boundary",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What happens exactly at each min/max/threshold — and one step either side?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "precision",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 2,
|
||||
"resolved": 0,
|
||||
"unresolved": 2,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -250,11 +358,44 @@ Given a requirement to deduplicate a list of items, the requirement classifies a
|
||||
```json edge-probe:05-list-dedupe/expected-coverage.json
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "adjacency", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
|
||||
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
|
||||
{ "requirement_id": "R1", "category": "ordering", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When elements compare equal, is output order specified and stable?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "adjacency",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "empty",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What is the result for empty, single-element, or null input?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "ordering",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "When elements compare equal, is output order specified and stable?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 3, "resolved": 0, "unresolved": 3, "byVerification": { "explicit": 0, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 3,
|
||||
"resolved": 0,
|
||||
"unresolved": 3,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 0,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -270,11 +411,44 @@ left unresolved:
|
||||
```json edge-probe:06-resolved-mixed/expected-coverage.json
|
||||
{
|
||||
"items": [
|
||||
{ "requirement_id": "R1", "category": "adjacency", "status": "resolved", "verification": "explicit", "resolution": "AC#6: touching intervals merge", "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
|
||||
{ "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
|
||||
{ "requirement_id": "R1", "category": "ordering", "status": "dismissed", "verification": null, "resolution": null, "reason": "output is canonically sorted; no tie possible", "probe": "When elements compare equal, is output order specified and stable?" }
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "adjacency",
|
||||
"status": "resolved",
|
||||
"verification": "explicit",
|
||||
"resolution": "AC#6: touching intervals merge",
|
||||
"reason": null,
|
||||
"probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "empty",
|
||||
"status": "unresolved",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": null,
|
||||
"probe": "What is the result for empty, single-element, or null input?"
|
||||
},
|
||||
{
|
||||
"requirement_id": "R1",
|
||||
"category": "ordering",
|
||||
"status": "dismissed",
|
||||
"verification": null,
|
||||
"resolution": null,
|
||||
"reason": "output is canonically sorted; no tie possible",
|
||||
"probe": "When elements compare equal, is output order specified and stable?"
|
||||
}
|
||||
],
|
||||
"coverage": { "applicable": 3, "resolved": 2, "unresolved": 1, "byVerification": { "explicit": 1, "backstop": 0 } }
|
||||
"coverage": {
|
||||
"applicable": 3,
|
||||
"resolved": 2,
|
||||
"unresolved": 1,
|
||||
"unclassified": 0,
|
||||
"byVerification": {
|
||||
"explicit": 1,
|
||||
"backstop": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -206,7 +206,10 @@ The SPEC keeps the original language — only `text_en` is translated, and requi
|
||||
Criteria you write from the resolved edges go into the SPEC in `response_language`). Populate
|
||||
`text_en` for **every** requirement, not only the ones that look edge-relevant: the
|
||||
`$APPLICABLE = 0` warning below fires only when *all* requirements are unclassified, so a
|
||||
partly-classified spec slips through with no signal at all. When `response_language` is unset
|
||||
partly-classified spec slips through with no signal at all. (#4656) The warning now ALSO
|
||||
fires on the all-unclassified case itself: `coverage.unclassified` counts the soft-signal
|
||||
rows, and the guard below fires when `$UNCLASSIFIED = $APPLICABLE` — the case where the
|
||||
classifier learned nothing about ANY requirement. When `response_language` is unset
|
||||
(an English-language project), omit `text_en` — `text` is already English and the engine
|
||||
falls back to it automatically (`text_en ?? text`).
|
||||
If a requirement still classifies to zero shapes with `text_en` populated, it carries no cue in
|
||||
@@ -315,12 +318,15 @@ echo "### (end edge-probe coverage report)"
|
||||
# applicable:0. Surface it loudly; the author must explicitly confirm "no applicable edges"
|
||||
# below rather than silently emitting a green empty ## Edge Coverage section.
|
||||
APPLICABLE=$(printf '%s' "$COVERAGE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let n=0;try{n=JSON.parse(s).coverage.applicable}catch{n=0}process.stdout.write(String(n))})')
|
||||
if [ "$APPLICABLE" = "0" ]; then
|
||||
UNCLASSIFIED=$(printf '%s' "$COVERAGE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let n=0;try{n=JSON.parse(s).coverage.unclassified}catch{n=0}process.stdout.write(String(n))})')
|
||||
# #4656: all-unclassified is the accidental-miss case the applicable:0 guard could never
|
||||
# reach — #1110's soft-signal rows make applicable non-zero by construction there.
|
||||
if [ "$APPLICABLE" = "0" ] || [ "$UNCLASSIFIED" = "$APPLICABLE" ]; then
|
||||
echo "WARNING: edge-probe proposed ZERO applicable edges across all requirements — likely a classification miss or malformed requirements, not a genuinely edge-free spec. Do NOT silently write an empty Edge Coverage section." >&2
|
||||
fi
|
||||
```
|
||||
|
||||
If `$APPLICABLE` is `0`, do NOT proceed silently: ask the author to confirm via AskUserQuestion
|
||||
If the guard above fired (`$APPLICABLE` is `0`, or every requirement is unclassified — `$UNCLASSIFIED = $APPLICABLE`, #4656), do NOT proceed silently: ask the author to confirm via AskUserQuestion
|
||||
("The edge probe found no applicable edges for any requirement — is this genuinely an
|
||||
edge-free spec, or should we revisit the requirement wording / authored shapes?"). Only write
|
||||
an empty `## Edge Coverage` section after explicit confirmation.
|
||||
|
||||
@@ -385,12 +385,14 @@ fi
|
||||
# Zero-applicable guard: a report where NO category applied across ANY element is far more likely a
|
||||
# classification miss (or malformed elements) than a genuinely state-free UI. Surface it loudly.
|
||||
APPLICABLE=$(printf '%s' "$COVERAGE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let n=0;try{n=JSON.parse(s).coverage.applicable}catch{n=0}process.stdout.write(String(n))})')
|
||||
if [ "$APPLICABLE" = "0" ]; then
|
||||
UNCLASSIFIED=$(printf '%s' "$COVERAGE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let n=0;try{n=JSON.parse(s).coverage.unclassified}catch{n=0}process.stdout.write(String(n))})')
|
||||
# #4656: all-unclassified reads as a non-zero applicable — widen the guard to fire there too.
|
||||
if [ "$APPLICABLE" = "0" ] || [ "$UNCLASSIFIED" = "$APPLICABLE" ]; then
|
||||
echo "WARNING: ui-consideration-probe proposed ZERO applicable categories across all elements — likely a classification miss or malformed elements, not a genuinely state-free UI. Do NOT silently write an empty UI Considerations section." >&2
|
||||
fi
|
||||
```
|
||||
|
||||
If `$APPLICABLE` is `0`, do NOT proceed silently: ask via AskUserQuestion ("The UI probe found no
|
||||
If the guard above fired (`$APPLICABLE` is `0`, or every element is unclassified — `$UNCLASSIFIED = $APPLICABLE`, #4656), do NOT proceed silently: ask via AskUserQuestion ("The UI probe found no
|
||||
applicable state considerations — is this genuinely a state-free surface, or should we revisit the
|
||||
element descriptions?"). Only write an empty section after explicit confirmation.
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ export interface CoverageReport<V extends string = string> {
|
||||
applicable: number;
|
||||
resolved: number;
|
||||
unresolved: number;
|
||||
/** #4656: items in the soft-signal `unclassified` category. Subset of
|
||||
* `applicable`; a spec where every requirement is unclassified reports
|
||||
* `unclassified === applicable`, which is the signal the zero-applicable
|
||||
* guards in spec-phase/ui-phase widen on. */
|
||||
unclassified: number;
|
||||
byVerification: Record<string, number>;
|
||||
};
|
||||
}
|
||||
@@ -281,6 +286,12 @@ export function analyzeCoverage<V extends string>(
|
||||
const unresolved = merged.filter((i) => i.status === 'unresolved').length;
|
||||
const applicable = merged.length;
|
||||
const resolved = applicable - unresolved; // closed set: resolved-status + dismissed
|
||||
// #4656: the unclassified soft-signal rows count toward `applicable` (the
|
||||
// rollup is count-preserving and `resolved = applicable - unresolved` is a
|
||||
// documented identity), so the count is exposed as a SIBLING field — the
|
||||
// zero-applicable guards can then also fire when EVERY requirement is
|
||||
// unclassified, the case the spec-phase/ui-phase docs promise to catch.
|
||||
const unclassified = merged.filter((i) => i.category === 'unclassified').length;
|
||||
const byVerification: Record<string, number> = {};
|
||||
for (const tier of validators.verification) byVerification[tier] = 0;
|
||||
for (const i of merged) {
|
||||
@@ -288,7 +299,7 @@ export function analyzeCoverage<V extends string>(
|
||||
byVerification[i.verification] = (byVerification[i.verification] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
return { items: merged, coverage: { applicable, resolved, unresolved, byVerification } };
|
||||
return { items: merged, coverage: { applicable, resolved, unresolved, unclassified, byVerification } };
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- *
|
||||
|
||||
@@ -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