@am_shork/attest 0.9.1 → 0.9.2

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/CHANGELOG.md CHANGED
@@ -13,6 +13,71 @@ input, removes/renames a public API or `--json` field, or changes a default
13
13
  runtime behavior an existing invocation relies on — diagnostic message text is
14
14
  not API.
15
15
 
16
+ ## [0.9.2] - 2026-08-17
17
+
18
+ ### Added
19
+
20
+ - **`@am_shork/attest/inspect`: read what a project declares, without executing
21
+ it and without acquiring the runner peer.** One function,
22
+ `inspectProject(root)`, answering the requirements a project declares, the
23
+ scenarios declared for each, and the issues that stopped a file being read.
24
+ Nothing about the quality of any of it — that is the ground truth this project
25
+ refuses to compute for someone else's requirements (design §0).
26
+
27
+ *Why it exists rather than what it does.* Everything this repository knows
28
+ about the quality of its own intent layer is a hand-judged table in
29
+ `tests/intent-rule-candidates.spec.ts`, and the check that keeps such a table
30
+ from rotting is that a scenario a row names is one a spec really declares. An
31
+ adopter could not write that check: the package exported the authoring API and
32
+ nothing else, so `parseSpecs` and the static registry read were internal and
33
+ **the scenario names of their own project were unreachable** — not
34
+ undocumented, unreachable. This is the enabling half of the judgement-table
35
+ proposal under *Under consideration*, and deliberately only that half: it
36
+ ships no schema and decides nothing, and it is the only thing that can produce
37
+ the second table that proposal is waiting for.
38
+
39
+ *One function, not the four internals it composes,* because `loadRegistry`
40
+ takes a reader and which of the two runs is a security property fixed by the
41
+ command, never chosen by a caller (design §5.1). The static reader is welded
42
+ in and no parameter can change it.
43
+
44
+ *And its import closure names neither runner peer,* so a consumer that only
45
+ analyses acquires neither `vite` nor `vitest` — the same argument `./define`
46
+ was added on (design §10). Measured on the built entry: ten modules, and the
47
+ only bare specifiers in the closure are `typescript`, `zod` and two node
48
+ builtins. That claim is gated over the **built** package rather than the
49
+ source, because `locate.ts` names `loader.ts` in a type-only import that the
50
+ emit erases: the two graphs differ by exactly that edge, and it is the edge
51
+ the promise is about. `tests/import-boundary.spec.ts` holds the source side,
52
+ where a type-only mention still counts as *naming* the runner, and its
53
+ reachability walk now follows runtime edges only for the same reason.
54
+
55
+ Additive: no command, flag, exit code or `--json` field changes, and nothing
56
+ that used to pass now fails.
57
+
58
+ - **"Judging your own intent layer" (`docs/{en,zh}/intent-quality.md`): the four
59
+ things no gate is looking at, and a method for building your own
60
+ counter-pressure out of them.** For an existing adopter, on the back of the
61
+ export above.
62
+
63
+ *A method with no template, and the omission is the point.* The page states
64
+ the four **findings** — coverage is binary so lumping is free, a quantifier is
65
+ unread, an iterated `params` list is a run's domain, an assertion can echo
66
+ rather than pin — and the two **mechanical properties** that make a
67
+ hand-judged record a counter-pressure rather than a note: completeness
68
+ (something enumerable must force a row) and rot-binding (a scenario a row
69
+ names must be asserted to exist). Both generalise without assuming a single
70
+ column. What it withholds is this repository's own answer — the four tables
71
+ in `tests/intent-rule-candidates.spec.ts` — and it asks a reader to reach that
72
+ file only after doing the hand pass, saying why rather than hiding it: there
73
+ is one such table in existence and its author wrote both the schema and every
74
+ answer in it, so a second one built from someone else's corpus is evidence
75
+ while one built from ours is an echo. That is the fourth finding turned on the
76
+ question itself.
77
+ The worked example is deliberately **one column**, so the shape that comes
78
+ back is not a copy of the shape that went out, and the report the page asks
79
+ for names the thing worth most: *the column you needed that we do not have.*
80
+
16
81
  ## [0.9.1] - 2026-08-13
17
82
 
18
83
  ### Changed
@@ -5082,7 +5147,21 @@ have to gather again.
5082
5147
  far past the two registry readers' current surface. **(3)** No mechanism is yet
5083
5148
  known that separates "the assertion pins this independently" from "the assertion
5084
5149
  echoes it" without executing the scenario twice under different values — which
5085
- is mutation testing, a much larger thing to own.
5150
+ is mutation testing, a much larger thing to own. *One has since been built and
5151
+ scored, and it does not lift this blocker.* The syntactic candidate — flag a
5152
+ scenario whose expectation is derived from the param the code under test
5153
+ consumed, decided inside one body without executing anything — flags 9 params
5154
+ here, of which **4 are real echoes and 5 are pinned by something outside the
5155
+ body it cannot see**. Under half precision, and the failure is the one
5156
+ predicted above rather than a tuning problem: what pins a value is often a
5157
+ fixture or a second derivation in another file, so a single-body detector is
5158
+ looking in the wrong place by construction. The newest false positive is the
5159
+ clearest instance — `ATX-79.subpath` is compared against `package.json`, which
5160
+ is not the code under test and does not read the registry, so the independent
5161
+ term is in another file entirely. Precision has fallen on every addition
5162
+ measured so far, which is the direction that matters. The score is quoted from the
5163
+ suite's snapshot and re-read with the judgement-table entry above, which is
5164
+ where the same measurement is used for a different decision.
5086
5165
  **The guidance half has shipped; this entry is now only about the detector.**
5087
5166
  "Read the param inside the assertion" was insufficient and half-wrong as
5088
5167
  written, and `[0.5.0]` replaces it everywhere it appeared — README, the
@@ -5378,6 +5457,98 @@ have to gather again.
5378
5457
  and the second report of the same shape, which this measurement does not
5379
5458
  supply and does not replace.
5380
5459
 
5460
+ - **An outward-facing form of the judgement tables, so an adopter can run this
5461
+ counter-pressure on their own registry.**
5462
+ `tests/intent-rule-candidates.spec.ts` maintains four hand-judged tables over
5463
+ this repo's registry and enforces their completeness. Design §11 records the
5464
+ properties they defend and that nothing enforces any of them; what the tables
5465
+ add is a counter-pressure holding for **one** registry whose authors agreed to
5466
+ maintain it. An adopter got none of it, and the reason was sharper
5467
+ than "nobody wrote it for them": the package exported `requirement`,
5468
+ `scenario`, `defineRequirements`, `delta` and types, and nothing else.
5469
+ `parseSpecs`, `findFiles` and `readRegistrySource` were internal, so an
5470
+ adopter **could not enumerate their own scenario names** — the table's central
5471
+ rot check, that a claimed scenario is one a spec really declares, was not
5472
+ merely unwritten downstream but unreachable. **That half shipped under
5473
+ `[0.9.2]` as `@am_shork/attest/inspect`** and is recorded at the foot of
5474
+ this entry; what stays here is the schema built on top of it, and the three
5475
+ blockers below are untouched by the export.
5476
+ *The shape that makes it shippable at all is a split, not a feature.* The
5477
+ engine can compute which ids exist, which statements match the universal
5478
+ quantifier regex, which params are list-valued, and whether a named scenario
5479
+ is declared — all four already, all statically. It cannot compute how many
5480
+ obligations a requirement carries, what a quantifier ranges over, or whether a
5481
+ scenario iterates a param. So the shippable object is **the enforcement, never
5482
+ the judgement**: an opt-in `*.judge.ts` read by the same static reader and
5483
+ under the same literal-only rule the registry is (it is metadata about
5484
+ requirements under review, so `check` must execute nothing to read it), whose
5485
+ contents are entirely the adopter's. Opting in is creating the file; once it
5486
+ exists, `requirement-unjudged`, `orphan-judgement`, `unbound-site` and
5487
+ `quantifier-unjudged` make it complete and keep it from rotting. The
5488
+ asymmetry is the only way this can exist without Attest asserting a ground
5489
+ truth it refuses to compute (§0), and it is the same asymmetry `never-red`
5490
+ already runs on: the archive gate would owe judgement only for ids a change
5491
+ ADDs, so a brownfield adoption is not blocked wholesale.
5492
+ *One part of the existing table cannot ship in any form.* `DOMAINS.pin` comes
5493
+ in two kinds — a scenario name, checkable against the AST, and a **source-text
5494
+ fragment** grepped out of the spec file. The second is asserting on the
5495
+ wording of somebody's test, which is the `Issue.message` category error under
5496
+ another name. Only the scenario-name kind is exportable, and what that costs
5497
+ is real: the grep is what caught a deleted pin that `toContain` had passed.
5498
+ *Why no detector ships with it, and this is the part that is already
5499
+ measured.* Three rule families were built and scored against the whole corpus
5500
+ (86 entries, counting a fixture id once per registry file) before any was
5501
+ written into the engine, which is the order `divergent-param` was originally
5502
+ designed in the wrong way round. Every figure below is a quotation of the
5503
+ suite's inline snapshots; the snapshot is the fresh copy, and this paragraph
5504
+ is only ever as fresh as its last edit.
5505
+ **(1) `compound-requirement`** — 34 requirements carry more than one
5506
+ obligation, it flags 16, is **wrong about none**, and misses 18. Never wrong
5507
+ and nearly blind, and the 18 are the form the coverage incentive actually
5508
+ rewards: extra clauses under a single keyword. **(2) The widened variant** —
5509
+ 29 correct, but 14 wrong and still 5 missed, and it is silenced by deleting an
5510
+ "and". A rule whose cheapest fix degrades the artifact it protects does not
5511
+ ship, and its precision falls as the registry grows. **(3) `divergent-param`**
5512
+ — 5 divergences on this repo, **all 5 judged wrong**, zero true positives ever
5513
+ recorded. The `code` group now spans 19 ids, and the failure is structural
5514
+ rather than a matter of naming: the key is built by discarding the evidence of
5515
+ relatedness, so the one genuinely paired divergence in the group is the one it
5516
+ cannot show, and the key has churned three times under `flag` on additions
5517
+ that changed no verdict. A fourth candidate — the echo detector, scored at
5518
+ half precision — belongs to the tautology entry above and is not re-quoted
5519
+ here, because a figure stated in two places goes stale in one. Taken together
5520
+ these are the measured range of what a token or single-body detector over
5521
+ prose can do here, and they are why the proposal is enforcement-only.
5522
+ *What blocks the decision — three things.* **(1) One adapter.** This
5523
+ repository's table is the only one that exists, and its author wrote both the
5524
+ schema and every answer in it. By this project's own rule a second table is
5525
+ what makes the seam real, and freezing a `Judgement` type now is deciding for
5526
+ everyone from 34 requirements written by one person. **(2) The breaking form
5527
+ of this was already rejected** — see *Countable obligations* below — and one
5528
+ of its three reasons survives the separate-file shape intact: **nothing can
5529
+ force the count to be true.** The other two do not survive, which is what is
5530
+ new here: a separate artifact touches no `--json` field, no delta apply and no
5531
+ `render` output, and completeness *across ids* is computable in a way
5532
+ completeness *within* an obligation array never was. So this is a re-proposal
5533
+ with one of three objections answered and the sharpest one standing.
5534
+ **(3) `quantifier-unjudged` reuses a rejected detector as its trigger.**
5535
+ Firing on the quantifier token was rejected below at 43% reach and 70% wrong;
5536
+ the claim here is that the same regex is acceptable when it asks a question an
5537
+ author discharges in one line (`over: 'values'`) rather than issuing a
5538
+ verdict. That claim is untested on anyone who did not also write the regex.
5539
+ *The half that is not blocked has shipped, under `[0.9.2]`.*
5540
+ `@am_shork/attest/inspect` is one function answering what a project declares —
5541
+ AST-only, no runner peer, landing on the existing static/executing boundary.
5542
+ It ships no schema and decides nothing, which is why it could go ahead of the
5543
+ three blockers rather than waiting on them. What it changes about this entry
5544
+ is the *kind* of thing that is now missing: the mechanism is no longer
5545
+ unreachable, only unwritten, so a second table is something an adopter can
5546
+ build rather than something only this repository could.
5547
+ *What would decide it:* one judgement table maintained by someone who did not
5548
+ design it. If it comes back the same shape, the seam is real and the schema
5549
+ follows; if it comes back different, `inspect` was the whole answer and the
5550
+ schema never needs to exist.
5551
+
5381
5552
  ## Considered and rejected
5382
5553
 
5383
5554
  Decisions **not** to build something, kept where they can be found before the
@@ -5864,24 +6035,27 @@ quotation of that snapshot and is only ever as fresh as the last edit here —
5864
6035
  read the snapshot when the number is what matters. *`compound-requirement`
5865
6036
  (WARNING) — rejected.* More than one RFC-2119 keyword in one `statement` is
5866
6037
  objectively more than one obligation. Measured (figures re-read from the suite,
5867
- which is the only place they are not prose): of the 8 requirements in the corpus
5868
- carrying more than one, it flags 3 (`ATX-10`, `ATX-21`, `ATX-26`) and misses
5869
- `ATX-5`, `ATX-9`, `ATX-13`, `ATX-14`, `ATX-17` every one that packs clauses
5870
- under a single keyword, which is the form the coverage incentive actually
5871
- rewards. A wider variant (keyword plus a coordinating `and`/`while`/`;`) reaches
5872
- 7 of 8, at 3 wrong hits (`ATX-6`, `ATX-22`, `ATX-23`) rather than the 1 recorded
5873
- when it was first scored so reach is not the deciding argument, and the
5874
- precision it trades away grows with the registry. This is: both variants are
6038
+ which is the only place they are not prose): of the 34 requirements in the
6039
+ corpus carrying more than one, it flags 16 and misses 18 every one of those 18
6040
+ packs clauses under a single keyword, which is the form the coverage incentive
6041
+ actually rewards. A wider variant (keyword plus a coordinating `and`/`while`/`;`)
6042
+ reaches 29 of 34, at 14 wrong hits rather than the 3 quoted here when the corpus
6043
+ was half this size and the 1 recorded when it was first scored so reach is not
6044
+ the deciding argument, and the precision it trades away grows with the registry,
6045
+ which three successive re-readings of this paragraph have now measured rather
6046
+ than predicted. This is: both variants are
5875
6047
  silenced by deleting the word that triggered them, and neither can tell that
5876
6048
  deletion from a real split. `SHALL do A and MUST do B` clears the warning as
5877
6049
  `SHALL do A and do B` — same two obligations, one fewer normative keyword. A
5878
6050
  rule whose cheapest fix degrades the artifact it protects does not ship.
5879
6051
  *`divergent-param` (WARNING) — rejected.* One param name declared with different
5880
- values in two requirements sharing an id prefix. Measured: it fires twice on
5881
- this repo and both firings are wrong — `idleTimeoutMin` (`15` in `ATX-3`, `30`
5882
- in `ATX-10`), unrelated illustrative fixtures, and `code` (`registry-not-static`
5883
- in `ATX-17`, `empty-spec` in `ATX-18`, `rationale-placeholder` in `ATX-21`),
5884
- diagnostics that could not possibly agree. The second arrived on its own when
6052
+ values in two requirements sharing an id prefix. Measured: it fires 5 times on
6053
+ this repo and **every firing is judged wrong** — `idleTimeoutMin` (`15` in
6054
+ `ATX-3`, `30` in `ATX-10`), unrelated illustrative fixtures, and `code`
6055
+ (`registry-not-static` in `ATX-17`, `empty-spec` in `ATX-18`,
6056
+ `rationale-placeholder` in `ATX-21`), diagnostics that could not possibly agree.
6057
+ That second group has since grown to span **19 ids**, and two more param names
6058
+ have joined it on the same structure (`flag`, `payloadKinds`). The second arrived on its own when
5885
6059
  `ATX-18` was added and grew a third member on its own again when `ATX-21` was,
5886
6060
  then `ATX-25`, then `ATX-26`, which is the more damning half: a generic param
5887
6061
  name collides more often as a registry grows, so the false-positive rate rises
@@ -5898,8 +6072,19 @@ firing. Until it appeared, every false positive had been on the single name
5898
6072
  workaround and therefore left the rule arguably salvageable. It is not the word:
5899
6073
  two requirements owning unrelated constants of the same *kind* is the structure,
5900
6074
  and any registry large enough to be worth linting has it. Still zero true
5901
- positives, now across a 34-entry corpus (the `corpus` figure in the suite's
6075
+ positives, now across an 86-entry corpus (the `corpus` figure in the suite's
5902
6076
  snapshot, which counts a fixture id once per registry file that declares it).
6077
+ **Two failure modes have been measured since, and both are about the key rather
6078
+ than the threshold.** The grouping keeps one id per distinct value, so two
6079
+ requirements naming the *same* constant — the case a reader would most want
6080
+ compared — are the pair the key deduplicates away; that has now happened twice,
6081
+ most sharply on `ATX-18`/`ATX-77`, a pair split deliberately because each is
6082
+ falsifiable in a direction the other is blind to. And the key's *identity*
6083
+ moves when a requirement agreeing with an existing member is added, so a finding
6084
+ has to be re-judged by hand while nothing about it changed — three re-keyings
6085
+ across three releases under `flag` alone, none of which moved a verdict. A rule
6086
+ whose output churns on additions that agree with it is one whose maintenance
6087
+ cost is a function of registry growth, on top of the zero precision above.
5903
6088
  The escape hatch it was blocked on has since been designed and does not rescue
5904
6089
  it: with no config file the only workable shape is an additive
5905
6090
  `independentParams?: string[]` on the requirement (a CLI flag is per-invocation
package/README.md CHANGED
@@ -167,7 +167,7 @@ Every diagnostic carries a `code`, and every code has a section in
167
167
  ```
168
168
  ERROR registry-not-static (requirements/upload.reqs.ts:5)
169
169
  Value is not a literal.
170
- → https://gitlab.com/Pseudorca/attest/-/blob/v0.9.1/docs/en/troubleshooting.md#registry-not-static
170
+ → https://gitlab.com/Pseudorca/attest/-/blob/v0.9.2/docs/en/troubleshooting.md#registry-not-static
171
171
  ```
172
172
 
173
173
  The anchor **is** the code, so the link cannot point somewhere the section
@@ -180,6 +180,7 @@ isn't. In `--json` the same link is on each issue as `docsUrl`.
180
180
  | **CLI reference** — every command, flag and JSON field | [en](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/en/cli-reference.md) | [中文](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/zh/cli-reference.md) |
181
181
  | **Troubleshooting** — one section per issue code | [en](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/en/troubleshooting.md) | [中文](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/zh/troubleshooting.md) |
182
182
  | **Design** — the authoritative design of the framework | [en](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/en/attest-design.md) | [中文](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/zh/attest-design.md) |
183
+ | **Judging your own intent layer** — what no gate checks, and a method for it | [en](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/en/intent-quality.md) | [中文](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/zh/intent-quality.md) |
183
184
  | **Feedback template** — report how adoption actually went | [en](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/en/feedback.template.md) | [中文](https://gitlab.com/Pseudorca/attest/-/blob/main/docs/zh/feedback.template.md) |
184
185
 
185
186
  The change workflow is not here: it is what `attest init` writes into your
@@ -0,0 +1,54 @@
1
+ import type { Issue, ParsedScenario, Registry } from './core/types.js';
2
+ export type { Issue, ParsedScenario, Registry, Requirement } from './core/types.js';
3
+ /** What a project declares, read without executing any of it. */
4
+ export interface Inspection {
5
+ /**
6
+ * Every requirement declared under the root, merged across registry files.
7
+ *
8
+ * A registry file that could not be read statically contributes nothing here
9
+ * and one entry to {@link Inspection.issues} — so an empty result never means
10
+ * "this project has no requirements" without the issues saying why.
11
+ */
12
+ readonly registry: Registry;
13
+ /**
14
+ * The scenarios each requirement id has, in declaration order.
15
+ *
16
+ * A `Map` rather than a record, and not only for the key type: a record keyed
17
+ * by ids from a file on disk is the `Object.prototype` hazard ATX-42 exists
18
+ * for, and a `Map` has no prototype chain to read a requirement out of. The
19
+ * cost is that `JSON.stringify` renders it as `{}` — a caller serialising
20
+ * this owes `Object.fromEntries`, and the type is a `Map` so that the
21
+ * conversion is a decision rather than a surprise.
22
+ *
23
+ * An id with no scenarios is absent rather than present-and-empty — the same
24
+ * distinction `cover` reports, and the one a coverage check is asking about.
25
+ */
26
+ readonly scenarios: ReadonlyMap<string, readonly ParsedScenario[]>;
27
+ /**
28
+ * Everything that stopped a *file* being read, in the shape every command
29
+ * reports: one unreadable file scraps itself and nothing else (ATX-65), which
30
+ * is what lets a caller report on the rest of the project.
31
+ *
32
+ * Not a promise that nothing throws. A root that cannot be walked at all is a
33
+ * caller's mistake rather than a finding about a project, and rejects.
34
+ */
35
+ readonly issues: readonly Issue[];
36
+ }
37
+ /**
38
+ * Read the requirements and scenarios declared under `root`, executing nothing.
39
+ *
40
+ * `root` is the project directory, resolved here so a relative one behaves the
41
+ * same as an absolute one rather than half-working: the walk would find the
42
+ * files either way, and only the relative paths in the result would be wrong,
43
+ * which is the shape of bug that survives a demo.
44
+ *
45
+ * Every `file` in the result is relative to that root and POSIX-spelled on
46
+ * every platform (`paths.ts`) — the spelling a caller can compare against and
47
+ * store, on a value that would otherwise carry the host's separator.
48
+ *
49
+ * The registry read and the spec parse are independent, so they run
50
+ * concurrently — each already bounds its own fan-out over a tree whose size is
51
+ * not ours to choose.
52
+ */
53
+ export declare function inspectProject(root: string): Promise<Inspection>;
54
+ //# sourceMappingURL=inspect.d.ts.map
@@ -0,0 +1,68 @@
1
+ // The static reading layer, as a second public entry point (design §10).
2
+ //
3
+ // Why it exists: everything this repository knows about the quality of its own
4
+ // requirements is a hand-judged table in `tests/intent-rule-candidates.spec.ts`,
5
+ // and the check that keeps such a table from rotting is "the scenario this row
6
+ // names is one a spec really declares". An adopter could not write that check.
7
+ // The package exported the authoring API and nothing else, so the ids, the
8
+ // statements and above all the *scenario names* of their own project were
9
+ // unreachable from outside — not merely undocumented, unreachable. That is what
10
+ // this entry point answers, and the deliberate limit is that it answers only
11
+ // that: it reports what a project declares, and judges none of it (design §0).
12
+ //
13
+ // **One function rather than the four internals it composes**, and the reason is
14
+ // a security property rather than convenience. `loadRegistry` takes a
15
+ // `RegistryReader`, and which reader runs is fixed by the command — never chosen
16
+ // by a caller, never a fallback — because one of the two executes the project's
17
+ // code and the other does not (design §5.1). Exporting the readers would hand
18
+ // that choice to an adopter, which is the one thing the two-adapter seam must
19
+ // never allow. What is exported therefore has the static reader welded in, and
20
+ // there is no parameter that could change it.
21
+ //
22
+ // The same rule decides the import graph: nothing here may reach `vite` or
23
+ // `vitest`, so a consumer that only analyses does not acquire the runner peer.
24
+ // `tests/import-boundary.spec.ts` gates that rather than leaving it to review —
25
+ // this module is on the short list of entry points whose closure is walked.
26
+ import { resolve } from 'node:path';
27
+ import { loadRegistry, parseSpecs, scanProject, staticReader } from './core/locate.js';
28
+ /**
29
+ * Read the requirements and scenarios declared under `root`, executing nothing.
30
+ *
31
+ * `root` is the project directory, resolved here so a relative one behaves the
32
+ * same as an absolute one rather than half-working: the walk would find the
33
+ * files either way, and only the relative paths in the result would be wrong,
34
+ * which is the shape of bug that survives a demo.
35
+ *
36
+ * Every `file` in the result is relative to that root and POSIX-spelled on
37
+ * every platform (`paths.ts`) — the spelling a caller can compare against and
38
+ * store, on a value that would otherwise carry the host's separator.
39
+ *
40
+ * The registry read and the spec parse are independent, so they run
41
+ * concurrently — each already bounds its own fan-out over a tree whose size is
42
+ * not ours to choose.
43
+ */
44
+ export async function inspectProject(root) {
45
+ const projectRoot = resolve(root);
46
+ const { reqsFiles, specFiles } = await scanProject(projectRoot);
47
+ const [loaded, parsed] = await Promise.all([
48
+ loadRegistry(projectRoot, staticReader(), reqsFiles),
49
+ parseSpecs(specFiles, projectRoot),
50
+ ]);
51
+ const scenarios = new Map();
52
+ for (const scenario of parsed.plan.scenarios) {
53
+ const existing = scenarios.get(scenario.reqId);
54
+ if (existing === undefined)
55
+ scenarios.set(scenario.reqId, [scenario]);
56
+ else
57
+ existing.push(scenario);
58
+ }
59
+ return {
60
+ registry: loaded.registry,
61
+ scenarios,
62
+ // Registry issues before spec issues, and each already in sorted file
63
+ // order: the list is a function of the project rather than of which read
64
+ // finished first, so two runs over one tree answer identically.
65
+ issues: [...loaded.issues, ...parsed.issues],
66
+ };
67
+ }
68
+ //# sourceMappingURL=inspect.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@am_shork/attest",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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": {
@@ -26,6 +26,10 @@
26
26
  "types": "./dist/core/registry.d.ts",
27
27
  "default": "./dist/core/registry.js"
28
28
  },
29
+ "./inspect": {
30
+ "types": "./dist/inspect.d.ts",
31
+ "default": "./dist/inspect.js"
32
+ },
29
33
  "./package.json": "./package.json"
30
34
  },
31
35
  "files": [