@am_shork/attest 0.7.4 → 0.9.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.
@@ -49,7 +49,7 @@ export function statusRows(addedIds, plan, firstRun) {
49
49
  name: s.name,
50
50
  file: s.file,
51
51
  line: s.line,
52
- firstRun: recordedOutcome(firstRun, reqId, s.name) ?? null,
52
+ firstRun: recordedOutcome(firstRun, { reqId, file: s.file, name: s.name }) ?? null,
53
53
  }));
54
54
  return { reqId, state: obligationState(reqId, scenarios, firstRun), scenarios };
55
55
  });
@@ -59,7 +59,7 @@ function obligationState(reqId, scenarios, firstRun) {
59
59
  return 'no-scenario';
60
60
  // Every scenario, not any: the gate raises never-red per scenario, so one
61
61
  // proven scenario beside an unproven one is still a blocked change.
62
- return scenarios.every((s) => hasRecordedRed(firstRun, reqId, s.name))
62
+ return scenarios.every((s) => hasRecordedRed(firstRun, { reqId, file: s.file, name: s.name }))
63
63
  ? 'proven'
64
64
  : 'unproven';
65
65
  }
@@ -12,8 +12,11 @@
12
12
  // write to the terminal of whoever is checking it. On a fork MR the author is
13
13
  // not the reviewer, and the payload is a scenario name or an exception: erase
14
14
  // the lines above, repaint a red verdict green, rewrite the window title. The
15
- // `--json` path was never exposed `JSON.stringify` escapes every C0
16
- // character which is precisely why this has to hold on the side people read.
15
+ // `--json` path was recorded here as never exposed, on the grounds that
16
+ // `JSON.stringify` escapes every C0 character true, and narrower than the
17
+ // class below, which also holds DEL and C1. `cli/json.ts` now applies the same
18
+ // rule at the point that document is serialised (ATX-74); this still has to
19
+ // hold independently, because a stream has no such point.
17
20
  //
18
21
  // **In `core/` rather than in the CLI**, though the CLI is its main caller: the
19
22
  // loader has to sanitise Vite's log output for the same reason and cannot
@@ -1,11 +1,22 @@
1
1
  export type { Requirement, Registry, ParamValue } from './schema.js';
2
2
  export type { IssueCode } from './docs.js';
3
3
  import type { IssueCode } from './docs.js';
4
- /** A scenario extracted statically from a spec file (design §5.2). */
5
- export interface ParsedScenario {
4
+ /**
5
+ * What identifies one scenario, everywhere the engine compares one (design §5.4).
6
+ *
7
+ * All three, and as a bundle rather than three positional strings: the file was
8
+ * missing from the key that `declared-not-run` and the first-run record are
9
+ * decided by, and adding it as a fourth `string` argument beside `reqId` and
10
+ * `name` would have made two adjacent parameters of one type that a caller can
11
+ * silently transpose. The compiler cannot catch that; a field name can.
12
+ */
13
+ export interface ScenarioRef {
6
14
  reqId: string;
7
- name: string;
8
15
  file: string;
16
+ name: string;
17
+ }
18
+ /** A scenario extracted statically from a spec file (design §5.2). */
19
+ export interface ParsedScenario extends ScenarioRef {
9
20
  line: number;
10
21
  }
11
22
  /**
@@ -31,15 +42,33 @@ export type Outcome = 'pass' | 'fail';
31
42
  export interface RunResult {
32
43
  /** True when no test failed. */
33
44
  passed: boolean;
34
- /** reqId -> set of scenario names that actually executed (from the task tree). */
35
- runtimeCoverage: Map<string, Set<string>>;
36
45
  /**
37
- * reqId -> scenario name -> how it ended. The same task-tree walk that fills
38
- * `runtimeCoverage` already had to tell `pass` from `fail` to decide whether a
39
- * scenario executed at all; this keeps that distinction instead of discarding
40
- * it, which is what §6's mechanism 2 records.
46
+ * reqId -> spec file -> the scenario names that actually executed (from the
47
+ * task tree).
48
+ *
49
+ * **The file is part of the key, and its absence was a false green.** Keyed by
50
+ * `(reqId, name)` alone, two spec files declaring the same scenario name under
51
+ * the same `requirement()` are one entry, so whichever of them executed
52
+ * vouched for the other and `declared-not-run` stayed silent about a scenario
53
+ * that never ran — the one report this tool exists to refuse. The static plan
54
+ * carries `file` on every `ParsedScenario` and the task tree carries it on
55
+ * every file task, so only this map ever forgot it.
56
+ *
57
+ * Project-relative POSIX, like `unloadedFiles` and for the same reason: it is
58
+ * compared against a plan built that way (`paths.ts`).
59
+ */
60
+ runtimeCoverage: Map<string, Map<string, Set<string>>>;
61
+ /**
62
+ * reqId -> spec file -> scenario name -> how it ended. The same task-tree walk
63
+ * that fills `runtimeCoverage` already had to tell `pass` from `fail` to
64
+ * decide whether a scenario executed at all; this keeps that distinction
65
+ * instead of discarding it, which is what §6's mechanism 2 records.
66
+ *
67
+ * Keyed identically, and for the identical reason: the same collapse reaches
68
+ * `mergeRedRecord`, where a base scenario's recorded `fail` satisfied a
69
+ * same-named proposed scenario's first-red obligation.
41
70
  */
42
- outcomes: Map<string, Map<string, Outcome>>;
71
+ outcomes: Map<string, Map<string, Map<string, Outcome>>>;
43
72
  /**
44
73
  * Spec files that failed to *load*, as project-relative POSIX paths.
45
74
  *
@@ -13,6 +13,7 @@ import type { AttestPlan, Issue, ParamRef, Registry } from './types.js';
13
13
  export declare function uncoveredIssues(registry: Registry, plan: AttestPlan): Issue[];
14
14
  /**
15
15
  * Static structural validation (design §5.3). Emits ERROR-level issues for:
16
+ * - empty-spec: the root declares no requirements at all
16
17
  * - orphan-test: a scenario covers an unknown requirement id
17
18
  * - uncovered-requirement: a requirement has no scenario
18
19
  * - unbound-param: a statement placeholder has no matching param
@@ -28,6 +28,7 @@ export function uncoveredIssues(registry, plan) {
28
28
  }
29
29
  /**
30
30
  * Static structural validation (design §5.3). Emits ERROR-level issues for:
31
+ * - empty-spec: the root declares no requirements at all
31
32
  * - orphan-test: a scenario covers an unknown requirement id
32
33
  * - uncovered-requirement: a requirement has no scenario
33
34
  * - unbound-param: a statement placeholder has no matching param
@@ -47,6 +48,27 @@ export function uncoveredIssues(registry, plan) {
47
48
  export function validateStructure(registry, plan, unreadable = []) {
48
49
  const issues = [];
49
50
  const knownIds = new Set(Object.keys(registry));
51
+ // empty-spec: nothing under this root declares intent, so no finding below is
52
+ // about a requirement — and the report has no line saying why. It is stated
53
+ // first because everything else this function can emit against an empty
54
+ // registry is fallout from it: every scenario in the project is an orphan, and
55
+ // the diagnosis after 135 of them is a diagnosis nobody reaches.
56
+ //
57
+ // Guarded on `unreadable`, which is the same withdrawal the orphan advice
58
+ // makes below and for the same reason. "Point attest at the directory holding
59
+ // your *.reqs.ts files" is right for a root that has none, and **wrong** for
60
+ // one whose registry is sitting right there and failed to load: the files were
61
+ // found, the fix is the load error already in the report, and following the
62
+ // hint would move a working path. A registry that could not be read is not an
63
+ // absent one, and the two take different repairs.
64
+ if (knownIds.size === 0 && unreadable.length === 0) {
65
+ issues.push({
66
+ level: 'ERROR',
67
+ code: 'empty-spec',
68
+ message: `No requirements found under this root, so there is no intent here to attest. ` +
69
+ `Point attest at the directory holding your *.reqs.ts files, or add one.`,
70
+ });
71
+ }
50
72
  // orphan-test: covers a requirement that does not exist.
51
73
  //
52
74
  // "Add it to the registry, or fix the id" is the right advice for an id that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@am_shork/attest",
3
- "version": "0.7.4",
3
+ "version": "0.9.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",