@am_shork/attest 1.1.0 → 1.1.1

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
@@ -90,6 +90,149 @@ above and 110 stale lines on this one.
90
90
  security hole takes a major, on whatever schedule a hole is found. That price is
91
91
  now stated rather than unknown, which is what made the line decidable at all.
92
92
 
93
+ ## [1.1.1] - 2026-09-03
94
+
95
+ ### Documentation
96
+
97
+ - **`docs/{en,zh}/intent-quality.md` gains a seventh finding: a param no
98
+ statement interpolates changes with nothing outside the line to show it.**
99
+ `render --check` gives a review surface for the 92.1% of this repository's
100
+ params that a statement does interpolate — editing one moves `SPEC.md`. It
101
+ cannot help with the rest: five are structurally uninterpolatable (a
102
+ kind-to-payload table or a list, which `non-scalar-interpolation` refuses in
103
+ a statement) and two are scalars nobody interpolated. For those, every gate
104
+ treats the edit as fine — `check` sees a schema-valid literal,
105
+ `possible-drift` is satisfied by any covering scenario reading the param at
106
+ all, and `verify` stays green wherever the assertion derives its expectation
107
+ from the same value — so a human reading the registry diff is the only thing
108
+ that catches an unintended move. Split out of the *Under consideration* entry
109
+ on changing an existing param's value, as the cheap half it named while the
110
+ WARNING it also considered stays undecided for want of a second corpus.
111
+
112
+ ### Added
113
+
114
+ - **`pre-commit-check` gained §9, a hand sweep for a dependency bump.** Closes
115
+ the *Considered and rejected* entry on a standing `no-deprecated` gate: three
116
+ deprecated call sites across a Zod 3→4, Vitest 3→4 and typescript-eslint
117
+ 8.2→8.65 walk was too small a denominator to carry a third tsconfig or an
118
+ `allowJs` admission into the wider typed project, so the check stays a manual
119
+ step — run `@typescript-eslint/no-deprecated` against a throwaway project that
120
+ also covers the five files no tsconfig in this tree includes
121
+ (`eslint.config.js`, `bin/attest.js`, the three `vitest*.config.ts`) — rather
122
+ than a permanent gate.
123
+
124
+ - **A gate on which files inside this tree may name the package by the name it
125
+ publishes under.** A built `dist/` makes this package resolve *itself*:
126
+ `package.json` names the subpaths in `exports`, so `@am_shork/attest/define`
127
+ resolves to `dist/core/registry.js` on any machine that has run a build. CI's
128
+ lint, test and typecheck jobs run before one, and there it resolves nowhere.
129
+ Measured once, on an archived `requirements.delta.ts` written with the
130
+ specifier the workflow document hands *adopters*: it passed `eslint`, both
131
+ typechecks, the whole suite, `check self`, `verify self` and `archive --apply`
132
+ locally — **six gates** — then failed **three CI jobs** on that one line. This
133
+ is the sibling of the `bin/attest.js` loads `dist/` gotcha and the more
134
+ dangerous half of it: there a stale build runs old code, here the build
135
+ vouches for an import that has no other resolution.
136
+
137
+ `tests/import-boundary.spec.ts` now reads the whole checked tree through the
138
+ same AST walk the runner-boundary tests use, and asserts the set of published
139
+ specifiers against a one-entry allowlist. The single legitimate namer is
140
+ `examples/judgement-table/judgement.spec.ts` at `@am_shork/attest/inspect`,
141
+ and it is legitimate for exactly the reason the rule exists: `./inspect` is
142
+ the only subpath with somewhere to resolve *to* inside this tree, via
143
+ `resolve.alias` in `vitest.config.ts` and the matching `paths` entry in
144
+ `tsconfig.typecheck.json`.
145
+
146
+ *Two things about the walk are load-bearing and neither is obvious.* It does
147
+ not use `findFiles`, because `SKIP_DIRS` excludes `changes/` and `archive/` —
148
+ correctly, for a normal scan — while `tsc` and `eslint` are handed `self/**`
149
+ and walk straight into both, and the import this exists for was in an
150
+ *archived* delta; the engine's own scanner would have produced a gate that
151
+ passes over the only file it was written about, which is what the first draft
152
+ of it did. And it covers the repo root as well as the four directories
153
+ `tsconfig.typecheck.json` includes, because `vitest.config.ts`,
154
+ `vitest.self.config.ts` and `vitest.consumer.config.ts` are named by no
155
+ `include` in the tree yet Vitest resolves them **first**, before it can
156
+ collect a single test — a published specifier there fails CI earlier and
157
+ harder than one under `self/`, and the directory-shaped version of this gate
158
+ would not have looked. Each half is pinned by an instrument guard, so a walk
159
+ that quietly stops reading `examples/` or the root fails on the guard rather
160
+ than passing empty.
161
+
162
+ - **A product identity, as delivery artifacts.** `assets/brand/` carries the
163
+ mark, four icon sizes and four lockups as SVG, with `assets/brand/README.md`
164
+ as the guide: the geometry grid, the mortise-and-tenon overlap that must not
165
+ become clearance, the scaling rules, the type stack and the palette. The
166
+ README header and `package.json`'s keywords are the rest of it.
167
+
168
+ **Nothing in `src/` reads any of it, and that is the shape of the change
169
+ rather than a stage it has not reached yet.** A CLI half was built and dropped
170
+ in the same session — a palette module plus a `--help` banner — and the
171
+ argument that closed it is filed under *Considered and rejected*.
172
+
173
+ ### Changed
174
+
175
+ - **Three deprecated call sites retired, found by turning
176
+ `@typescript-eslint/no-deprecated` on the whole typed tree rather than by
177
+ reading the diff.** Neither was reachable by the repo's standing gates: both
178
+ compiled, linted and ran green, because a deprecation is a JSDoc tag and
179
+ nothing in `eslint.config.js` was asking for it.
180
+
181
+ `src/core/schema.ts` passed `{ message: … }` to the `statement` refinement.
182
+ Zod 4 deprecated that parameter in favour of `error`, and the rest of the file
183
+ had already moved — `paramValue`, `RegistryRecord` and `RegistrySchema` all
184
+ spell it `error` — so this was the one survivor of an incomplete migration,
185
+ not a considered exception. The wording is unchanged, and by the
186
+ `Issue.message` rule it is prose either way, so nothing observable moved:
187
+ parsed against the built schema, the refusal still comes back with the same
188
+ `message` under the same `custom` code. Design §5.1 quotes this schema in both
189
+ languages and carries the same edit.
190
+
191
+ `tests/registry-types.spec-d.ts` used `expectTypeOf(…).toMatchTypeOf<…>()`,
192
+ deprecated in expect-type 1.2.0 and split into two successors. Both sites
193
+ assert *assignability to a supertype* — `Registry` for the whole registry,
194
+ `Requirement` for one entry — which is `toExtend`, the successor that keeps
195
+ the `extends` relation. `toMatchObjectType` is the other one and would have
196
+ been the wrong reading: it checks a strict subset of keys, which is a
197
+ different claim than the one these two tests were making.
198
+
199
+ `eslint.config.js` called `tseslint.config()`, deprecated in
200
+ typescript-eslint 8.65 now that ESLint core provides the same `extends`
201
+ flattening via `defineConfig()`. Swapped for `defineConfig` + `globalIgnores`
202
+ from `eslint/config`. Because both spellings lint clean, *clean was not
203
+ evidence*: equivalence was established by diffing `eslint --print-config` for
204
+ a file in each of the five trees across the swap — byte-identical — plus the
205
+ linted file set and problem count, 166 and 0 on both sides.
206
+
207
+ **The third one is the interesting one, because the first sweep structurally
208
+ could not see it.** `no-deprecated` is type-aware, so it only reaches files
209
+ some project includes — and `tsconfig.typecheck.json` includes `src`, `tests`,
210
+ `self` and `examples`, which leaves `eslint.config.js`, `bin/attest.js` and
211
+ the three `vitest*.config.ts` named by no `include` in the tree. This is the
212
+ same blind spot `tests/import-boundary.spec.ts` was written about from the
213
+ other direction: the root config files are read *first* by the tooling and
214
+ belong to no project last. Re-run against a throwaway project that includes
215
+ them, the remaining four are clean — and that result was itself checked
216
+ against the failure shape this repo keeps meeting, by re-running the scan on
217
+ the pre-fix `eslint.config.js` and confirming it goes red, since a scan
218
+ matching no files is also silent.
219
+
220
+ With that gap closed — and the installed dependency tree carrying no registry
221
+ deprecation, the adopter-facing samples inside `skill.ts` and every code fence
222
+ in `docs/` and the README checked against the deprecated identifiers harvested
223
+ from the dependency typings, `fixtures/**` scanned under a throwaway project,
224
+ and the suite run under `--pending-deprecation` surfacing no runtime `DEP`
225
+ warning — this is the whole inventory as of Zod 4.4, expect-type 1.4,
226
+ typescript-eslint 8.65, Vitest 4.1, TypeScript 5.9 and Node 24.
227
+
228
+ **Two halves of that are unmeasured rather than clean, and are named here
229
+ rather than left to be inferred from the version list.** `package.json`
230
+ declares `typescript` as `^5.5.0 || ^6.0.0` while 5.9.3 is what is installed,
231
+ so anything TypeScript 6 deprecates has not been looked at; and `engines`
232
+ allows Node 20.19 while the sweep ran on 24, so a `DEP` warning that fires
233
+ only on an older runtime would not have appeared. Nothing gates any of it
234
+ going forward — see *Under consideration*.
235
+
93
236
  ## [1.1.0] - 2026-09-02
94
237
 
95
238
  ### Added
@@ -7098,6 +7241,15 @@ have to gather again.
7098
7241
  `intent-quality.md`, the four things no gate is looking at already being that
7099
7242
  page's subject and this being a fifth of the same kind.
7100
7243
 
7244
+ **The cheap half is written under `[Unreleased]`, and the decision above is
7245
+ unchanged by it.** `docs/{en,zh}/intent-quality.md` now carries a seventh
7246
+ finding: a param no statement interpolates changes with nothing outside the
7247
+ line to show it, so the discipline that catches an unintended move there is a
7248
+ human reading the registry diff. That costs no engine surface and settles
7249
+ nothing this entry is still waiting on — the WARNING in blocker **(2)** is
7250
+ still a rule with no measured population, and this is the fallback the entry
7251
+ itself named for exactly that state, not a smaller version of the rule.
7252
+
7101
7253
  - **The intent layer is separable from the repository it describes, and the
7102
7254
  verification layer is not — so a third party can audit intent and never
7103
7255
  obtain a verdict.** Filed 2026-08-22 from a design evaluation of using Attest
@@ -7399,47 +7551,6 @@ have to gather again.
7399
7551
  cost somebody a round trip. Without the first, this trades a derivable field for
7400
7552
  an underivable duplicate.
7401
7553
 
7402
- - **A Stop hook that will not let an agent end a turn while `check self` is red.**
7403
- From the same read: `nostics` ships `hooks/consume-diagnostic.mjs`, a Claude
7404
- Code Stop hook that reads the log its dev-server collector writes and returns
7405
- `{"decision": "block"}` with the diagnostic as the reason, so the agent keeps
7406
- working rather than stopping on a red application. Three details there are the
7407
- design and not the plumbing, and are the transferable part: it guards on
7408
- `stop_hook_active` so a blocked stop cannot loop; it feeds back **one**
7409
- diagnostic per turn rather than the whole log; and when it consumes one it drops
7410
- every other entry sharing that code and file, so a fault at twenty call sites is
7411
- one round trip and not twenty.
7412
- *Why the shape fits this repository specifically.* `CLAUDE.md`'s "Before
7413
- committing" and the whole of `pre-commit-check` are discipline: nothing runs
7414
- `check self` unless a person or an agent remembers to, and the repeat-grouping
7415
- that hook does by hand is something `formatIssues` already computes. `.claude/`
7416
- here holds five skills, no hooks and no `settings.json`, so this would be the
7417
- first of its kind rather than an extension of anything.
7418
- *Measured cost, 2026-09-01.* `node bin/attest.js check self --json` runs in
7419
- **1.04–1.57 s** over three consecutive runs; `status self --json` in 0.78 s.
7420
- That is paid at the end of every turn, which is the number to weigh rather than
7421
- the wall clock of a single invocation.
7422
- *What blocks it is the stale-`dist/` trap, and a hook is the worst place to meet
7423
- it.* `bin/attest.js` loads `dist/`, not `src/` — the Gotcha this repository
7424
- already keeps — so a Stop hook reports on the last build. After a `src/` edit it
7425
- blocks on a diagnostic the working tree has already fixed, or clears while the
7426
- tree is broken, and in both directions it is a gate arguing about a state nobody
7427
- is in. Either the hook builds first, adding seconds to every turn, or it runs the
7428
- engine from source and stops being the thing an adopter could copy.
7429
- *And the larger question it sits in front of.* If this is right here it is right
7430
- for an adopter, which makes it a question about what `attest init` writes rather
7431
- than about `.claude/` in this repository. A hook that can block an agent's stop
7432
- is a much larger commitment than a document, and the rejection quoted in full
7433
- inside the `init --check` entry in this section — the gate on the installed
7434
- skill's freshness — turned on exactly the distinction it needs:
7435
- a signal about work the reader actually did, against a chore the tool imposes. A
7436
- block on `check` is the first kind, which is the argument for it, and that
7437
- argument should be made explicitly rather than arrived at by shipping.
7438
- *What would move it.* Running it here, unshipped and repo-local, for a few weeks,
7439
- and counting how often it blocks on something that would otherwise have been
7440
- committed. If that count is zero the discipline was already working and the hook
7441
- is overhead; if it is not, the number is the case for `init` writing one.
7442
-
7443
7554
  - **An outward-facing form of the judgement tables, so an adopter can run this
7444
7555
  counter-pressure on their own registry.**
7445
7556
  `tests/intent-rule-candidates.spec.ts` maintains four hand-judged tables over
@@ -7675,6 +7786,68 @@ it sat between 0.2.0 and 0.1.7 for two releases, where standing still meant
7675
7786
  sinking one version deeper each time a release was cut above it, and a rejection
7676
7787
  filed under a version reads as belonging to it.
7677
7788
 
7789
+ ### A brand palette and banner in the CLI
7790
+
7791
+ Built, measured green on every gate, and dropped 2026-09-03 in the session that
7792
+ built it. Filed here rather than deleted, because the half that was wrong is not
7793
+ the half that looks wrong, and the next person to propose "let's put the brand
7794
+ colours in the CLI" should reach this before rebuilding it.
7795
+
7796
+ **What it was.** `src/cli/brand.ts`: one module holding the palette behind a
7797
+ semantic interface — `pass`, `drift`, `muted`, `note`, `id`, plus the three
7798
+ marks — with `cli/report.ts` and `cli/index.ts` rewritten onto it so `chalk` was
7799
+ imported in exactly one place. Attest Green for a passing verdict, Drift Orange
7800
+ for a WARNING, Slate for every secondary line, and a four-row ASCII mark above
7801
+ `--help` on a TTY.
7802
+
7803
+ **The deletion test passed, which is why this is worth recording.** Delete the
7804
+ module and the same seven colour decisions reappear across two files and thirty
7805
+ call sites, spelled slightly differently each time — that is not a pass-through,
7806
+ and it is the ordinary reason this repository consolidates something. The gates
7807
+ agreed: lint, both typechecks, the full suite, `check self`, `verify self` and
7808
+ `render --check` were all green, and `attest --help` through a pipe was
7809
+ byte-identical, so `self/cli-surface.spec.ts` saw nothing.
7810
+
7811
+ **What it could not answer is who the interface is for.** A module earns its
7812
+ keep by having callers who vary across it, and this one had two callers that
7813
+ never will: there is no second theme, no `--color` flag, no second renderer, and
7814
+ nothing in the roadmap proposes one. *One adapter is a hypothetical seam.* The
7815
+ identity was consolidated at a place where nothing varies, which buys a
7816
+ re-tunability nobody has asked to use, and every colour in it is invisible to
7817
+ every test in the tree — they strip SGR before asserting, correctly, because
7818
+ `Issue.message` is prose and so is its colour. So the module's whole surface is
7819
+ unassertable: **the interface is the test surface, and this interface had none.**
7820
+
7821
+ **Three findings survive the rejection, and they are the reason it is written
7822
+ out.** They cost a session to learn and none of them is recoverable from the
7823
+ diff, which no longer exists.
7824
+
7825
+ - *Frost (`#E6EAF2`) can never be written by a CLI.* The guide defines it as
7826
+ "text on Void", and a terminal's background belongs to the reader — Frost on a
7827
+ light profile is invisible. The terminal's own foreground already plays that
7828
+ role. The same argument retires Void, Surface and Paper: a CLI that paints its
7829
+ own ground fights the profile the reader picked.
7830
+ - *Drift Orange cannot become the ERROR colour.* The palette has no failure
7831
+ colour, and the guide makes Drift Orange's scarcity part of what the product
7832
+ means — it appears when something is wrong. Spending it on every ERROR spends
7833
+ the meaning. The terminal's red is not a placeholder for a brand colour; it is
7834
+ the right answer.
7835
+ - *A monospace mark must be ASCII, and the reason is width, not encoding.*
7836
+ `█`, `╱`, `╲` and `═` are all East Asian **ambiguous** width, so a CJK
7837
+ terminal — which is where this repository is read — renders them two cells
7838
+ wide, and mixing them with a one-cell `=` or space tears a multi-row figure
7839
+ apart. The CLI's existing `✓`, `✗` and `●` are ambiguous too and survive it
7840
+ only because each is alone on its line with nothing aligned under it. This
7841
+ generalises past branding: it applies to any table, tree or rule the CLI ever
7842
+ draws.
7843
+
7844
+ *One thing found on the way is not a branding decision and does not belong to
7845
+ this rejection*: `chalk.dim` is SGR 2, which some profiles render at 40% opacity
7846
+ and others ignore outright, so the same secondary line is either unreadable or
7847
+ undifferentiated depending on the terminal. That is a legibility defect in
7848
+ output that ships today, and it survives this entry as a candidate on its own
7849
+ terms rather than as a reason to bring the palette back.
7850
+
7678
7851
  ### A backlink index over `archive/`, recording which change introduced or renamed an id
7679
7852
 
7680
7853
  Proposed from OpenViking's `.relations.json`, restated twice on measurement, and
@@ -9407,6 +9580,47 @@ declared prefix ownership recorded above in this section — rejected on there
9407
9580
  being no adoption that needs it, which is the same condition this sentence
9408
9581
  supposes — which addresses, rather than a scorer, which ranks.
9409
9582
 
9583
+ ### A standing gate on deprecated API usage
9584
+
9585
+ Priced on 2026-09-02, on the sweep that retired three deprecated call sites
9586
+ under `[Unreleased]` — turning `@typescript-eslint/no-deprecated` on by hand
9587
+ over a dependency walk through Zod 3→4, Vitest 3→4 and typescript-eslint
9588
+ 8.2→8.65, none of the three reachable by anything the repo runs on a commit.
9589
+ The obvious move was to promote the rule into `eslint.config.js` beside
9590
+ `no-unnecessary-condition`, itself promoted from `strictTypeChecked` on exactly
9591
+ this argument: it needs the checker, and it found real instances here.
9592
+
9593
+ *What blocked it was not the rule, it was the reach.* `no-deprecated` is
9594
+ type-aware, so it only sees files a project includes, and the one deprecated
9595
+ site with the widest blast radius — `tseslint.config()` in `eslint.config.js`
9596
+ itself — sits in the set no project includes, along with `bin/attest.js` and
9597
+ the three `vitest*.config.ts`. Turning the rule on as-is buys a gate that
9598
+ covers `src`, `tests`, `self` and `examples` and silently *not* the files the
9599
+ tooling reads first, which is the shape this repo has been burned by twice
9600
+ already: `tests/import-boundary.spec.ts` deliberately does not use `findFiles`
9601
+ for the same reason, and its instrument guards exist because a walk that
9602
+ quietly stops reading a tree passes empty. A gate whose coverage is
9603
+ believed-wider-than-it-is is the failure this file keeps recording. Closing the
9604
+ gap meant either growing `tsconfig.typecheck.json` over the root config files —
9605
+ pulling `allowJs` into the wider project for `eslint.config.js` and
9606
+ `bin/attest.js` — or a second small project just for them, a third tsconfig in
9607
+ a repo that deliberately has two and has a comment in each explaining why.
9608
+
9609
+ **Rejected on the denominator.** Three call sites across an entire dependency
9610
+ walk is too small to carry the cost of either tsconfig shape — permanent, on
9611
+ every file the wider project would newly admit — against a defect a hand sweep
9612
+ at upgrade time already catches. `pre-commit-check` §9 records that sweep: run
9613
+ `no-deprecated` against a throwaway project covering what
9614
+ `tsconfig.typecheck.json` does and the five files it does not, and confirm the
9615
+ scan is actually seeing them before trusting a clean result. The check still
9616
+ happens, on the cadence the defect actually arrives on, without a third
9617
+ tsconfig or `allowJs` admitted into a wider project.
9618
+
9619
+ *Worth reopening if the count moves* — a dependency bump that turns up more
9620
+ than a handful of deprecated sites in one sweep is the signal that a hand pass
9621
+ is no longer keeping up, and it is measured the same way this entry was: read
9622
+ the `no-deprecated` output before deciding whether it needs a permanent home.
9623
+
9410
9624
  ## How an entry is re-measured
9411
9625
 
9412
9626
  What the three sections above have learned about themselves, kept apart from all
package/README.md CHANGED
@@ -1,4 +1,9 @@
1
- # Attest
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://gitlab.com/Pseudorca/attest/-/raw/main/assets/brand/attest-logo-horizontal.svg">
4
+ <img src="https://gitlab.com/Pseudorca/attest/-/raw/main/assets/brand/attest-logo-horizontal-light.svg" alt="Attest" width="260">
5
+ </picture>
6
+ </p>
2
7
 
3
8
  A TDD-native spec framework. **Tests are the source of truth for verification;
4
9
  ID-bound requirements are the source of truth for intent.** Attest binds the two
@@ -159,7 +164,7 @@ Every diagnostic carries a `code`, and every code has a section in
159
164
  ```
160
165
  ERROR registry-not-static (requirements/upload.reqs.ts:5)
161
166
  Value is not a literal.
162
- → https://gitlab.com/Pseudorca/attest/-/blob/v1.1.0/docs/en/troubleshooting.md#registry-not-static
167
+ → https://gitlab.com/Pseudorca/attest/-/blob/v1.1.1/docs/en/troubleshooting.md#registry-not-static
163
168
  ```
164
169
 
165
170
  The anchor **is** the code, so the link cannot point somewhere the section
@@ -42,7 +42,7 @@ export const RequirementSchema = z.object({
42
42
  statement: z
43
43
  .string()
44
44
  .refine((s) => /\b(SHALL|MUST)\b/.test(s), {
45
- message: 'statement must contain the RFC-2119 keyword SHALL or MUST',
45
+ error: 'statement must contain the RFC-2119 keyword SHALL or MUST',
46
46
  }),
47
47
  rationale: z.string().min(10, 'rationale must not be empty (the intent layer has to say why)'),
48
48
  // A param is any JSON value. Lists (vendor blacklists, id sets) are the most
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@am_shork/attest",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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
  "packageManager": "pnpm@10.28.0",
@@ -64,9 +64,12 @@
64
64
  "keywords": [
65
65
  "tdd",
66
66
  "spec",
67
- "requirements",
68
67
  "drift",
69
- "vitest"
68
+ "requirements",
69
+ "traceability",
70
+ "requirements-traceability",
71
+ "vitest",
72
+ "typescript"
70
73
  ],
71
74
  "license": "MIT",
72
75
  "dependencies": {