@am_shork/attest 0.4.2 → 0.4.3

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,172 @@ 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.4.3] - 2026-08-01
17
+
18
+ ### Added
19
+
20
+ - **The README's two getting-started samples are now the fixture the packaging
21
+ test installs and runs.** `tests/docs-consistency.spec.ts` opened by naming
22
+ this hole: every `*.md` outside the generated `SPEC.md` is hand-written prose
23
+ that no mechanism binds to the code, "the framework's own blind spot (design
24
+ §11)". Every check in that file until now gated a version string or a name;
25
+ this is the first that gates *meaning* — a registry or a spec on the page that
26
+ the engine would now reject was undetectable, which for a tool whose whole
27
+ claim is reporting drift between intent and what the tests execute is the
28
+ failure that costs the most.
29
+ The mechanism is a naming split rather than new machinery, and most of it was
30
+ already there and unnamed: `fixtures/consumer/` imports by bare specifier from
31
+ `@am_shork/attest` and `@am_shork/attest/define` because it is installed from a
32
+ real tarball outside the repo — which is exactly what a reader has to be able
33
+ to paste. So the two files lost their fixture-specific commentary, gained the
34
+ README's teaching comments, and the README now quotes them; the new assertion
35
+ is byte-equality between the fenced block and the file, newline-normalised so a
36
+ CRLF checkout cannot call a clean tree stale. The fixture is the original and
37
+ the page is the quote: breaking either side fails.
38
+ *What moved in the README:* the spec sample is now `session.spec.ts` beside
39
+ `session.ts` rather than `auth/session.spec.ts`, matching the layout the
40
+ packaging test actually runs, and it shows both of the requirement's scenarios
41
+ because the installed run asserts `scenarioCount: 2`.
42
+ *Limits, both deliberate:* the gate covers 2 of the 23 fenced `ts` blocks in
43
+ the README and `docs/en/**` — the rest are elided, ✓/✗-annotated, or
44
+ deliberately rejected by the engine, and none of those can equal a runnable
45
+ file. And the two claims are joined only on a release: this gate runs in
46
+ `pnpm test` and proves the page and the fixture agree, while the run that
47
+ proves the fixture still *works* is `pnpm test:consumer`, which needs a network
48
+ and a tarball. Both remaining halves are in `Planned`.
49
+
50
+ - **The `docs/en` ↔ `docs/zh` mirror rule is now a gate rather than a paragraph
51
+ in `CLAUDE.md`.** Two assertions in `tests/docs-consistency.spec.ts`, both
52
+ scoped to what is comparable without comparing prose. The file set: a page
53
+ added on one side only is not a stale translation, it is a page that does not
54
+ exist, and nothing in the repository would have noticed. And the structure of
55
+ each pair — the heading tree as levels in document order, and the sequence of
56
+ fence info strings — which catches a section dropped or reordered in
57
+ translation, a code sample present in one language only, and a ```ts that
58
+ became ```bash.
59
+ *The parse is the part with a trap in it.* A `#` at the start of a line inside
60
+ a ```bash sample is a shell comment, and these pages are full of them, so the
61
+ reader tracks fences and skips their contents; a closing fence must repeat the
62
+ character that opened it, or a ``` quoted inside a ~~~ block ends the wrong
63
+ thing. An unterminated fence is reported rather than ignored, because it hides
64
+ every heading after it — two documents can agree by being truncated. All three
65
+ are asserted directly, on the standard this file already applies to its other
66
+ parses: a check whose parser can silently match nothing is not a check.
67
+ *One third of the queued entry turned out to be already covered and was not
68
+ written.* It asked for the `##` issue-code headings of `troubleshooting.md` to
69
+ be held equal between languages; the roster check that predates it already
70
+ asserts each language against `ISSUE_CODES` in both directions, so their
71
+ equality follows and a third assertion would catch nothing they let through.
72
+ *The limit, stated because it is not small:* a page whose structure held while
73
+ its meaning went stale passes everything here. Translation is not a diff. Same
74
+ shape as the limit the History gate carries — it can check that a row cites a
75
+ real version, not that the row is true.
76
+
77
+ ### Documentation
78
+
79
+ - **The agent-facing documents gained a diagnosis discipline, a review pass, and
80
+ a design vocabulary**, adapted from an external skill collection rather than
81
+ copied from it — the parts that survived are the ones this repository's own
82
+ defect history had already paid for.
83
+ *New skill, `.claude/skills/diagnose/`.* Its one rule is that no hypothesis may
84
+ be formed before a command exists that has been run at least once and can go
85
+ **red** on the bug in hand. Every defect this repository has recorded failed
86
+ silently rather than loudly, so that bar is higher here than the phrase
87
+ suggests, and the skill's local content is the questions that tell a green run
88
+ apart from one that never happened: read the test count rather than the colour,
89
+ since a `beforeAll` throw reports a file as *skipped* (`[0.4.2]`); confirm the
90
+ file you care about actually ran, since a glob that matched nothing is a clean
91
+ run over code that never loaded; and name which half a command covers before
92
+ trusting it. The ways that goes wrong are the Gotchas in `CLAUDE.md`, pointed
93
+ at rather than restated — a second copy of them would only be a second copy to
94
+ drift. The remainder is the standard loop: minimise until every element is
95
+ load-bearing, three to five falsifiable hypotheses before testing any,
96
+ `[DEBUG-…]`-tagged probes, and "no correct seam is itself the finding".
97
+ *`pre-commit-check` gained §8, a reading pass on two axes*, kept unmerged and
98
+ unranked against each other because a change can pass either and fail the
99
+ other. The Spec axis is mostly machine-checked here and nowhere else — `check`,
100
+ `cover` and `verify` *are* drift detection — so it reads only for what those
101
+ cannot reach: whether an `ATX-n`'s text states the contract, whether `params`
102
+ pin the values the scenario should be pinned to, and whether a green scenario
103
+ attests its requirement or merely passes. The Standards axis carries a twelve-
104
+ smell baseline (Fowler, *Refactoring* ch.3) as labelled heuristics that any
105
+ documented rule here overrides, with two weighted for this repository:
106
+ duplicated path-shaped code means a second copy that did not go through
107
+ `src/core/paths.ts`, and speculative generality is the other face of §1's rule
108
+ that every requirement costs a scenario forever.
109
+ *`CLAUDE.md` gained a design vocabulary* — module, interface, depth, seam,
110
+ adapter, and the three tests that settle most arguments — which exists because
111
+ the two structures it names were already these things and had no word for it.
112
+ `src/core/paths.ts` is the repository's deepest module, and the deletion test
113
+ is not hypothetical for it: every silent path defect in the Gotchas is what
114
+ this codebase looked like when that knowledge lived at the call sites. The two
115
+ registry readers are two adapters at one real seam, which is what makes the
116
+ differential suite possible — and the entry marks where the analogy stops,
117
+ since which adapter runs is fixed by the command and never a fallback, that
118
+ being a security property rather than a design preference.
119
+ *And a section on where a decision goes*, which is this file. It is stated in
120
+ `CLAUDE.md` rather than only in `pre-commit-check` because the moment to write
121
+ one is when the decision happens; a design evaluated and dropped mid-session is
122
+ what is most likely to be lost, and by commit time the reasoning that killed it
123
+ is already gone.
124
+ *The candidate that did not survive is the reason the section exists.* The
125
+ proposal was a `docs/adr/` directory holding three decisions lifted out of the
126
+ Gotchas — why `fixtures/nested-spec/` may not be flattened, why `render
127
+ --check` compares the document rather than the bytes, why one `spawn` helper.
128
+ All three were already recorded here, in more detail than the proposal would
129
+ have carried, so the directory's whole effect would have been to split a store
130
+ this file keeps deliberately single. Recorded rather than filed as work, since
131
+ no such directory exists: had it been put under `docs/en/`, the mirror gate
132
+ added above would not have seen it at all — `pages()` is a non-recursive
133
+ `readdir` filtered to `*.md`, so a subdirectory is invisible to the file-set
134
+ check, the structure check, and the version-bearing sample scan alike. A
135
+ documentation subdirectory is therefore an unmirrored one, silently, which is
136
+ this repository's signature failure shape wearing documentation's clothes.
137
+
138
+ ### Fixed
139
+
140
+ - **The packaging test installed into a directory Windows spells two ways, and
141
+ read the result as the package being broken.** `tests/consumer.spec.ts` staged
142
+ its throwaway project under `os.tmpdir()`, which on Windows reads `TEMP` — set
143
+ by the OS to the 8.3 short form, `C:\Users\JOHN_C~1\...`. Vite resolves the
144
+ peer `vitest` from the *long* form of the same directory, so the consumer's
145
+ spec file and the installed `@am_shork/attest` imported two module instances of
146
+ it; `describe` ran against an uninitialised runner and the spec died at import
147
+ with `Cannot read properties of undefined (reading 'config')`, before a
148
+ scenario executed. What that surfaced as is the part worth recording: `attest
149
+ verify` reported `tests-red` plus `declared-not-run` for every scenario —
150
+ a report indistinguishable from a genuinely broken package, produced by a
151
+ package that is fine. Staging through `realpath` fixes it.
152
+ This is `CLAUDE.md`'s first failure family one form further out — not a
153
+ separator inside a path, but two spellings of one directory — and it was
154
+ unreachable until `[0.4.2]` made this suite run on Windows at all. CI is Linux,
155
+ where `os.tmpdir()` has one spelling, so the pipeline was never going to say
156
+ otherwise.
157
+
158
+ - **The AST parser's own source file was binary, so every change to it since the
159
+ initial commit was unreviewable.** `src/core/parser.ts` separated the two
160
+ halves of its param-ref dedupe key with a **raw NUL byte** rather than the
161
+ `\x00` escape. One character no editor renders reclassified the whole file:
162
+ `git diff` answered `Binary files a/… and b/… differ` for every change to it,
163
+ `git grep` and `grep` skipped it in silence, and `.gitattributes`' `eol=lf`
164
+ never applied, because `text=auto` tests for binary first.
165
+ *What it cost is the reviewing, not the running.* A string escape is resolved
166
+ by the compiler, so the key is the same three code units on every platform —
167
+ no behaviour changes, and no requirement, scenario or schema moves. What was
168
+ lost is that every text sweep over this repository silently excluded the one
169
+ module that turns spec source into the plan every command reads. A pre-commit
170
+ pass grepping `src/` for a rule — a `path.relative` outside `paths.ts`, a
171
+ stray `spawn` — was answering about 27 of 28 files and reporting no gap, which
172
+ is worse than not having run it.
173
+ *And it shipped.* `files` carries `dist`, and `tsc` copies a template
174
+ literal's contents verbatim, so `dist/core/parser.js` put the byte into every
175
+ installed copy.
176
+ Recorded at this length because the defect is not the byte but its invisibility:
177
+ this is `CLAUDE.md`'s silent-failure shape with no platform in it at all, and
178
+ the tools anyone would bring to look for it are the ones it disables. The last
179
+ thing it costs is its own diff — the old side of this commit is still the
180
+ binary blob, so this entry is the only readable account of the change.
181
+
16
182
  ## [0.4.2] - 2026-07-30
17
183
 
18
184
  ### Added
@@ -1880,26 +2046,10 @@ feedback reports are triaged into issues or `Unreleased` and deliberately not
1880
2046
  kept in this tree; that rule stands, and holds for anything with a reporter, a
1881
2047
  repro, or a state to track. What is kept here is the narrower thing this file
1882
2048
  already keeps one section of: a **decision with an argument attached**, where
1883
- losing the argument would cost more than losing the task. The first and last
1884
- below came out of reading another repository against this one — `fast-check` and
1885
- `msw` respectively. The middle one came out of building something queued here,
1886
- which is the other way an entry arrives: a gate that finds a defect on its first
1887
- run files the defect.
1888
-
1889
- - **A structural parity gate over `docs/en/**` and `docs/zh/**`.** The mirror
1890
- rule — one Chinese page per English page, matching down to the comments inside
1891
- the code samples, both carried by the same commit — is enforced today by a
1892
- paragraph in `CLAUDE.md` and by whoever remembers to read it. The home is
1893
- `tests/docs-consistency.spec.ts`, which already enumerates both directories
1894
- and already says in its own header why it checks only the two couplings tight
1895
- enough to assert cheaply; this is a third. What is comparable without
1896
- comparing prose: the heading tree (level, order, count), the count of fenced
1897
- code blocks, and — for `troubleshooting.md` — the set of `##` issue-code
1898
- headings, which is machine-derived on both sides and so must be equal
1899
- exactly. Translation is not a diff and nothing here should pretend otherwise,
1900
- which is also the limit: a page whose structure held while its meaning went
1901
- stale passes. Same shape as the limit the History gate already carries — it
1902
- can check that a row cites a real version, not that the row is true.
2049
+ losing the argument would cost more than losing the task. The last below came out
2050
+ of reading another repository against this one — `msw`. The first came out of
2051
+ building something queued here, which is the other way an entry arrives: a gate
2052
+ that finds a defect on its first run files the defect.
1903
2053
 
1904
2054
  - **A diagnosable refusal when the resolved compiler has no AST API.**
1905
2055
  Discovered by building the `typescript` matrix (see `[0.4.2]`), which is
@@ -1928,40 +2078,36 @@ run files the defect.
1928
2078
  it yet — a separate decision, with the honest answer for now being that the
1929
2079
  supported range has a ceiling and says so.
1930
2080
 
1931
- - **Fixtures that are also the code samples the documentation shows.**
1932
- `tests/docs-consistency.spec.ts` opens by naming this exact hole: every `*.md`
1933
- outside the generated `SPEC.md` is hand-written prose that no mechanism binds
1934
- to the code — "the framework's own blind spot (design §11)" and it asserts
1935
- only the two couplings cheap enough to assert. This is a fourth, after the
1936
- `docs/en` `docs/zh` parity gate queued above, and it is the one that reaches
1937
- meaning rather than structure: a fenced block in `cli-reference.md` or
1938
- `troubleshooting.md` showing a registry the engine would now reject is
1939
- undetectable today. For a tool whose entire claim is reporting drift between
1940
- intent and what the tests execute, its own documentation drifting from what the
1941
- engine does is the failure that costs the most and is currently reported by
1942
- nobody.
1943
- **The shape that makes it cheap is a naming split, not a new mechanism.** `msw`
1944
- requires every integration test to be two files: a `*.mocks.ts` holding a usage
1945
- example with *nothing test-specific in it*, and a `*.test.ts` that loads the
1946
- example and asserts against it so the example is simultaneously the fixture
1947
- and a snippet publishable to users unmodified. Here that means promoting a
1948
- subset of `fixtures/` to that status and having the English page quote the file
1949
- rather than restate it, with the assertion being byte-equality between the
1950
- fenced block and the fixture.
1951
- **Not every fixture is a candidate, and the exclusions are the existing ones.**
1952
- `fixtures/runner-red` and `fixtures/e2e-bad` are inputs to child runs that
1953
- assert failure, and `fixtures/nested-spec/` exists to put a separator in a path
1954
- none is written to be read as an example, and dressing them up as one would
1955
- cost the thing they are for.
1956
- *The cost is the mirror rule, and it caps what can be claimed.* `docs/zh/**`
1957
- matches `docs/en/**` down to the comments inside its code samples, so
1958
- byte-equality can hold on one side only; the Chinese block stays translated
1959
- prose around the same code, which is the parity gate's problem and is why these
1960
- two entries should land together or in that order. A page whose sample is
1961
- byte-equal to a living fixture while the paragraph above it describes the old
1962
- behaviour still passes — the same limit already written down for the History
1963
- rows, and the same answer: this binds what is machine-derivable and leaves the
1964
- rest to a reader.
2081
+ - **Region-marked samples, so a partial quote can be bound too.** The whole-file
2082
+ half of this shipped in `[0.4.3]`: the README's two getting-started blocks
2083
+ are now byte-equal to `fixtures/consumer/`. That buys 2 of the 23 fenced `ts`
2084
+ blocks in the README and `docs/en/**`, and the ceiling is structural rather
2085
+ than lazy — the other 21 are elided (`{ /* */ }`), annotated (`// ✓ the single
2086
+ source`, `// a literal that can drift`), or deliberately rejected by the engine
2087
+ (`params: { maxMb: MAX_MB }`), and none of those can equal a runnable file.
2088
+ **What raises it is marking spans, not a second mechanism.** A
2089
+ `// #region readme:registry` and its close inside the fixture, with the
2090
+ assertion running block against dedented span, lets one file feed several
2091
+ passages and makes a partial quote bindable by construction. Where that
2092
+ actually reaches, counted rather than hoped: `troubleshooting.md`'s
2093
+ single-source pair is two verbatim lines of `fixtures/consumer/session.spec.ts`
2094
+ and its `AUTH-3` block is the registry entry at a different indent — so a
2095
+ handful more, not the remainder. Anything carrying a `✓`/`✗` is authored
2096
+ commentary and stays a fragment; that is what it is for.
2097
+ *The blocks are a separate question, and worth not conflating.* A sample
2098
+ showing input the engine must reject could be bound to a fixture whose
2099
+ *rejection* is asserted which is what `fixtures/eval-invalid-registry`
2100
+ already is. Binding those is the more valuable half, because a counterexample
2101
+ that stopped being a counterexample is the one kind of stale sample that
2102
+ actively teaches the wrong thing. It is also the one that needs a fixture per
2103
+ diagnostic, so it is not free the way the regions are.
2104
+ *The mirror rule still caps what can be claimed on the Chinese side.*
2105
+ `docs/zh/**` matches `docs/en/**` down to the comments inside its code samples,
2106
+ so byte-equality holds on one side only and the zh blocks stay translated prose
2107
+ around the same code the parity gate in `[0.4.3]` is what covers them now,
2108
+ and it covers structure, not meaning. Unchanged from the whole-file half: the residual
2109
+ risk recorded with it in `[0.4.3]` that the gate and the run proving the
2110
+ fixture still works live in different suites applies here identically.
1965
2111
 
1966
2112
  ## Under consideration
1967
2113
 
@@ -2271,7 +2417,8 @@ requirement, and human review at propose is still the whole answer. What is
2271
2417
  rejected is grouping as a way to assist it, and this reopens only on a
2272
2418
  contradiction that grouping would have caught.
2273
2419
 
2274
- [Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.4.2...main
2420
+ [Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.4.3...main
2421
+ [0.4.3]: https://gitlab.com/Pseudorca/attest/-/tags/v0.4.3
2275
2422
  [0.4.2]: https://gitlab.com/Pseudorca/attest/-/tags/v0.4.2
2276
2423
  [0.4.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.4.1
2277
2424
  [0.4.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.4.0
package/README.md CHANGED
@@ -67,13 +67,13 @@ export default defineRequirements({
67
67
  });
68
68
  ```
69
69
 
70
- **2. Attest it with a scenario** (`auth/session.spec.ts`):
70
+ **2. Attest it with scenarios** (`session.spec.ts`):
71
71
 
72
72
  ```ts
73
73
  import { expect } from 'vitest';
74
74
  import { requirement, scenario } from '@am_shork/attest';
75
- import reqs from '../requirements/auth.reqs.js';
76
- import { createSession, advance, isValid } from './session.js';
75
+ import reqs from './requirements/auth.reqs.js';
76
+ import { createSession, advance, touch, isValid } from './session.js';
77
77
 
78
78
  requirement('AUTH-3', () => {
79
79
  scenario('idle timeout invalidates the session', () => {
@@ -82,9 +82,22 @@ requirement('AUTH-3', () => {
82
82
  advance(s, t + 1, 'minutes');
83
83
  expect(isValid(s, t)).toBe(false);
84
84
  });
85
+
86
+ scenario('activity resets the idle timer', () => {
87
+ const t = reqs['AUTH-3'].params.idleTimeoutMin;
88
+ const s = createSession();
89
+ advance(s, t - 1, 'minutes');
90
+ touch(s);
91
+ advance(s, t - 1, 'minutes');
92
+ expect(isValid(s, t)).toBe(true);
93
+ });
85
94
  });
86
95
  ```
87
96
 
97
+ Both files above are quoted from `fixtures/consumer/`, which the packaging test
98
+ installs from a real tarball and runs — a test asserts the quotes are byte-equal
99
+ to the files, so a sample the engine would now reject cannot survive here.
100
+
88
101
  **3. Run the engine:**
89
102
 
90
103
  ```bash
@@ -136,7 +149,7 @@ Every diagnostic carries a `code`, and every code has a section in
136
149
  ```
137
150
  ERROR registry-not-static (requirements/upload.reqs.ts:5)
138
151
  Value is not a literal.
139
- → https://gitlab.com/Pseudorca/attest/-/blob/v0.4.2/docs/en/troubleshooting.md#registry-not-static
152
+ → https://gitlab.com/Pseudorca/attest/-/blob/v0.4.3/docs/en/troubleshooting.md#registry-not-static
140
153
  ```
141
154
 
142
155
  The anchor **is** the code, so the link cannot point somewhere the section
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@am_shork/attest",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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": {