@am_shork/attest 0.4.3 → 0.6.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.
package/README.md CHANGED
@@ -8,18 +8,26 @@ by a stable ID and continuously detects drift.
8
8
  - **Result** — is every test green? (from the test runner)
9
9
  - **Drift** — do intent and assertions still agree? (static + runtime cross-check)
10
10
 
11
- The killer move against drift: values that change (timeouts, limits) live **once**
12
- in a requirement's `params`, and tests read them from there — so a number is
13
- physically impossible to drift between the spec and the assertion. A param may be
14
- a scalar or an array of scalars, so list-shaped constants (vendor blacklists, id
15
- sets) get the same single source as a lone number.
11
+ The killer move against drift: values a requirement **promises** (timeouts,
12
+ limits, budgets) live **once** in its `params`, and tests read them from there —
13
+ so a number is physically impossible to drift between the spec and the assertion.
14
+ Values that merely tune behaviour stay ordinary constants; nothing is owed to
15
+ anyone when a tuning knob changes. A param may be a scalar or an array of
16
+ scalars, so list-shaped constants (vendor blacklists, id sets) get the same
17
+ single source as a lone number.
16
18
 
17
19
  What that does not buy is a warning when you change the value. `check` runs
18
20
  nothing, so editing a param leaves it at `✓ No issues` — nothing became unbound,
19
21
  nothing became uncovered. The value cannot *diverge* from the assertion, which is
20
22
  the stronger property; noticing that it *moved* is `verify`'s job, and only when
21
- a scenario asserts on the value it read from `params`. Read the param inside the
22
- assertion, not beside it.
23
+ a scenario asserts on the value it read from `params`.
24
+
25
+ Reading the param is necessary and **not sufficient**, which is worth knowing
26
+ before you rely on it: an assertion that recomputes its expectation from the same
27
+ param the code under test just read has no independent term, so both sides move
28
+ together and the test stays green through any edit. Pin the expectation to
29
+ something that does not move with the param — a fixture, a literal in the test,
30
+ or a second independently derived value.
23
31
 
24
32
  ## Prerequisites
25
33
 
@@ -149,7 +157,7 @@ Every diagnostic carries a `code`, and every code has a section in
149
157
  ```
150
158
  ERROR registry-not-static (requirements/upload.reqs.ts:5)
151
159
  Value is not a literal.
152
- → https://gitlab.com/Pseudorca/attest/-/blob/v0.4.3/docs/en/troubleshooting.md#registry-not-static
160
+ → https://gitlab.com/Pseudorca/attest/-/blob/v0.6.0/docs/en/troubleshooting.md#registry-not-static
153
161
  ```
154
162
 
155
163
  The anchor **is** the code, so the link cannot point somewhere the section
package/dist/cli/index.js CHANGED
@@ -11,12 +11,17 @@ import { Command } from 'commander';
11
11
  import chalk from 'chalk';
12
12
  import { resolve } from 'node:path';
13
13
  import { writeAtomic } from '../core/write.js';
14
- import { runCheck, runVerify, runCover, runArchive, runInit, runRender, runRenderCheck, runStatus, DEFAULT_TARGET, TARGET_NAMES, } from '../core/pipeline.js';
14
+ import { runCheck, runVerify, runCover, runArchive, runArchiveApply, runInit, runRender, runRenderCheck, runStatus, DEFAULT_TARGET, TARGET_NAMES, } from '../core/pipeline.js';
15
15
  import { hasError } from '../core/types.js';
16
16
  import { formatCrash, formatIssues, formatScope, summarize, formatCoverage, formatStatus, } from './report.js';
17
17
  import { archiveReport, checkReport, coverReport, errorReport, initReport, renderJson, renderReport, statusReport, verifyReport, } from './json.js';
18
18
  import { packageVersion } from '../core/version.js';
19
19
  const VERSION = packageVersion();
20
+ /** `runArchive` in the shape `--apply` returns, so the two share one call site. */
21
+ const gateOnly = async (dir, change, options) => ({
22
+ issues: await runArchive(dir, change, options),
23
+ written: [],
24
+ });
20
25
  const program = new Command();
21
26
  program
22
27
  .name('attest')
@@ -267,14 +272,27 @@ program
267
272
  .argument('[dir]', 'project root', undefined)
268
273
  .option(JSON_FLAG, JSON_HELP)
269
274
  .option(VITEST_CONFIG_FLAG, VITEST_CONFIG_HELP)
275
+ .option('--apply', 'if the gate passes, merge the change: splice its ADDED requirements into the registry, rename its proposed specs, and move the change folder to archive/')
270
276
  .description('Run the archive gate; merge only if it passes.')
271
277
  .action((change, dir, opts) => runAction('archive', opts, async () => {
272
- const blocking = await runArchive(root(dir), change, { vitestConfig: vitestConfig(opts) });
278
+ const run = opts.apply ? runArchiveApply : gateOnly;
279
+ const { issues: blocking, written } = await run(root(dir), change, {
280
+ vitestConfig: vitestConfig(opts),
281
+ });
273
282
  return {
274
283
  report: archiveReport(VERSION, change, blocking),
275
284
  human: () => {
276
285
  if (blocking.length === 0) {
277
- console.log(chalk.green(`✓ Gate passed: change "${change}" can be archived.`));
286
+ console.log(chalk.green(opts.apply
287
+ ? `✓ Gate passed and merged: change "${change}".`
288
+ : `✓ Gate passed: change "${change}" can be archived.`));
289
+ // Every path the merge touched, because this is the command that
290
+ // edits files the user cannot regenerate and they need to know which
291
+ // ones to read before committing. It is also what a working-tree
292
+ // check would only have approximated, and it works for someone not
293
+ // using version control at all.
294
+ for (const path of written)
295
+ console.log(chalk.dim(` ${path}`));
278
296
  }
279
297
  else {
280
298
  console.log(chalk.red(`✗ Gate failed: change "${change}"`));
@@ -12,6 +12,21 @@ export interface ApplyResult {
12
12
  * than off `delta.added`, so the two can never be scoped differently.
13
13
  */
14
14
  export declare function addedIds(d: RegistryDelta): string[];
15
+ /**
16
+ * The ids a delta **claims**: what it adds, renames to, or modifies.
17
+ *
18
+ * This is how a proposed spec is attributed to a change (design §7). A spec
19
+ * sitting at its merged location carries no change name, so the delta names its
20
+ * specs the only way that cannot drift from them — by the requirements they
21
+ * declare a scenario for. Wider than `addedIds` on purpose: a delta that
22
+ * retunes a param or renames an id may need a scenario of its own, and one that
23
+ * could not be claimed would be a spec the gate never ran.
24
+ *
25
+ * REMOVED ids are absent. A scenario for a requirement the change deletes is
26
+ * not proposed behaviour, and claiming it would put a spec into the gate run
27
+ * whose requirement the same delta has just taken away.
28
+ */
29
+ export declare function claimedIds(d: RegistryDelta): string[];
15
30
  /**
16
31
  * Apply a change delta to a base registry, returning the merged registry plus
17
32
  * any apply-level issues (conflicts, missing targets, invalid results). The
@@ -14,6 +14,27 @@ import { byCodeUnit } from './order.js';
14
14
  export function addedIds(d) {
15
15
  return Object.keys(d.added ?? {});
16
16
  }
17
+ /**
18
+ * The ids a delta **claims**: what it adds, renames to, or modifies.
19
+ *
20
+ * This is how a proposed spec is attributed to a change (design §7). A spec
21
+ * sitting at its merged location carries no change name, so the delta names its
22
+ * specs the only way that cannot drift from them — by the requirements they
23
+ * declare a scenario for. Wider than `addedIds` on purpose: a delta that
24
+ * retunes a param or renames an id may need a scenario of its own, and one that
25
+ * could not be claimed would be a spec the gate never ran.
26
+ *
27
+ * REMOVED ids are absent. A scenario for a requirement the change deletes is
28
+ * not proposed behaviour, and claiming it would put a spec into the gate run
29
+ * whose requirement the same delta has just taken away.
30
+ */
31
+ export function claimedIds(d) {
32
+ return [
33
+ ...Object.keys(d.added ?? {}),
34
+ ...(d.renamed ?? []).map((r) => r.to),
35
+ ...Object.keys(d.modified ?? {}),
36
+ ].sort(byCodeUnit);
37
+ }
17
38
  /**
18
39
  * Apply a change delta to a base registry, returning the merged registry plus
19
40
  * any apply-level issues (conflicts, missing targets, invalid results). The
@@ -132,8 +153,29 @@ function introducedIdIssue(id, code, prefix) {
132
153
  function err(code, message, reqId) {
133
154
  return { level: 'ERROR', code, ...(reqId ? { reqId } : {}), message };
134
155
  }
156
+ /**
157
+ * The first schema failure, as `path: message`.
158
+ *
159
+ * The path is the half that was being dropped. A requirement carries a record —
160
+ * `params` — so "invalid" on its own does not say *which* param, and the value
161
+ * that most often fails is one the author has to find by reading the schema's
162
+ * `.d.ts`. `RegistryValidationError` has spelled a path `a.b.c` since the
163
+ * registry shipped, and `troubleshooting.md` has been quoting `rationale:
164
+ * Required` for `add-invalid` the whole time — a form this function could not
165
+ * produce. So this is the spelling matching what is already documented, not a
166
+ * new one.
167
+ *
168
+ * Omitted rather than rendered as `(root)` when the path is empty, because the
169
+ * one caller that gets an empty path is `introducedIdIssue`, whose prefix has
170
+ * already named the thing: `Added requirement "auth-7" is invalid: id must look
171
+ * like AUTH-3` reads correctly and `(root): id must look like AUTH-3` does not.
172
+ */
135
173
  function firstMessage(error) {
136
- return error.issues[0]?.message ?? 'unknown error';
174
+ const first = error.issues[0];
175
+ if (!first)
176
+ return 'unknown error';
177
+ const path = first.path.map(String).join('.');
178
+ return path ? `${path}: ${first.message}` : first.message;
137
179
  }
138
180
  /** Content equality via canonical JSON (params key order does not matter). */
139
181
  function sameRequirement(a, b) {
@@ -0,0 +1,40 @@
1
+ import type { Issue } from './types.js';
2
+ /**
3
+ * The range the readers are written against.
4
+ *
5
+ * Stated here rather than read from the manifest at runtime, because the
6
+ * manifest is not on disk beside a bundled build and a diagnostic that cannot
7
+ * name the range is most of the diagnostic gone. `ATX-56` keeps the two honest:
8
+ * its `params` carry this string, and its scenario compares them with
9
+ * `package.json`, so the constant cannot drift from the dependency it describes.
10
+ */
11
+ export declare const SUPPORTED_TYPESCRIPT = "^5.5.0 || ^6.0.0";
12
+ /**
13
+ * The part of the compiler surface every AST reader in this project touches.
14
+ *
15
+ * Deliberately the two members that are reached *first* rather than an
16
+ * exhaustive list: `createSourceFile` and `ScriptTarget` are what
17
+ * `parser.ts`, `static-registry.ts` and `splice.ts` all open with, so a compiler
18
+ * that has them has the shape this check is about, and one that lacks either
19
+ * cannot get far enough for a longer list to matter.
20
+ */
21
+ interface CompilerSurface {
22
+ version?: unknown;
23
+ createSourceFile?: unknown;
24
+ ScriptTarget?: unknown;
25
+ }
26
+ /**
27
+ * An ERROR when the resolved compiler has no AST API, or `undefined` when it
28
+ * does.
29
+ *
30
+ * Takes the compiler rather than reaching for the import directly, so the
31
+ * refusal can be attested against a stand-in: the condition only exists on a
32
+ * compiler this repository cannot install alongside the one it builds with, and
33
+ * a check nothing can make fail is the shape `never-red` exists to catch.
34
+ *
35
+ * No `reqId` and no `file`: the finding is about the toolchain, and every
36
+ * requirement and every file in the project is equally unreadable because of it.
37
+ */
38
+ export declare function compilerIssue(compiler?: CompilerSurface): Issue | undefined;
39
+ export {};
40
+ //# sourceMappingURL=compiler.d.ts.map
@@ -0,0 +1,64 @@
1
+ // Whether the resolved TypeScript can be read from at all (design §5.1).
2
+ //
3
+ // Both registry readers and the spec parser walk the AST with the compiler API,
4
+ // which makes `typescript` the one dependency this tool cannot survive the
5
+ // absence of — and "absent" is not the shape it arrives in. `typescript@7` is
6
+ // the native port: its package resolves, imports cleanly, and answers `version`
7
+ // and `versionMajorMinor`, while `createSourceFile` and every `ts.isX` guard
8
+ // moved behind `typescript/unstable/ast*`. So the members are `undefined` rather
9
+ // than missing, and the first thing to touch one throws.
10
+ //
11
+ // Measured: `attest check` on it died with
12
+ // `TypeError: Cannot read properties of undefined (reading 'Latest')` and a raw
13
+ // stack — no issue code, no fix hint, and nothing saying which compiler it
14
+ // wanted, on the crash path `[0.4.2]` had just finished sanitising.
15
+ //
16
+ // **The declared range excludes 7, and that is not the same as being safe from
17
+ // it.** A caret is a request, not a constraint the adopter cannot override:
18
+ // `pnpm.overrides` and Yarn `resolutions` both pin a transitive dependency
19
+ // across a whole tree, and a repo standardising on one compiler is exactly the
20
+ // repo that uses them — this one has an `overrides` block of its own. So the
21
+ // reachable case is a deliberate, reasonable act by an adopter, answered with a
22
+ // stack trace.
23
+ //
24
+ // What this cannot do is make Attest work on TypeScript 7. That is a migration
25
+ // to `typescript/unstable/ast`, against an API whose own name says not to depend
26
+ // on it yet. The honest answer for now is that the supported range has a ceiling
27
+ // and says so.
28
+ import ts from 'typescript';
29
+ /**
30
+ * The range the readers are written against.
31
+ *
32
+ * Stated here rather than read from the manifest at runtime, because the
33
+ * manifest is not on disk beside a bundled build and a diagnostic that cannot
34
+ * name the range is most of the diagnostic gone. `ATX-56` keeps the two honest:
35
+ * its `params` carry this string, and its scenario compares them with
36
+ * `package.json`, so the constant cannot drift from the dependency it describes.
37
+ */
38
+ export const SUPPORTED_TYPESCRIPT = '^5.5.0 || ^6.0.0';
39
+ /**
40
+ * An ERROR when the resolved compiler has no AST API, or `undefined` when it
41
+ * does.
42
+ *
43
+ * Takes the compiler rather than reaching for the import directly, so the
44
+ * refusal can be attested against a stand-in: the condition only exists on a
45
+ * compiler this repository cannot install alongside the one it builds with, and
46
+ * a check nothing can make fail is the shape `never-red` exists to catch.
47
+ *
48
+ * No `reqId` and no `file`: the finding is about the toolchain, and every
49
+ * requirement and every file in the project is equally unreadable because of it.
50
+ */
51
+ export function compilerIssue(compiler = ts) {
52
+ if (typeof compiler.createSourceFile === 'function' && compiler.ScriptTarget !== undefined) {
53
+ return undefined;
54
+ }
55
+ const version = typeof compiler.version === 'string' ? compiler.version : 'unknown';
56
+ return {
57
+ level: 'ERROR',
58
+ code: 'compiler-unsupported',
59
+ message: `The resolved TypeScript (${version}) does not expose the AST API Attest reads registries and specs with. ` +
60
+ `Attest supports ${SUPPORTED_TYPESCRIPT}. ` +
61
+ `If a resolution or override pins the compiler for the whole tree, exclude Attest from it or move that pin back into the supported range.`,
62
+ };
63
+ }
64
+ //# sourceMappingURL=compiler.js.map
@@ -8,7 +8,7 @@
8
8
  * and the `##` headings of both language documents, so landing here cannot
9
9
  * produce a dead link.
10
10
  */
11
- export declare const ISSUE_CODES: readonly ["add-conflict", "add-invalid", "change-not-found", "declared-not-run", "duplicate-prefix", "duplicate-requirement", "empty-spec", "internal-error", "invalid-change-name", "missing-spec-doc", "modify-invalid", "modify-missing", "never-red", "orphan-test", "possible-drift", "rationale-placeholder", "registry-invalid", "registry-no-default", "registry-not-static", "rename-source-missing", "rename-target-exists", "rename-target-invalid", "stale-spec-doc", "tests-red", "unbound-param", "uncovered-requirement", "unknown-target"];
11
+ export declare const ISSUE_CODES: readonly ["add-conflict", "add-invalid", "added-id-unmerged", "apply-no-prefix-owner", "apply-unsupported-delta", "change-not-found", "compiler-unsupported", "declared-not-run", "duplicate-prefix", "duplicate-requirement", "empty-spec", "internal-error", "invalid-change-name", "missing-spec-doc", "modify-invalid", "modify-missing", "never-red", "orphan-test", "possible-drift", "proposed-spec-name-taken", "proposed-spec-unclaimed", "rationale-placeholder", "registry-invalid", "registry-no-default", "registry-not-static", "rename-source-missing", "rename-target-exists", "rename-target-invalid", "spec-in-change-dir", "stale-spec-doc", "tests-red", "unbound-param", "uncovered-requirement", "unknown-target"];
12
12
  export type IssueCode = (typeof ISSUE_CODES)[number];
13
13
  /**
14
14
  * The page explaining `code`, or `undefined` when nothing explains it.
package/dist/core/docs.js CHANGED
@@ -21,7 +21,11 @@ import { packageVersion } from './version.js';
21
21
  export const ISSUE_CODES = [
22
22
  'add-conflict',
23
23
  'add-invalid',
24
+ 'added-id-unmerged',
25
+ 'apply-no-prefix-owner',
26
+ 'apply-unsupported-delta',
24
27
  'change-not-found',
28
+ 'compiler-unsupported',
25
29
  'declared-not-run',
26
30
  'duplicate-prefix',
27
31
  'duplicate-requirement',
@@ -34,6 +38,8 @@ export const ISSUE_CODES = [
34
38
  'never-red',
35
39
  'orphan-test',
36
40
  'possible-drift',
41
+ 'proposed-spec-name-taken',
42
+ 'proposed-spec-unclaimed',
37
43
  'rationale-placeholder',
38
44
  'registry-invalid',
39
45
  'registry-no-default',
@@ -41,6 +47,7 @@ export const ISSUE_CODES = [
41
47
  'rename-source-missing',
42
48
  'rename-target-exists',
43
49
  'rename-target-invalid',
50
+ 'spec-in-change-dir',
44
51
  'stale-spec-doc',
45
52
  'tests-red',
46
53
  'unbound-param',
@@ -9,6 +9,15 @@ export interface GateInputs {
9
9
  run: RunResult;
10
10
  /** Ids this change ADDs — the scenarios that carry a first-red obligation. */
11
11
  addedIds?: readonly string[];
12
+ /**
13
+ * Of those, the ids the registry **on disk** does not have yet.
14
+ *
15
+ * Computed against the base registry rather than the applied one, because
16
+ * that is the registry the child run imports: the gate applies the delta in
17
+ * memory, and the suite is a separate process reading `*.reqs.ts` off the
18
+ * filesystem. The two disagreeing is the whole content of `added-id-unmerged`.
19
+ */
20
+ unmergedAddedIds?: readonly string[];
12
21
  /** First observed outcome per scenario, already merged with this run. */
13
22
  firstRun?: RedRecord;
14
23
  }
@@ -21,6 +30,32 @@ export interface GateInputs {
21
30
  * this framework is that agreement should be structural, not clerical.
22
31
  */
23
32
  export declare function declaredNotRunIssues(plan: AttestPlan, run: RunResult): Issue[];
33
+ /**
34
+ * A spec file that failed to load, while the change adds ids the registry on
35
+ * disk does not have yet (design §8).
36
+ *
37
+ * The gate applies the delta in memory and hands the result to itself; the
38
+ * suite is a child process that imports `*.reqs.ts` from the filesystem. So
39
+ * implementation code doing exactly what the workflow requires —
40
+ * `reqs['FOG-4'].params.modestRisk` for an id the change *adds* — throws at
41
+ * import, every spec file transitively importing that module fails to load, and
42
+ * what the gate could see was `tests-red` plus a `declared-not-run` per
43
+ * scenario, whose message sends the reader to look for a `skip` or an `.only`
44
+ * that is not there. The change could not be made green by the documented
45
+ * workflow, and the verdict pointed away from the reason.
46
+ *
47
+ * This names the reason instead. It is a diagnosis, not a fix: the run still
48
+ * fails, and the way forward is still to merge the added requirement into the
49
+ * registry before running the gate. What it buys is that the reader is told
50
+ * that, rather than sent to audit their own spec files for a skip.
51
+ *
52
+ * The conjunction is the whole test, and it is deliberately a heuristic: a
53
+ * module can fail to import for reasons that have nothing to do with a
54
+ * requirement id. Naming an unmerged id when one exists is more useful than
55
+ * silence, so the message states the two facts and the inference between them
56
+ * rather than asserting a cause.
57
+ */
58
+ export declare function unmergedIdIssues(run: RunResult, unmergedAddedIds: readonly string[]): Issue[];
24
59
  /**
25
60
  * Never-red: a scenario attesting a requirement this change ADDs, whose first
26
61
  * observed run did not fail (design §6, mechanism 2).
@@ -48,10 +83,12 @@ export declare function neverRedIssues(plan: AttestPlan, addedIds: readonly stri
48
83
  * the registry+plan here are already the applied result, so one pass
49
84
  * validates the end state.
50
85
  * 2. Executable: all tests green.
51
- * 3. Static coverage vs runtime coverage: every declared scenario actually
86
+ * 3. A spec file that failed to load while the change adds an id the registry
87
+ * on disk lacks — the reason, named before the absences it causes.
88
+ * 4. Static coverage vs runtime coverage: every declared scenario actually
52
89
  * ran (catches skip/only false coverage).
53
- * 4. Never-red: every scenario of an ADDED requirement failed on its first
90
+ * 5. Never-red: every scenario of an ADDED requirement failed on its first
54
91
  * recorded run (design §6, mechanism 2).
55
92
  */
56
- export declare function evaluateGate({ registry, plan, run, addedIds, firstRun }: GateInputs): Issue[];
93
+ export declare function evaluateGate({ registry, plan, run, addedIds, unmergedAddedIds, firstRun, }: GateInputs): Issue[];
57
94
  //# sourceMappingURL=gate.d.ts.map
package/dist/core/gate.js CHANGED
@@ -29,6 +29,43 @@ export function declaredNotRunIssues(plan, run) {
29
29
  }
30
30
  return issues;
31
31
  }
32
+ /**
33
+ * A spec file that failed to load, while the change adds ids the registry on
34
+ * disk does not have yet (design §8).
35
+ *
36
+ * The gate applies the delta in memory and hands the result to itself; the
37
+ * suite is a child process that imports `*.reqs.ts` from the filesystem. So
38
+ * implementation code doing exactly what the workflow requires —
39
+ * `reqs['FOG-4'].params.modestRisk` for an id the change *adds* — throws at
40
+ * import, every spec file transitively importing that module fails to load, and
41
+ * what the gate could see was `tests-red` plus a `declared-not-run` per
42
+ * scenario, whose message sends the reader to look for a `skip` or an `.only`
43
+ * that is not there. The change could not be made green by the documented
44
+ * workflow, and the verdict pointed away from the reason.
45
+ *
46
+ * This names the reason instead. It is a diagnosis, not a fix: the run still
47
+ * fails, and the way forward is still to merge the added requirement into the
48
+ * registry before running the gate. What it buys is that the reader is told
49
+ * that, rather than sent to audit their own spec files for a skip.
50
+ *
51
+ * The conjunction is the whole test, and it is deliberately a heuristic: a
52
+ * module can fail to import for reasons that have nothing to do with a
53
+ * requirement id. Naming an unmerged id when one exists is more useful than
54
+ * silence, so the message states the two facts and the inference between them
55
+ * rather than asserting a cause.
56
+ */
57
+ export function unmergedIdIssues(run, unmergedAddedIds) {
58
+ if (unmergedAddedIds.length === 0)
59
+ return [];
60
+ return run.unloadedFiles.map((file) => ({
61
+ level: 'ERROR',
62
+ code: 'added-id-unmerged',
63
+ file,
64
+ message: `${file} failed to load, and this change adds ${unmergedAddedIds.join(', ')}, which the registry on disk does not have yet. ` +
65
+ `The suite runs against the registry files, not the applied registry the gate computed, so code reading a requirement this change adds throws at import. ` +
66
+ `Merge the added requirement into the registry and run the gate again: applying a delta whose ADDED entry already exists with identical content is a no-op, so the change still documents the intent.`,
67
+ }));
68
+ }
32
69
  /**
33
70
  * Never-red: a scenario attesting a requirement this change ADDs, whose first
34
71
  * observed run did not fail (design §6, mechanism 2).
@@ -77,12 +114,14 @@ export function neverRedIssues(plan, addedIds, firstRun) {
77
114
  * the registry+plan here are already the applied result, so one pass
78
115
  * validates the end state.
79
116
  * 2. Executable: all tests green.
80
- * 3. Static coverage vs runtime coverage: every declared scenario actually
117
+ * 3. A spec file that failed to load while the change adds an id the registry
118
+ * on disk lacks — the reason, named before the absences it causes.
119
+ * 4. Static coverage vs runtime coverage: every declared scenario actually
81
120
  * ran (catches skip/only false coverage).
82
- * 4. Never-red: every scenario of an ADDED requirement failed on its first
121
+ * 5. Never-red: every scenario of an ADDED requirement failed on its first
83
122
  * recorded run (design §6, mechanism 2).
84
123
  */
85
- export function evaluateGate({ registry, plan, run, addedIds, firstRun }) {
124
+ export function evaluateGate({ registry, plan, run, addedIds, unmergedAddedIds, firstRun, }) {
86
125
  const blocking = [];
87
126
  // 1) Structure (end-state re-validation).
88
127
  blocking.push(...validateStructure(registry, plan).filter((i) => i.level === 'ERROR'));
@@ -94,9 +133,20 @@ export function evaluateGate({ registry, plan, run, addedIds, firstRun }) {
94
133
  message: 'Some tests are failing; the change cannot be archived.',
95
134
  });
96
135
  }
97
- // 3) Declared-not-run: static coverage claimed, runtime never executed it.
98
- blocking.push(...declaredNotRunIssues(plan, run));
99
- // 4) Never-red: the added scenarios have to have discriminated once.
136
+ // 3) A file that failed to load, named before the absences it produces.
137
+ const unmerged = unmergedIdIssues(run, unmergedAddedIds ?? []);
138
+ blocking.push(...unmerged);
139
+ // 4) Declared-not-run: static coverage claimed, runtime never executed it.
140
+ //
141
+ // Scenarios in a file diagnosed just above are left out. Not because they
142
+ // ran — they could not have — but because "declared but never executed
143
+ // (skipped, or excluded by an .only?)" is one fact restated as a guess about
144
+ // a cause the line above has already established. Only when that line was
145
+ // emitted: with no diagnosis to replace it, a wrong message still beats
146
+ // silence, which is why `verify` keeps reporting them.
147
+ const diagnosed = new Set(unmerged.map((i) => i.file));
148
+ blocking.push(...declaredNotRunIssues(plan, run).filter((i) => !diagnosed.has(i.file)));
149
+ // 5) Never-red: the added scenarios have to have discriminated once.
100
150
  if (addedIds && addedIds.length > 0) {
101
151
  blocking.push(...neverRedIssues(plan, addedIds, firstRun ?? {}));
102
152
  }
@@ -1,6 +1,24 @@
1
1
  import type { Loader } from './loader.js';
2
2
  import type { AttestPlan, Issue, Registry } from './types.js';
3
3
  export declare const isReqsFile: (name: string) => boolean;
4
+ /**
5
+ * A spec belonging to a change that has not been agreed yet (design §7).
6
+ *
7
+ * The marker is in the **name** rather than the directory because the file
8
+ * already sits where it will live once the change is merged: a proposal's spec
9
+ * is written next to the code it attests, so its relative imports resolve
10
+ * identically before and after the merge, and merging renames it in place
11
+ * instead of moving it up a tree and rewriting every specifier.
12
+ */
13
+ export declare const isProposedSpecFile: (name: string) => boolean;
14
+ /**
15
+ * A spec belonging to the merged suite.
16
+ *
17
+ * Proposed specs are excluded here rather than at each call site, so the base
18
+ * scan, the base run and every reporting command are wrong together or not at
19
+ * all: a red scenario for behaviour nobody has implemented must not reach any
20
+ * of them until its gate passes.
21
+ */
4
22
  export declare const isSpecFile: (name: string) => boolean;
5
23
  /**
6
24
  * Recursively find files under root whose basename matches `match`.
@@ -11,16 +29,21 @@ export declare const isSpecFile: (name: string) => boolean;
11
29
  * order never depends on which `readdir` happened to resolve first.
12
30
  */
13
31
  export declare function findFiles(root: string, match: (name: string) => boolean): Promise<string[]>;
14
- /** The two file sets every command needs, collected in one pass. */
32
+ /** The file sets every command needs, collected in one pass. */
15
33
  export interface ProjectScan {
16
34
  reqsFiles: string[];
17
35
  specFiles: string[];
36
+ /** Specs of changes still under review; never part of the base suite. */
37
+ proposedSpecFiles: string[];
18
38
  }
19
39
  /**
20
40
  * Find the registry and spec files under root in a **single** traversal.
21
41
  *
22
42
  * Calling findFiles once per pattern meant `check` walked the tree twice and
23
- * `archive` four times, over a tree that cannot change in between.
43
+ * `archive` four times, over a tree that cannot change in between. Proposed
44
+ * specs are collected in the same pass for that reason and kept in their own
45
+ * list: they are found everywhere the merged ones are, and no caller may reach
46
+ * them by accident.
24
47
  */
25
48
  export declare function scanProject(root: string): Promise<ProjectScan>;
26
49
  /** One file's worth of registry, or the single issue that stopped it. */
@@ -75,11 +98,28 @@ export declare function staticReader(): RegistryReader;
75
98
  * prefix a duplicate-prefix ERROR.
76
99
  *
77
100
  * Pass `files` to reuse a {@link scanProject} result instead of re-walking.
101
+ *
102
+ * `prefixOwners` comes back with the registry because the rule deciding it —
103
+ * first claim wins, in sorted file order — must have exactly one spelling. It is
104
+ * what `duplicate-prefix` is computed from here, and what tells `--apply` which
105
+ * file an ADDED id belongs in (design §7); a second walk arriving at its own
106
+ * answer would be a second rule the moment either was edited.
78
107
  */
79
108
  export declare function loadRegistry(root: string, reader: RegistryReader, files?: string[]): Promise<{
80
109
  registry: Registry;
81
110
  issues: Issue[];
111
+ prefixOwners: Record<string, string>;
82
112
  }>;
113
+ /**
114
+ * The part of an id that names the space it lives in — `AUTH` of `AUTH-3`.
115
+ *
116
+ * The prefix is the only unit above the requirement (design §11) and two things
117
+ * now depend on agreeing about it: `duplicate-prefix`, and which file `--apply`
118
+ * writes an ADDED requirement into. An id with no dash is its own prefix, which
119
+ * cannot arise from `RequirementIdSchema` and is handled anyway because this
120
+ * also runs over ids a delta proposed.
121
+ */
122
+ export declare function idPrefix(id: string): string;
83
123
  /**
84
124
  * Parse the given spec files into one merged plan (paths shown relative to
85
125
  * `displayRoot`). Files are read concurrently; the merge follows the input
@@ -92,8 +132,26 @@ export declare function loadRegistry(root: string, reader: RegistryReader, files
92
132
  export declare function parseSpecs(files: string[], displayRoot: string): Promise<AttestPlan>;
93
133
  /** Parse every `*.spec.ts` under root into one merged plan (file paths shown relative to root). */
94
134
  export declare function parseAllSpecFiles(root: string): Promise<AttestPlan>;
95
- /** Parse the spec files belonging to a single proposed change (design §8). */
96
- export declare function parseChangeSpecs(root: string, changeName: string): Promise<AttestPlan>;
135
+ /**
136
+ * Spec-shaped files sitting under `root/changes` the location a change's
137
+ * specs used to live at, and which nothing walks any more (design §7).
138
+ *
139
+ * Two correct decisions compose into a blind spot. `changes` is in `SKIP_DIRS`,
140
+ * so `scanProject` cannot reach these; and the explicit include `attest archive`
141
+ * once applied to `changes/<name>/specs/` went away with `changeExcludeGlobs`
142
+ * when specs moved to their merged location. A spec left here therefore executes
143
+ * in no suite and no gate, and the only command that says anything is the gate,
144
+ * blaming coverage for a file it cannot see.
145
+ *
146
+ * Walks through `findFiles` rather than a second walker: `SKIP_DIRS` is consulted
147
+ * for *sub*directories only, so starting the walk at `changes` itself both
148
+ * reaches these files and keeps skipping `node_modules` beneath them.
149
+ *
150
+ * Both spellings are wrong here and both are returned. A `*.spec.ts` is the
151
+ * pre-move layout; a `*.proposed.spec.ts` is the right marker at the wrong path,
152
+ * which is the half-done migration and no more visible than the other.
153
+ */
154
+ export declare function findChangeDirSpecs(root: string): Promise<string[]>;
97
155
  /** List the names of proposed changes under `root/changes`. */
98
156
  export declare function listChangeNames(root: string): Promise<string[]>;
99
157
  //# sourceMappingURL=locate.d.ts.map