@am_shork/attest 0.7.0 → 0.7.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 +285 -112
- package/README.md +4 -4
- package/dist/core/apply.js +7 -10
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +1 -0
- package/dist/core/locate.d.ts +5 -7
- package/dist/core/locate.js +5 -7
- package/dist/core/merge.js +27 -2
- package/dist/core/order.d.ts +17 -0
- package/dist/core/order.js +25 -0
- package/dist/core/pipeline.js +1 -4
- package/dist/core/render.js +90 -50
- package/dist/core/runner.js +4 -6
- package/dist/core/schema.d.ts +13 -6
- package/dist/core/schema.js +54 -18
- package/dist/core/splice.d.ts +13 -12
- package/dist/core/splice.js +59 -18
- package/dist/core/static-registry.js +6 -0
- package/dist/core/status.js +4 -9
- package/dist/core/terminal.d.ts +9 -23
- package/dist/core/terminal.js +9 -23
- package/dist/core/types.d.ts +1 -1
- package/dist/core/validator.d.ts +1 -0
- package/dist/core/validator.js +37 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,141 @@ 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.7.1] - 2026-08-05
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`params` takes any JSON value, and the constraint moved to the place it was
|
|
21
|
+
actually protecting.** A param was a scalar or a list of scalars. The argument
|
|
22
|
+
for admitting lists — the values most likely to drift are the composite ones,
|
|
23
|
+
and rejecting them left exactly those outside the one mechanism built to pin
|
|
24
|
+
them — does not stop at a list: a `kind -> payload` table drifts harder than a
|
|
25
|
+
flat list of names, and it was the one shape still excluded. `null` comes with
|
|
26
|
+
it, because refusing it forces a sentinel (`''`, `-1`) that means something
|
|
27
|
+
else to every later reader.
|
|
28
|
+
What the old union was defending was not the registry but the **rendering**:
|
|
29
|
+
`{payloadKinds}` interpolated into a statement as `[object Object]`, in a
|
|
30
|
+
document reviewers and audit read as the system's promise. That is a property
|
|
31
|
+
of the interpolation point, so it is stated there — a new
|
|
32
|
+
**`non-scalar-interpolation`** ERROR from `check` and `verify`. Narrower and
|
|
33
|
+
truer: it refuses what actually breaks and admits the data a scenario reads,
|
|
34
|
+
which the schema rule could not tell apart. What the schema keeps is what does
|
|
35
|
+
not survive being written down — a function, a `Date`, a class instance, an
|
|
36
|
+
object whose prototype was swapped.
|
|
37
|
+
*The part that is not a schema edit.* Depth. Four obligations had exactly one
|
|
38
|
+
level of reach, which is correct only while a param cannot nest, and none of
|
|
39
|
+
them fails loudly: the canonical form behind `add-conflict` (an identical copy
|
|
40
|
+
written with its inner keys in another order would have conflicted with
|
|
41
|
+
itself), the ATX-58 control-character stripping (nested strings would have
|
|
42
|
+
gone unsanitised, reopening the hole `[0.7.0]` closed), the key order behind a
|
|
43
|
+
byte-stable rendering, and `splice.ts`, which writes a requirement back out as
|
|
44
|
+
TypeScript source and would have committed `[object Object]` into a
|
|
45
|
+
`*.reqs.ts` through `archive --apply`. Three of the four are ordering, and are
|
|
46
|
+
now one shared `sortDeep` in `order.ts` rather than three sorts a level deep —
|
|
47
|
+
they wanted the same guarantee for the same reason, which is what makes it one
|
|
48
|
+
function and not a helper each.
|
|
49
|
+
*Two divergences between the readers, which is what the differential suite is
|
|
50
|
+
for.* `null` is a keyword rather than a literal node, so the AST reader fell
|
|
51
|
+
off the end of `literalValue` and reported `registry-not-static` for the whole
|
|
52
|
+
file while the evaluating reader accepted it. And `{ __proto__: … }` swaps the
|
|
53
|
+
prototype instead of creating a key, so `z.record` copies own keys into a fresh
|
|
54
|
+
object and the taint is invisible one step later — the static reader refuses
|
|
55
|
+
that source at any depth, and the schema now refuses the object the evaluator
|
|
56
|
+
builds from it, both spellings (a literal `__proto__:` and the own key
|
|
57
|
+
`JSON.parse` produces).
|
|
58
|
+
*Rendering.* A structured param gets a fenced JSON block below the params
|
|
59
|
+
table, keys sorted at every depth (ATX-10: the same registry renders the same
|
|
60
|
+
bytes) with the fence measured against backtick runs in the value, as `code()`
|
|
61
|
+
already did. It is below the table rather than in it because a fence cannot
|
|
62
|
+
live in a table cell. And `render` defends the document on its own: it reads
|
|
63
|
+
the registry with no spec parse, so no `AttestPlan` and no `validateStructure`
|
|
64
|
+
— `attest render` runs to completion on a registry `check` refuses. Leaning on
|
|
65
|
+
the diagnostic would have left the document defended by a command nobody is
|
|
66
|
+
obliged to run first.
|
|
67
|
+
*The writing side of the `__proto__` rule.* `splice.ts` turns a requirement
|
|
68
|
+
back into TypeScript source for `--apply`, and it is the only site that does.
|
|
69
|
+
Extending it to depth meant it would have emitted a `__proto__` key as a bare
|
|
70
|
+
property — text whose *evaluation* swaps a prototype rather than being the
|
|
71
|
+
value it was handed. Unreachable through any command (the schema rejects a
|
|
72
|
+
nested `__proto__` and `z.record` drops a top-level one), and refused anyway
|
|
73
|
+
with a new `UnwritableValue`, reported as `internal-error` by the merge with
|
|
74
|
+
its account of what it had already written intact. Quoting is not the repair —
|
|
75
|
+
a quoted `__proto__` swaps the prototype in a literal exactly as the bare form
|
|
76
|
+
does, and the bracketed form that would create an own property is a computed
|
|
77
|
+
key no static reader will read. The reader's guard is worth nothing if the
|
|
78
|
+
writer can produce the file it exists to refuse.
|
|
79
|
+
*Not breaking, with one edge worth naming.* No previously-green registry goes
|
|
80
|
+
red — a structured param was `registry-invalid` before, so nothing that
|
|
81
|
+
compiled and passed now fails — and an all-scalar registry renders
|
|
82
|
+
byte-identical, so a committed `render --check` stays green across the
|
|
83
|
+
upgrade. What did widen is the exported type in a **read** position:
|
|
84
|
+
`Requirement['params'][string]` is now the recursive JSON value. The const
|
|
85
|
+
generic is untouched, so `reqs['AUTH-3'].params.idleTimeoutMin` is still the
|
|
86
|
+
literal `30` and every anti-drift test keeps its narrow type. A consumer only
|
|
87
|
+
notices if they assign an *undeclared* key to the old scalar-or-list union
|
|
88
|
+
(`Type 'null' is not assignable`), or wrote their own handler over that type
|
|
89
|
+
and does `String(v)` in it — which is the `[object Object]` defect this entry
|
|
90
|
+
fixes in `render` and `splice`, seen from outside.
|
|
91
|
+
*This overturns a standing rejection, and not on its stated terms.* *Nested
|
|
92
|
+
objects in `params`* sat under **Considered and rejected** since `[0.4.2]`,
|
|
93
|
+
raised by `mine-capablanca`, and that entry is removed by this release. Its
|
|
94
|
+
argument was that a param has two properties a table lacks: rendered as a value
|
|
95
|
+
a human checks at a glance, and read by a scenario as the one place the value
|
|
96
|
+
lives. The first is answered rather than waived — `non-scalar-interpolation`
|
|
97
|
+
keeps a table out of the *sentence*, so what a reader takes in at a glance is
|
|
98
|
+
still glance-sized, and the table is rendered below it as a block. The second
|
|
99
|
+
is answered by the criterion that admitted arrays in the first place: a
|
|
100
|
+
`kind -> payload` table drifts *harder* than a flat list, so the reason arrays
|
|
101
|
+
were let in is the reason this shape should have been.
|
|
102
|
+
**The reopening condition it named was never met, and that is worth stating
|
|
103
|
+
plainly.** The entry said it reopens on "a report where the value is genuinely
|
|
104
|
+
promised — a table an adopter owes their users"; none has been seen. This
|
|
105
|
+
shipped on the strength of the argument alone. What changed is the recognition
|
|
106
|
+
that the schema was answering the wrong question: *params for promises, plain
|
|
107
|
+
constants for tuning* is the rule, and a type can only ever check shape, so it
|
|
108
|
+
refused a promised table while admitting a list of tuning knobs. Design §11
|
|
109
|
+
now carries that as a judgement rather than a type, which is the permanent half
|
|
110
|
+
— and it is weaker than what it replaces, because nothing enforces a judgement.
|
|
111
|
+
New requirements: `ATX-7` rewritten, `ATX-63` added.
|
|
112
|
+
|
|
113
|
+
### Changed
|
|
114
|
+
|
|
115
|
+
- **Rationale that only a comment held now has a design section, and the comment
|
|
116
|
+
cites it.** Measured over the diffs in three eras split at `[0.3.0]` and
|
|
117
|
+
`[0.5.0]`: comment blocks got better by every specificity signal — naming a
|
|
118
|
+
symbol 50 → 60%, a measured number 3 → 13%, a causal argument 26 → 54% — and
|
|
119
|
+
roughly doubled per line of code, while the share citing a `§` went 15 → 12 →
|
|
120
|
+
11% and the design document stopped growing (sixteen sections since `[0.3.0]`,
|
|
121
|
+
105 lines against `src/`'s 2,862). **The anchoring did not fall; it never
|
|
122
|
+
scaled**, and a `§` is the only part of a comment any gate reads.
|
|
123
|
+
Nineteen blocks written since `[0.5.0]` carried the cost — ten lines or more,
|
|
124
|
+
fully argued, unanchored — and they split three ways. *Six were properties a
|
|
125
|
+
rewrite would have to preserve*, and now sit in the document: a new **§9.1**
|
|
126
|
+
for the artifacts Attest writes, an extension to **§8** (`--apply` acts on this
|
|
127
|
+
run's verdict), and one to **§5.4** (the two readers of one plan must descend
|
|
128
|
+
the same way). *Four were history* — a quadratic backtrack, a 27–84x constant
|
|
129
|
+
factor, a 47 MiB peak, an indexing cost filed as invisible — and were
|
|
130
|
+
compressed to the invariant a maintainer would otherwise re-break; every
|
|
131
|
+
measurement in them was already under `[0.7.0]`, so this deleted duplicates
|
|
132
|
+
rather than moving anything. *Ten were correct as they stood and were left
|
|
133
|
+
alone*, which is the half worth naming: `§` is low because the design document
|
|
134
|
+
stalled, not because comments are too long, so a pass that shortened comments
|
|
135
|
+
and left it at one in nine would have moved the wrong quantity and called it
|
|
136
|
+
done. Net `src/`: 29 lines removed, all of them comments and no code touched,
|
|
137
|
+
which is also how the deletion was checked.
|
|
138
|
+
**One of the six corrected the document rather than extending it.** §8
|
|
139
|
+
summarised a merge as renaming a proposed spec "in place", which is true of its
|
|
140
|
+
location and not of its imports — the delta it reads its proposed params from
|
|
141
|
+
is what moves to `archive/`, so the sentence hid the one specifier the merge
|
|
142
|
+
has to repoint.
|
|
143
|
+
*What the measurement cost.* A first pass reported the `§` share as halving,
|
|
144
|
+
25 → 12%, an artefact of bucketing by `git blame` over the current tree: that
|
|
145
|
+
sees only surviving lines, and `§`-citing comments survive better, being tied
|
|
146
|
+
to sections that do not move, so the early era was measured on its most durable
|
|
147
|
+
residue. Re-measuring from the diffs before building is what caught it.
|
|
148
|
+
The routing test this produced is in `pre-commit-check` §6, and the obligation
|
|
149
|
+
a new diagnostic code owes the CLI reference is now in §2.
|
|
150
|
+
|
|
16
151
|
## [0.7.0] - 2026-08-04
|
|
17
152
|
|
|
18
153
|
### Added
|
|
@@ -3022,82 +3157,26 @@ feedback reports are triaged into issues or `Unreleased` and deliberately not
|
|
|
3022
3157
|
kept in this tree; that rule stands, and holds for anything with a reporter, a
|
|
3023
3158
|
repro, or a state to track. What is kept here is the narrower thing this file
|
|
3024
3159
|
already keeps one section of: a **decision with an argument attached**, where
|
|
3025
|
-
losing the argument would cost more than losing the task.
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
cost the most was the thing already written down** — which is an argument for
|
|
3046
|
-
this section, since the note that corrected the estimate survived only because
|
|
3047
|
-
someone had filed the reasoning next to the code.
|
|
3048
|
-
`compiler-unsupported` came from building something queued here,
|
|
3049
|
-
which is one way an entry arrives: a gate that finds a defect on its first run
|
|
3050
|
-
files the defect, and the `typescript` matrix `[0.4.2]` added is what found this
|
|
3051
|
-
one.
|
|
3052
|
-
`--apply` came from an adoption report (`mine-capablanca`, 0.4.2, 34 requirements
|
|
3053
|
-
/ 56 scenarios / greenfield) — a third way an entry arrives: not a defect, but a
|
|
3054
|
-
shape the tool made an adopter work around eight times without ever erroring. It
|
|
3055
|
-
took `spec-in-change-dir` out with it, by a fourth way worth naming: designing one
|
|
3056
|
-
of `--apply`'s checks surfaced a different one, against a migration this file had
|
|
3057
|
-
itself shipped. **What a release costs its adopters is not fully known on the day
|
|
3058
|
-
it is cut** — and building the entry is what finds out, which is also how
|
|
3059
|
-
`--apply` discovered that one clause of its own argument here was wrong. Both are
|
|
3060
|
-
recorded under `[0.6.0]`, and two earlier entries from the same report left
|
|
3061
|
-
by shipping in `[0.5.0]` — a change's specs moving to their merged location, and
|
|
3062
|
-
the delta reading as the registry of what it adds — their arguments recorded
|
|
3063
|
-
there, including the part of the first one that turned out to be wrong.
|
|
3064
|
-
The entries added after `[0.6.0]` arrived a **fifth** way, and it is the
|
|
3065
|
-
one with no external input at all: a strict read of `src/` with nothing failing.
|
|
3066
|
-
Every gate in this repository was green while all three were true, which is the
|
|
3067
|
-
whole of what that way is for — a defect no test is shaped to ask about is
|
|
3068
|
-
reachable only by someone reading for it. **All three have now shipped under
|
|
3069
|
-
`[0.7.0]`**, and each closed having found something a strict read is
|
|
3070
|
-
apparently good for and a gate is not. The runner one: the gate that would have
|
|
3071
|
-
had to catch it *already* looked for the shape, so what the read bought was not a
|
|
3072
|
-
missing gate but a cost nobody had priced. The load-failure diagnosis: every gate
|
|
3073
|
-
was green because the report was *complete* — three ERRORs on a red run — and
|
|
3074
|
-
what was wrong was which three, a judgement no assertion in this repository is
|
|
3075
|
-
shaped to make. The third, eight items under one heading: six comments naming a
|
|
3076
|
-
property the code lacked and two costs beside them.
|
|
3077
|
-
|
|
3078
|
-
**One entry has now left this section the other way — by being rejected** — and
|
|
3079
|
-
it is the first, which is why it is worth a line. The region-marked samples were
|
|
3080
|
-
decided *yes*, and building the count they rested on is what killed them: the two
|
|
3081
|
-
spans the entry named are not spans, and both carry a mark the entry's own rule
|
|
3082
|
-
excludes. It moved down with that measurement attached. The lesson is not about
|
|
3083
|
-
regions. **An entry can sit in `Planned` for releases on the strength of a figure
|
|
3084
|
-
nobody has checked**, because the section's discipline is aimed at keeping the
|
|
3085
|
-
*argument* rather than at re-testing the evidence under it — so the first act of
|
|
3086
|
-
building one should be re-measuring what it claims, not implementing what it
|
|
3087
|
-
proposes. That is what happened here, and it cost an hour instead of a mechanism.
|
|
3088
|
-
|
|
3089
|
-
*What the fifth way cost and returned, now that all of it is spent.* Sixteen
|
|
3090
|
-
items, no external input, and nothing in the suite shaped to catch any of them.
|
|
3091
|
-
The returns were not evenly distributed and the split is the useful part: two
|
|
3092
|
-
defects a user could hit today (a report that misdiagnosed a load failure, a
|
|
3093
|
-
merge crossing midnight), two costs worth a second each on a real command
|
|
3094
|
-
(`check --eval` at 1 + N Vite servers, `control` at 27–84x), one fixed toll on
|
|
3095
|
-
every static invocation (~1 s), and the rest true-but-unobservable — identical
|
|
3096
|
-
output either way. Only the last group is genuinely optional, and it is also the
|
|
3097
|
-
group that would never have been found any other way, because *no observable
|
|
3098
|
-
symptom* is precisely the definition of what a gate cannot see. The transferable
|
|
3099
|
-
lesson is the batching: individually none of the unobservable ones justifies a
|
|
3100
|
-
commit, and together they cost about an hour.
|
|
3160
|
+
losing the argument would cost more than losing the task.
|
|
3161
|
+
|
|
3162
|
+
**Where an entry went is recorded under the version that shipped it.** This
|
|
3163
|
+
section keeps no ledger of its own departures — one was maintained here by hand
|
|
3164
|
+
for several releases, and a count kept in prose with nothing to check it is a
|
|
3165
|
+
claim that goes wrong quietly. What is worth keeping is what the section learned
|
|
3166
|
+
about itself, which is three things.
|
|
3167
|
+
|
|
3168
|
+
**An entry can sit here for releases on the strength of a figure nobody has
|
|
3169
|
+
checked.** The discipline aims at keeping the *argument*, not at re-testing the
|
|
3170
|
+
evidence under it, so the first act of building one is re-measuring what it
|
|
3171
|
+
claims rather than implementing what it proposes. That has paid twice: once
|
|
3172
|
+
killing an entry outright, when the spans it rested on turned out not to be
|
|
3173
|
+
spans; once correcting an entry's headline number before a line of it was
|
|
3174
|
+
written. **What a release costs its adopters is not known on the day it is cut**
|
|
3175
|
+
— several entries arrived from one adoption report, about shapes the tool made
|
|
3176
|
+
someone work around without ever erroring. And **the fixes with no observable
|
|
3177
|
+
symptom are worth batching**: individually none justifies a commit, together they
|
|
3178
|
+
cost about an hour, and they are the group no gate could have found, since having
|
|
3179
|
+
no symptom is the definition of what a gate cannot see.
|
|
3101
3180
|
|
|
3102
3181
|
- **Binding the `✗` samples to fixtures whose rejection is asserted.** A sample
|
|
3103
3182
|
showing input the engine must *reject* can be bound the way a runnable one is,
|
|
@@ -3117,6 +3196,99 @@ commit, and together they cost about an hour.
|
|
|
3117
3196
|
`[0.4.3]` — that the gate and the run proving the fixture still works live in
|
|
3118
3197
|
different suites — applies here identically.
|
|
3119
3198
|
|
|
3199
|
+
- **The child run's stdout is not Attest's, and `--json` promises it is.**
|
|
3200
|
+
`runAndCollect` starts Vitest with the parent's fd 1 inherited, so a spec file
|
|
3201
|
+
in the project under test writes straight to the same stream the report goes
|
|
3202
|
+
to. `quiet: true` does not close it: `silent: true` suppresses Vitest's
|
|
3203
|
+
*console interception*, and a bare `process.stdout.write` in a spec — at
|
|
3204
|
+
collection time or inside a scenario body, both measured — goes past it
|
|
3205
|
+
untouched. Neither `core/terminal.ts` nor `sanitisedLogger` is on that path at
|
|
3206
|
+
all, because no Attest code is.
|
|
3207
|
+
*The machine surface is the half that is actually new.* `cli/index.ts` opens
|
|
3208
|
+
by stating that under `--json` "the only thing written to stdout is one
|
|
3209
|
+
machine-readable report", and §9.1 records that this path "was never exposed —
|
|
3210
|
+
`JSON.stringify` escapes every C0 character". Both hold for `check`, `cover`,
|
|
3211
|
+
`render` and `status`, and neither holds for `verify` or `archive`: measured,
|
|
3212
|
+
a spec writing `{"ok":true,"POLLUTION":"…"}` puts that document **first** on
|
|
3213
|
+
stdout, ahead of the real report, so `JSON.parse` of the whole stream fails
|
|
3214
|
+
(position 46) while anything reading line-wise or taking the first object
|
|
3215
|
+
reads the attacker's verdict. The terminal half is the same hole and the
|
|
3216
|
+
smaller finding — `ESC[2K CR` plus a forged `ERROR` header at column 0 and an
|
|
3217
|
+
`OSC 0` window retitle all arrive intact — but `verify` executes project code
|
|
3218
|
+
by design, so repainting a terminal is not a capability that path lacked.
|
|
3219
|
+
*The fix belongs to `runAction`, not to `runner.ts`.* That function is already
|
|
3220
|
+
the single convergence point for "one output, one exit code", which is the
|
|
3221
|
+
same argument that put `formatCrash` in `cli/report.ts` rather than leaving a
|
|
3222
|
+
copy in each `catch`: `runner.ts` would be made to hold a contract it does not
|
|
3223
|
+
own, while `runAction` is the only place the promise is made.
|
|
3224
|
+
|
|
3225
|
+
- **`render`'s document can carry raw HTML and a forged requirement section.**
|
|
3226
|
+
`sanitised` is the entry §9.1 names and it strips control characters and
|
|
3227
|
+
nothing else, so `statement` and `rationale` reach the Markdown by
|
|
3228
|
+
concatenation exactly as written. Measured: a `rationale` containing newlines
|
|
3229
|
+
and `## ATX-999` renders a whole section — heading, statement, `**Why:**` —
|
|
3230
|
+
for a requirement no registry holds, sitting between two real ones and worded
|
|
3231
|
+
identically; `<script>` and `<img onerror>` survive the same way. `check` and
|
|
3232
|
+
`cover` see only the real id, and `render --check` cannot object, because that
|
|
3233
|
+
*is* what the registry renders to now.
|
|
3234
|
+
*Not an accepted risk, which is the part worth pinning.* §9.1 and the
|
|
3235
|
+
reference both scope their guarantee to control characters, and the
|
|
3236
|
+
reference's heading over that paragraph is "Safe to read, and safe to keep",
|
|
3237
|
+
with "a site generator" named in the threat it describes. What *is* a decision
|
|
3238
|
+
is that prose may be marked up — `plain()` escapes a param value precisely
|
|
3239
|
+
because it is data while the statement around it is not — and that decision
|
|
3240
|
+
has never been separated from allowing raw HTML or a heading. So the entry is
|
|
3241
|
+
as much about stating which of the three is intended as about the escaping,
|
|
3242
|
+
and if the answer is that Markdown stays and HTML and headings go, that is a
|
|
3243
|
+
fourth obligation in §9.1 rather than a patch at an emitter.
|
|
3244
|
+
|
|
3245
|
+
- **`attest init` follows a symlink out of the project root.** `runInit` does
|
|
3246
|
+
`mkdir(dirname(dest), { recursive: true })` and then `writeAtomic`, and
|
|
3247
|
+
neither asks whether `dest` is still under `root`. A `.claude/skills/attest`
|
|
3248
|
+
planted in the repository as a link to anywhere the user can write is followed
|
|
3249
|
+
by both: measured on Windows with a junction, a file at the target holding
|
|
3250
|
+
`USER OWNED FILE - DO NOT OVERWRITE` came back holding the skill. The report
|
|
3251
|
+
prints the relative path, so nothing on screen says where it went.
|
|
3252
|
+
*`write.ts` is not the gap and neither is `targets.ts`'s content.* The `wx`
|
|
3253
|
+
flag defends the *temporary* path against a planted link and does exactly
|
|
3254
|
+
that; the destination's directory was never in its scope. And `workflowBody()`
|
|
3255
|
+
is a pure function of nothing, so no byte of the checked project reaches the
|
|
3256
|
+
file — the exposure is the path, not the content, which is `targets.ts`'s
|
|
3257
|
+
first rule ("Attest owns the path") being an assumption rather than a check.
|
|
3258
|
+
The check belongs beside `resolveTargets`, which is already all-or-nothing for
|
|
3259
|
+
the same reason: a name known to be unwritable must not leave half a set
|
|
3260
|
+
behind.
|
|
3261
|
+
|
|
3262
|
+
- **A deeply nested literal takes `check` down before either reader sees it.**
|
|
3263
|
+
40 KB of `[[[…]]]` in a `params` value exhausts the stack inside
|
|
3264
|
+
`ts.createSourceFile` — TypeScript's own recursive-descent parser, so
|
|
3265
|
+
`literalValue`'s recursion is never reached and hardening it would change
|
|
3266
|
+
nothing. `parser.ts` opens with the same call, so `verify` takes it too, and
|
|
3267
|
+
`loadRegistry` reads with `Promise.all`, so one file ends the whole command.
|
|
3268
|
+
*It fails closed, which is why this is a repair and not an emergency.*
|
|
3269
|
+
Measured: `internal-error`, `ok: false`, exit 1, and a well-formed `--json`
|
|
3270
|
+
envelope. What it costs is that the command this project tells people to run
|
|
3271
|
+
first on an untrusted fork MR can be made to say nothing but that, forever.
|
|
3272
|
+
The shape of the fix is already in `locate.ts`: `unreadableFiles` exists to
|
|
3273
|
+
say "this file could not be read" without ending the run, and a `RangeError`
|
|
3274
|
+
out of a single `read` or `parseSpecFile` is that, so a hostile file scraps
|
|
3275
|
+
only itself and `check`'s breadth contract survives it.
|
|
3276
|
+
|
|
3277
|
+
- **`status` prints one line the sanitiser never sees.** `cli/index.ts` writes
|
|
3278
|
+
`Change: ${result.change}` bare, while `formatStatus` puts the same value
|
|
3279
|
+
through `inline` twice, three lines apart in the same output. Measured with a
|
|
3280
|
+
directory named with U+009B — the C1 CSI the class in `control` exists for,
|
|
3281
|
+
and one NTFS permits — the header carried `c2 9b` raw while the closing
|
|
3282
|
+
`Not a verdict:` line carried a space. A change name is a directory name in
|
|
3283
|
+
the repository under test, and the workflow `init` writes tells an agent to
|
|
3284
|
+
run `attest status <name>`, so the value is the project's to choose.
|
|
3285
|
+
*One `inline()` call, and the entry is here for the reason rather than the
|
|
3286
|
+
fix.* §9.1 says a stream has no entry, only writes, so the obligation sits on
|
|
3287
|
+
every write or not at all — and this is the second time a single missed write
|
|
3288
|
+
has been the whole defect, after the two `catch` blocks that printed a raw
|
|
3289
|
+
stack beside sanitised diagnostics. The sweep filed above under `ATX-37` is
|
|
3290
|
+
what would have found it; this is one more measurement in favour of it.
|
|
3291
|
+
|
|
3120
3292
|
## Under consideration
|
|
3121
3293
|
|
|
3122
3294
|
Candidates with **no decision yet**, between the two sections either side of it
|
|
@@ -3388,6 +3560,41 @@ have to gather again.
|
|
|
3388
3560
|
because a project whose requirement count only rises should have decided in
|
|
3389
3561
|
advance what would make one leave.
|
|
3390
3562
|
|
|
3563
|
+
- **The `cmd.exe` quoting in `tests/consumer.spec.ts` guards one character of
|
|
3564
|
+
several.** `shellArg` asserts the argument holds no `"` and then quotes only
|
|
3565
|
+
when it holds whitespace, so an argument with none goes to the shell bare —
|
|
3566
|
+
and `&`, `|`, `^`, `<`, `>`, `(` are metacharacters there that the assertion
|
|
3567
|
+
does not name. The fix is one character class, which is why the entry is not
|
|
3568
|
+
about the fix.
|
|
3569
|
+
*What blocks the decision is that no input reaches it.* Both arguments are
|
|
3570
|
+
either a literal flag or a path from `mkdtemp`, `NEEDS_SHELL` is win32 only
|
|
3571
|
+
and CI is Linux, and the one path an attacker could plausibly influence — the
|
|
3572
|
+
checkout directory on a fork MR pipeline — comes from a GitLab project path,
|
|
3573
|
+
whose character set excludes every metacharacter above. So this is an
|
|
3574
|
+
observation with no attack path, and the honest question is not whether to
|
|
3575
|
+
widen the class but whether the comment above it should stop claiming more
|
|
3576
|
+
than the assertion covers: it says "the assertion keeps that true" about
|
|
3577
|
+
arguments generally, where what it checks is one character. Widening the class
|
|
3578
|
+
and leaving the prose is the change that would look like a fix and settle
|
|
3579
|
+
nothing.
|
|
3580
|
+
|
|
3581
|
+
- **A loader that fails to start leaves its scratch directory behind.**
|
|
3582
|
+
`createLoader` calls `mkdtempSync` and writes the stub *before* `createServer`,
|
|
3583
|
+
and the only `rm` of that directory is inside the `close` of the object
|
|
3584
|
+
`createServer` returns — so a throw from it leaks one `attest-loader-*` per
|
|
3585
|
+
invocation, which on a CI runner is one per build. That is the exact failure
|
|
3586
|
+
shape the comment on `close` records as already fixed, surviving on the other
|
|
3587
|
+
path.
|
|
3588
|
+
*What blocks it is that nothing has been made to throw there.* With
|
|
3589
|
+
`configFile: false` Vite still reads the `package.json` at the root to decide
|
|
3590
|
+
`type`, so a malformed one in the checked project is the obvious candidate and
|
|
3591
|
+
it is the project's to write — but it was not tried, and an entry claiming a
|
|
3592
|
+
trigger it has not seen is the thing this section exists to not accumulate.
|
|
3593
|
+
The repair is three lines (a `try` around the two calls that `rm`s and
|
|
3594
|
+
rethrows) and is not what the decision turns on; measuring whether the throw
|
|
3595
|
+
is reachable at all is, because if it is not, this is a leak with no input
|
|
3596
|
+
that produces it.
|
|
3597
|
+
|
|
3391
3598
|
## Considered and rejected
|
|
3392
3599
|
|
|
3393
3600
|
Decisions **not** to build something, kept where they can be found before the
|
|
@@ -3712,42 +3919,8 @@ requirement, and human review at propose is still the whole answer. What is
|
|
|
3712
3919
|
rejected is grouping as a way to assist it, and this reopens only on a
|
|
3713
3920
|
contradiction that grouping would have caught.
|
|
3714
3921
|
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
Raised by `mine-capablanca` (0.4.2) and explicitly not as a request, which is
|
|
3718
|
-
why it is recorded with its own answer rather than left as a complaint about a
|
|
3719
|
-
message. A `params` value is a scalar or a list of scalars; a table — piece kind
|
|
3720
|
-
to weight, tier to limit — is refused, and the only way to register one is to
|
|
3721
|
-
split it across parallel arrays, which reads worse in the rendered spec than it
|
|
3722
|
-
buys. *What rejected it: the two properties a param has, and a table has
|
|
3723
|
-
neither.* A param is **rendered into `SPEC.md` as a value a human checks at a
|
|
3724
|
-
glance**, and it is **read by a scenario as the one place that value lives**. A
|
|
3725
|
-
nested object fails the first by construction — there is no reading of a
|
|
3726
|
-
five-row table that a reviewer verifies the way they verify `30`. It fails the
|
|
3727
|
-
second more quietly: a table has many readers by definition, so "the one place"
|
|
3728
|
-
becomes "the one file", which is what an ordinary module already is. The
|
|
3729
|
-
schema's own comment records where the line was drawn and why arrays were let
|
|
3730
|
-
in — list constants are the most drift-prone values, and an array still has one
|
|
3731
|
-
owner read by one scenario. A table does not clear that bar; the reason arrays
|
|
3732
|
-
did is exactly the reason it does not.
|
|
3733
|
-
*The reporter's own resolution is the argument's strongest form.* The table went
|
|
3734
|
-
to a shared module, is not registry-owned, and **nothing was lost** — because a
|
|
3735
|
-
piece-value table is tuning, not a promise. That is the rule design §11 now
|
|
3736
|
-
states — *params for promises, plain constants for tuning* — derived
|
|
3737
|
-
independently by the same adopter before it was written anywhere. Under it this
|
|
3738
|
-
candidate is not a limitation of `params` at all: it is a value that was never a
|
|
3739
|
-
requirement's to hold, and §11 carries this rejection's own argument in the same
|
|
3740
|
-
paragraph, as the rule stated over the type rather than over the value.
|
|
3741
|
-
*What ships instead*, in `[0.5.0]`: the refusal now says what is accepted
|
|
3742
|
-
instead of `Invalid input`, and `troubleshooting.md` states the constraint and
|
|
3743
|
-
where such a value belongs — the reporter's actual ask, which was for the
|
|
3744
|
-
decision to be visible rather than discovered by reading the shipped `.d.ts`.
|
|
3745
|
-
The permanent half goes to design §11 with the params rule, not here.
|
|
3746
|
-
*This reopens on* a report where the value is genuinely promised — a table an
|
|
3747
|
-
adopter owes their users, whose rows a reviewer would want in the rendered spec.
|
|
3748
|
-
None has been seen; the one that raised it was tuning.
|
|
3749
|
-
|
|
3750
|
-
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.7.0...main
|
|
3922
|
+
[Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.7.1...main
|
|
3923
|
+
[0.7.1]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.1
|
|
3751
3924
|
[0.7.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.7.0
|
|
3752
3925
|
[0.6.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.6.0
|
|
3753
3926
|
[0.5.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.5.0
|
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ The killer move against drift: values a requirement **promises** (timeouts,
|
|
|
12
12
|
limits, budgets) live **once** in its `params`, and tests read them from there —
|
|
13
13
|
so a number is physically impossible to drift between the spec and the assertion.
|
|
14
14
|
Values that merely tune behaviour stay ordinary constants; nothing is owed to
|
|
15
|
-
anyone when a tuning knob changes. A param may be
|
|
16
|
-
|
|
17
|
-
single source as a lone number.
|
|
15
|
+
anyone when a tuning knob changes. A param may be any JSON value, so a
|
|
16
|
+
composite constant — a vendor blacklist, a `kind -> payload` table — gets the
|
|
17
|
+
same single source as a lone number, which is where drift is worst.
|
|
18
18
|
|
|
19
19
|
What that does not buy is a warning when you change the value. `check` runs
|
|
20
20
|
nothing, so editing a param leaves it at `✓ No issues` — nothing became unbound,
|
|
@@ -157,7 +157,7 @@ Every diagnostic carries a `code`, and every code has a section in
|
|
|
157
157
|
```
|
|
158
158
|
ERROR registry-not-static (requirements/upload.reqs.ts:5)
|
|
159
159
|
Value is not a literal.
|
|
160
|
-
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.7.
|
|
160
|
+
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.7.1/docs/en/troubleshooting.md#registry-not-static
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
The anchor **is** the code, so the link cannot point somewhere the section
|
package/dist/core/apply.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Map<id, Requirement>, with content-compare on ADDED and already-synced
|
|
4
4
|
// no-ops on RENAMED.
|
|
5
5
|
import { RequirementIdSchema, RequirementSchema } from './schema.js';
|
|
6
|
-
import { byCodeUnit } from './order.js';
|
|
6
|
+
import { byCodeUnit, sortDeep } from './order.js';
|
|
7
7
|
/**
|
|
8
8
|
* The ids a delta ADDs — the scope of the first-red obligation (design §6).
|
|
9
9
|
*
|
|
@@ -185,16 +185,13 @@ function canonical(req) {
|
|
|
185
185
|
return JSON.stringify({
|
|
186
186
|
statement: req.statement,
|
|
187
187
|
rationale: req.rationale,
|
|
188
|
-
|
|
188
|
+
// Deep, not one level: a param is a JSON value, so the nested keys of a
|
|
189
|
+
// kind -> payload table are as much a part of this string as the top-level
|
|
190
|
+
// ones, and the failure is identical one level down — an identical copy
|
|
191
|
+
// written with its inner keys in another order reported as `add-conflict`
|
|
192
|
+
// against itself. Code-unit order throughout, and `sortDeep` says why.
|
|
193
|
+
params: sortDeep(req.params),
|
|
189
194
|
outOfScope: [...req.outOfScope],
|
|
190
195
|
});
|
|
191
196
|
}
|
|
192
|
-
// Code-unit order, not localeCompare: this string is a *verdict input*.
|
|
193
|
-
// localeCompare calls some distinct keys equal, and a stable sort then leaves
|
|
194
|
-
// them in insertion order — so the canonical form would encode how the params
|
|
195
|
-
// happened to be written, and `add-conflict` would report a requirement as
|
|
196
|
-
// conflicting with an identical copy of itself.
|
|
197
|
-
function sortKeys(obj) {
|
|
198
|
-
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => byCodeUnit(a, b)));
|
|
199
|
-
}
|
|
200
197
|
//# sourceMappingURL=apply.js.map
|
package/dist/core/docs.d.ts
CHANGED
|
@@ -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", "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", "spec-load-failed", "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", "non-scalar-interpolation", "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", "spec-load-failed", "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
package/dist/core/locate.d.ts
CHANGED
|
@@ -141,13 +141,11 @@ export declare function idPrefix(id: string): string;
|
|
|
141
141
|
* displayed, they become the child run's `include` globs, where a Windows
|
|
142
142
|
* separator would silently match nothing.
|
|
143
143
|
*
|
|
144
|
-
* Each source is parsed as it arrives rather than after all of them
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* people to run first on an untrusted fork MR.
|
|
149
|
-
* makes the peak `PARSE_CONCURRENCY` sources instead of `files.length`, and the
|
|
150
|
-
* plan is the only thing that still grows with the tree.
|
|
144
|
+
* Each source is parsed as it arrives rather than after all of them, so the peak
|
|
145
|
+
* is `PARSE_CONCURRENCY` sources rather than `files.length` and the plan is the
|
|
146
|
+
* only thing still growing with the tree. Reading them all first is the shape to
|
|
147
|
+
* avoid: input size is not ours to choose here, `check` being what this project
|
|
148
|
+
* tells people to run first on an untrusted fork MR. Measured in `[0.7.0]`.
|
|
151
149
|
*
|
|
152
150
|
* `findFiles` above is deliberately left unbounded: its fan-out is real, but the
|
|
153
151
|
* failure it invites is descriptor exhaustion, which no measurement on either
|
package/dist/core/locate.js
CHANGED
|
@@ -294,13 +294,11 @@ const PARSE_CONCURRENCY = 32;
|
|
|
294
294
|
* displayed, they become the child run's `include` globs, where a Windows
|
|
295
295
|
* separator would silently match nothing.
|
|
296
296
|
*
|
|
297
|
-
* Each source is parsed as it arrives rather than after all of them
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* people to run first on an untrusted fork MR.
|
|
302
|
-
* makes the peak `PARSE_CONCURRENCY` sources instead of `files.length`, and the
|
|
303
|
-
* plan is the only thing that still grows with the tree.
|
|
297
|
+
* Each source is parsed as it arrives rather than after all of them, so the peak
|
|
298
|
+
* is `PARSE_CONCURRENCY` sources rather than `files.length` and the plan is the
|
|
299
|
+
* only thing still growing with the tree. Reading them all first is the shape to
|
|
300
|
+
* avoid: input size is not ours to choose here, `check` being what this project
|
|
301
|
+
* tells people to run first on an untrusted fork MR. Measured in `[0.7.0]`.
|
|
304
302
|
*
|
|
305
303
|
* `findFiles` above is deliberately left unbounded: its fan-out is real, but the
|
|
306
304
|
* failure it invites is descriptor exhaustion, which no measurement on either
|
package/dist/core/merge.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// at runtime on the happy path, and a later reordering would look harmless.
|
|
31
31
|
import { mkdir, readFile, rename, stat } from 'node:fs/promises';
|
|
32
32
|
import { join, dirname, basename } from 'node:path';
|
|
33
|
-
import { repointImport, spliceRequirements } from './splice.js';
|
|
33
|
+
import { repointImport, spliceRequirements, UnwritableValue } from './splice.js';
|
|
34
34
|
import { writeAtomic } from './write.js';
|
|
35
35
|
import { idPrefix } from './locate.js';
|
|
36
36
|
import { addedIds } from './apply.js';
|
|
@@ -178,7 +178,32 @@ export async function applyMerge(input) {
|
|
|
178
178
|
}
|
|
179
179
|
for (const file of [...byFile.keys()].sort(byCodeUnit)) {
|
|
180
180
|
const source = await readFile(file, 'utf8');
|
|
181
|
-
|
|
181
|
+
let spliced;
|
|
182
|
+
try {
|
|
183
|
+
spliced = spliceRequirements(file, source, byFile.get(file));
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
// The emitter refused a value it cannot write as source — today only a
|
|
187
|
+
// `__proto__` param key, which the schema rejects before `--apply` runs.
|
|
188
|
+
// Caught rather than left to the CLI's crash envelope so the account of
|
|
189
|
+
// what this merge had already written survives: `--apply` is destructive
|
|
190
|
+
// and half a merge reported as a bare stack is the shape a resume cannot
|
|
191
|
+
// read. The write for *this* file has not happened — the throw is in the
|
|
192
|
+
// text generation, above `writeAtomic`.
|
|
193
|
+
if (!(err instanceof UnwritableValue))
|
|
194
|
+
throw err;
|
|
195
|
+
return {
|
|
196
|
+
issues: [
|
|
197
|
+
{
|
|
198
|
+
level: 'ERROR',
|
|
199
|
+
code: 'internal-error',
|
|
200
|
+
file: relativePath(root, file),
|
|
201
|
+
message: `${relativePath(root, file)} could not be written: ${err.message}.`,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
written,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
182
207
|
if (spliced === undefined) {
|
|
183
208
|
// Unreachable through the command — the gate read this file as a literal
|
|
184
209
|
// moments ago — so it is reported as the internal inconsistency it is
|
package/dist/core/order.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
/** Compare by UTF-16 code unit — the same order as a bare `Array#sort()`. */
|
|
2
2
|
export declare function byCodeUnit(a: string, b: string): number;
|
|
3
|
+
/**
|
|
4
|
+
* A JSON value with every object's keys in code-unit order, at every depth.
|
|
5
|
+
*
|
|
6
|
+
* `JSON.stringify` writes object keys in insertion order, which is *how the
|
|
7
|
+
* source happened to be written* — the thing this module exists to keep out of
|
|
8
|
+
* anything compared or committed. Two callers need the same guarantee for the
|
|
9
|
+
* same reason, one depth apart:
|
|
10
|
+
*
|
|
11
|
+
* - `apply.ts` canonicalises a requirement to decide `add-conflict`, so an
|
|
12
|
+
* identical copy written with its keys in another order must not read as a
|
|
13
|
+
* conflict with itself.
|
|
14
|
+
* - `render.ts` emits a structured param as JSON, and ATX-10 holds the same
|
|
15
|
+
* registry to the same bytes.
|
|
16
|
+
*
|
|
17
|
+
* Arrays keep their order: an array is data whose order is part of the value.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sortDeep<T>(value: T): T;
|
|
3
20
|
//# sourceMappingURL=order.d.ts.map
|