@am_shork/attest 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  // Structural validation + anti-drift heuristic (design §5.3, §6).
2
2
  // Graded reporting: ERROR / WARNING / INFO, each with file + line + fix hint.
3
+ import { byCodeUnit } from './order.js';
3
4
  /**
4
5
  * Uncovered requirements: intent exists but no scenario attests it (design §5.3).
5
6
  *
@@ -35,11 +36,15 @@ export function uncoveredIssues(registry, plan) {
35
36
  * and one WARNING:
36
37
  * - rationale-placeholder: a `{name}` in a rationale, which is never interpolated
37
38
  *
38
- * `registryIncomplete` says that at least one `*.reqs.ts` failed to load, so
39
- * `registry` is known to be missing whatever was in it. It changes no verdict —
40
- * only what `orphan-test` advises, for the reason below.
39
+ * and one more WARNING, which exists only when a registry file failed to load:
40
+ * - orphan-from-failed-registry: the scenarios attesting ids that file declares
41
+ *
42
+ * `unreadable` is the `*.reqs.ts` files that failed to load, each with the ids
43
+ * its source still names (see `loadRegistry`). It changes no verdict — only
44
+ * which findings are stated per scenario and which are stated once, for the
45
+ * reason below.
41
46
  */
42
- export function validateStructure(registry, plan, registryIncomplete = false) {
47
+ export function validateStructure(registry, plan, unreadable = []) {
43
48
  const issues = [];
44
49
  const knownIds = new Set(Object.keys(registry));
45
50
  // orphan-test: covers a requirement that does not exist.
@@ -50,27 +55,75 @@ export function validateStructure(registry, plan, registryIncomplete = false) {
50
55
  // following the hint would add a duplicate. That case is not rare when it
51
56
  // happens — one unreadable `*.reqs.ts` orphans every scenario of every
52
57
  // requirement it declared, so the wrong advice is also the loudest thing in
53
- // the report.
58
+ // the report. Measured on one broken registry of three requirements: six
59
+ // ERRORs, of which one was the cause and five were its shadow.
60
+ //
61
+ // Which of the two a given orphan is *can* be decided here, because
62
+ // `loadRegistry` brings back the ids the broken file's source still names. So
63
+ // the scenarios attesting those ids are not reported one by one: they are
64
+ // fallout from a finding already in the report, and the fallout is stated once
65
+ // per file that caused it. That is the same move `duplicate-prefix` makes —
66
+ // the fact is about the file, not about each requirement that reveals it — and
67
+ // the same order ATX-62 records for `spec-load-failed`: the replacement comes
68
+ // first, the withdrawal follows it, because suppressing with nothing in its
69
+ // place trades a wrong message for silence.
54
70
  //
55
- // Which of the two this is cannot be decided here: the ids of a file that
56
- // never parsed are exactly what is unavailable. So the finding stands and the
57
- // *advice* names the uncertainty and the order to work in a report that is
58
- // quieter about problems it can still see would be the worse trade for a
59
- // command whose contract is breadth.
60
- const orphanFix = (id) => registryIncomplete
61
- ? `A registry file failed to load, so ids it declares are missing here — fix that first, and add "${id}" only if it is still unknown afterwards.`
71
+ // Nothing is lost by collapsing them. The replacement names the file, the
72
+ // count and the ids, so a reader can still see which scenarios are affected;
73
+ // the load failure is already an ERROR, so the verdict cannot move; and the
74
+ // scenarios themselves are not defective they will be verified normally the
75
+ // moment the registry loads.
76
+ const swallowed = new Map(unreadable.map((u) => [u.file, []]));
77
+ const ownerOf = new Map();
78
+ for (const u of unreadable)
79
+ for (const id of u.ids)
80
+ ownerOf.set(id, u.file);
81
+ // An unreadable file whose ids could not be recovered leaves the question open
82
+ // for every orphan that is not claimed by another one, and the hedged advice
83
+ // is what an open question sounds like. When every unreadable file gave up its
84
+ // ids, an orphan none of them claims is genuinely unknown — and gets the plain
85
+ // advice it deserves, which the hedge had been withdrawing from correct
86
+ // findings too.
87
+ const someFileOpaque = unreadable.some((u) => u.ids.length === 0);
88
+ const orphanFix = (id) => someFileOpaque
89
+ ? `A registry file failed to load and its ids could not be read, so "${id}" may be one of them — fix that first, and add it only if it is still unknown afterwards.`
62
90
  : `Add it to the registry, or fix the id.`;
63
91
  for (const s of plan.scenarios) {
64
- if (!knownIds.has(s.reqId)) {
65
- issues.push({
66
- level: 'ERROR',
67
- code: 'orphan-test',
68
- reqId: s.reqId,
69
- file: s.file,
70
- line: s.line,
71
- message: `scenario "${s.name}" attests unknown requirement "${s.reqId}". ${orphanFix(s.reqId)}`,
72
- });
92
+ if (knownIds.has(s.reqId))
93
+ continue;
94
+ const owner = ownerOf.get(s.reqId);
95
+ if (owner !== undefined) {
96
+ swallowed.get(owner).push(s.reqId);
97
+ continue;
73
98
  }
99
+ issues.push({
100
+ level: 'ERROR',
101
+ code: 'orphan-test',
102
+ reqId: s.reqId,
103
+ file: s.file,
104
+ line: s.line,
105
+ message: `scenario "${s.name}" attests unknown requirement "${s.reqId}". ${orphanFix(s.reqId)}`,
106
+ });
107
+ }
108
+ // WARNING, not ERROR: the defect is the load failure, which is an ERROR of its
109
+ // own and always present when this is. Reporting it a second time at the same
110
+ // level would say a broken registry is two problems.
111
+ //
112
+ // No `reqId`: the finding is about a file and the set of ids it took down with
113
+ // it, and no single requirement is implicated — the same reason
114
+ // `duplicate-prefix` and `spec-load-failed` carry none.
115
+ for (const u of unreadable) {
116
+ const ids = swallowed.get(u.file);
117
+ if (ids.length === 0)
118
+ continue;
119
+ const unique = [...new Set(ids)].sort(byCodeUnit);
120
+ issues.push({
121
+ level: 'WARNING',
122
+ code: 'orphan-from-failed-registry',
123
+ file: u.file,
124
+ message: `${ids.length} ${ids.length === 1 ? 'scenario attests' : 'scenarios attest'} ids ${u.file} declares (${unique.join(', ')}), and it failed to load — so those ids are missing from this run. ` +
125
+ `They are not orphans: fix the load failure reported above and they will be verified as usual.`,
126
+ });
74
127
  }
75
128
  // uncovered-requirement: intent exists but no scenario attests it. Shared
76
129
  // with `cover`, so the two commands cannot drift apart.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@am_shork/attest",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "TDD-native spec framework: tests are the source of truth for verification, ID-bound requirements the source of truth for intent.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -48,7 +48,7 @@
48
48
  "chalk": "^5.3.0",
49
49
  "commander": "^12.1.0",
50
50
  "typescript": "^5.5.0 || ^6.0.0",
51
- "zod": "^3.23.0"
51
+ "zod": "^4.4.3"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "vite": "^8.0.0",