@am_shork/attest 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +846 -231
- package/README.md +14 -7
- package/dist/core/gate.d.ts +16 -6
- package/dist/core/gate.js +18 -8
- package/dist/core/red-record.d.ts +4 -4
- package/dist/core/red-record.js +12 -3
- package/dist/core/registry.d.ts +31 -1
- package/dist/core/registry.js +4 -0
- package/dist/core/skill.js +49 -28
- package/dist/core/splice.d.ts +20 -0
- package/dist/core/splice.js +25 -1
- package/dist/core/static-registry.js +81 -17
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -131,12 +131,19 @@ attest init --target cursor # .cursor/rules/attest.mdc
|
|
|
131
131
|
attest init --target copilot # .github/instructions/attest.instructions.md
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
"
|
|
135
|
-
each of a change's scenarios ended
|
|
136
|
-
`changes/<name>/first-run.json`, and blocks with `never-red` on
|
|
137
|
-
the delta adds whose scenarios were never seen to fail. Commit
|
|
138
|
-
with the change — it is the evidence, and CI has to reach the
|
|
139
|
-
you do.
|
|
134
|
+
"Every scenario has been seen to fail" is enforced rather than advised:
|
|
135
|
+
`archive` records how each of a change's scenarios ended in every run it
|
|
136
|
+
observes, into `changes/<name>/first-run.json`, and blocks with `never-red` on
|
|
137
|
+
any requirement the delta adds whose scenarios were never seen to fail. Commit
|
|
138
|
+
`first-run.json` with the change — it is the evidence, and CI has to reach the
|
|
139
|
+
same verdict as you do.
|
|
140
|
+
|
|
141
|
+
**It does not require you to write the test first.** A recorded failure is
|
|
142
|
+
permanent and a recorded pass is not, so a failure observed after the
|
|
143
|
+
implementation exists satisfies the gate exactly like one observed before it:
|
|
144
|
+
if you wrote the intent, the scenario and the code together, remove the
|
|
145
|
+
implementation, run `archive`, and put it back. What is enforced is that the
|
|
146
|
+
assertion can fail — not the order you worked in.
|
|
140
147
|
|
|
141
148
|
An agent loads the document on its own — its description is already in the
|
|
142
149
|
agent's context, or its path matches what you have open — so nothing has to be
|
|
@@ -152,7 +159,7 @@ Every diagnostic carries a `code`, and every code has a section in
|
|
|
152
159
|
```
|
|
153
160
|
ERROR registry-not-static (requirements/upload.reqs.ts:5)
|
|
154
161
|
Value is not a literal.
|
|
155
|
-
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.
|
|
162
|
+
→ https://gitlab.com/Pseudorca/attest/-/blob/v0.11.0/docs/en/troubleshooting.md#registry-not-static
|
|
156
163
|
```
|
|
157
164
|
|
|
158
165
|
The anchor **is** the code, so the link cannot point somewhere the section
|
package/dist/core/gate.d.ts
CHANGED
|
@@ -102,17 +102,27 @@ export declare function specLoadFailedIssues(run: RunResult, already?: ReadonlyS
|
|
|
102
102
|
*/
|
|
103
103
|
export declare function notRunIssues(plan: AttestPlan, run: RunResult, specific?: readonly Issue[]): Issue[];
|
|
104
104
|
/**
|
|
105
|
-
* Never-red: a scenario attesting a requirement this change ADDs,
|
|
106
|
-
* observed
|
|
105
|
+
* Never-red: a scenario attesting a requirement this change ADDs, which no run
|
|
106
|
+
* the gate observed has seen fail (design §6, mechanism 2).
|
|
107
107
|
*
|
|
108
108
|
* This is the half of the gate that certifies the tests ever *discriminated*.
|
|
109
109
|
* Green at archive time says only that they pass now — a scenario that asserted
|
|
110
110
|
* nothing has been green since the commit that added it and clears every other
|
|
111
111
|
* check identically. The two states are reported apart because the fixes differ:
|
|
112
|
-
* a
|
|
113
|
-
* no observation at all means
|
|
114
|
-
*
|
|
115
|
-
*
|
|
112
|
+
* a scenario observed passing and never failing does not test what it claims,
|
|
113
|
+
* while no observation at all means no run has been taken yet, and the remedy
|
|
114
|
+
* for the second is to take one rather than to rewrite anything.
|
|
115
|
+
*
|
|
116
|
+
* **The obligation is discrimination, and it says nothing about ordering.** The
|
|
117
|
+
* record is monotonic toward failure but a pass is provisional (`red-record.ts`),
|
|
118
|
+
* so a failure observed *after* the implementation exists replaces a recorded
|
|
119
|
+
* pass and satisfies this exactly like one observed before it. Running the gate
|
|
120
|
+
* first is the cheapest route to the evidence and not the only one: removing the
|
|
121
|
+
* implementation, running the gate, and restoring it produces the same fact by
|
|
122
|
+
* the same command. Nothing here could enforce an order anyway — a stage-1 red
|
|
123
|
+
* can come from a typo in the spec rather than from the requirement being
|
|
124
|
+
* unimplemented, so an earlier observation was never stronger evidence, only
|
|
125
|
+
* earlier.
|
|
116
126
|
*
|
|
117
127
|
* Scoped to ADDED ids only. A requirement written straight into the base
|
|
118
128
|
* registry — which is how existing behaviour gets described, in a brownfield
|
package/dist/core/gate.js
CHANGED
|
@@ -132,17 +132,27 @@ export function notRunIssues(plan, run, specific = []) {
|
|
|
132
132
|
];
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
135
|
-
* Never-red: a scenario attesting a requirement this change ADDs,
|
|
136
|
-
* observed
|
|
135
|
+
* Never-red: a scenario attesting a requirement this change ADDs, which no run
|
|
136
|
+
* the gate observed has seen fail (design §6, mechanism 2).
|
|
137
137
|
*
|
|
138
138
|
* This is the half of the gate that certifies the tests ever *discriminated*.
|
|
139
139
|
* Green at archive time says only that they pass now — a scenario that asserted
|
|
140
140
|
* nothing has been green since the commit that added it and clears every other
|
|
141
141
|
* check identically. The two states are reported apart because the fixes differ:
|
|
142
|
-
* a
|
|
143
|
-
* no observation at all means
|
|
144
|
-
*
|
|
145
|
-
*
|
|
142
|
+
* a scenario observed passing and never failing does not test what it claims,
|
|
143
|
+
* while no observation at all means no run has been taken yet, and the remedy
|
|
144
|
+
* for the second is to take one rather than to rewrite anything.
|
|
145
|
+
*
|
|
146
|
+
* **The obligation is discrimination, and it says nothing about ordering.** The
|
|
147
|
+
* record is monotonic toward failure but a pass is provisional (`red-record.ts`),
|
|
148
|
+
* so a failure observed *after* the implementation exists replaces a recorded
|
|
149
|
+
* pass and satisfies this exactly like one observed before it. Running the gate
|
|
150
|
+
* first is the cheapest route to the evidence and not the only one: removing the
|
|
151
|
+
* implementation, running the gate, and restoring it produces the same fact by
|
|
152
|
+
* the same command. Nothing here could enforce an order anyway — a stage-1 red
|
|
153
|
+
* can come from a typo in the spec rather than from the requirement being
|
|
154
|
+
* unimplemented, so an earlier observation was never stronger evidence, only
|
|
155
|
+
* earlier.
|
|
146
156
|
*
|
|
147
157
|
* Scoped to ADDED ids only. A requirement written straight into the base
|
|
148
158
|
* registry — which is how existing behaviour gets described, in a brownfield
|
|
@@ -165,8 +175,8 @@ export function neverRedIssues(plan, addedIds, firstRun) {
|
|
|
165
175
|
file: s.file,
|
|
166
176
|
line: s.line,
|
|
167
177
|
message: outcome === 'pass'
|
|
168
|
-
? `scenario "${s.name}"
|
|
169
|
-
: `scenario "${s.name}" has no
|
|
178
|
+
? `scenario "${s.name}" was observed passing and never failing, so it has not shown that it can fail without ${s.reqId} being implemented; give it an assertion that discriminates and run the gate again — a later failing run replaces this record.`
|
|
179
|
+
: `scenario "${s.name}" has no observed run recorded for ${s.reqId}; run \`attest archive\` with the implementation absent — before writing it, or with it removed — so ${RED_RECORD_FILE} records the scenario failing.`,
|
|
170
180
|
});
|
|
171
181
|
}
|
|
172
182
|
return issues;
|
|
@@ -18,11 +18,11 @@ export declare function redRecordPath(root: string, changeName: string): string;
|
|
|
18
18
|
/** The file name, exported so a diagnostic can name it without rebuilding it. */
|
|
19
19
|
export declare const RED_RECORD_FILE = "first-run.json";
|
|
20
20
|
/**
|
|
21
|
-
* reqId -> spec file -> scenario name -> the outcome
|
|
21
|
+
* reqId -> spec file -> scenario name -> the strongest outcome observed for it.
|
|
22
22
|
*
|
|
23
|
-
* Deliberately not a list of "red scenarios": a scenario
|
|
24
|
-
* is the case mechanism 2 exists to catch, so it has to be recorded as a
|
|
25
|
-
* rather than as an absence. An absence then means only one thing — never
|
|
23
|
+
* Deliberately not a list of "red scenarios": a scenario only ever observed
|
|
24
|
+
* passing is the case mechanism 2 exists to catch, so it has to be recorded as a
|
|
25
|
+
* fact rather than as an absence. An absence then means only one thing — never
|
|
26
26
|
* observed at all — and the gate can report the two separately.
|
|
27
27
|
*
|
|
28
28
|
* **The file level is what identifies a scenario, and it was missing.** A
|
package/dist/core/red-record.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
// Mechanism 2's record:
|
|
2
|
-
//
|
|
1
|
+
// Mechanism 2's record: the strongest outcome the gate has ever observed for
|
|
2
|
+
// each of a change's scenarios (design §6).
|
|
3
|
+
//
|
|
4
|
+
// Not "the first run", which is what the file is named after and what this was
|
|
5
|
+
// once described as. The record is revised — see the asymmetry below — and the
|
|
6
|
+
// difference is the whole reason the *order* of the observations is free: an
|
|
7
|
+
// author who has already written the implementation earns the evidence by
|
|
8
|
+
// removing it, running the gate, and restoring it. The file keeps its name
|
|
9
|
+
// because archived records already carry it (`inspect.ts`) and a rename would
|
|
10
|
+
// spend that to fix a word.
|
|
3
11
|
//
|
|
4
12
|
// §8's gate requires green at archive time. That certifies "these tests pass",
|
|
5
13
|
// not "these tests ever discriminated" — a scenario that asserted nothing has
|
|
@@ -36,7 +44,8 @@
|
|
|
36
44
|
// change red. Once a scenario has been seen to fail, nothing can take it back.
|
|
37
45
|
//
|
|
38
46
|
// Provisional pass: plain first-write-wins was the first design here, and it
|
|
39
|
-
// makes the defect this mechanism exists to catch *unrecoverable
|
|
47
|
+
// makes the defect this mechanism exists to catch *unrecoverable*, and it is
|
|
48
|
+
// also what would have made the ordering real. A scenario
|
|
40
49
|
// that passed on its first run is one that asserts nothing; the fix is to give
|
|
41
50
|
// it a real assertion and watch it fail — but under first-write-wins that
|
|
42
51
|
// observation could never be recorded, so the only way out was deleting the
|
package/dist/core/registry.d.ts
CHANGED
|
@@ -115,8 +115,34 @@ type ProposedRequirements<T extends RegistryDelta> = T extends {
|
|
|
115
115
|
* helper over a whole registry; the ids a single change adds are few and known
|
|
116
116
|
* at the call site, so the stricter shape costs nothing and rejects a mistyped
|
|
117
117
|
* id outright instead of leaving it to `check`.
|
|
118
|
+
*
|
|
119
|
+
* **`added` is withheld from the type although the value still carries it**, and
|
|
120
|
+
* that is the one place this shape deliberately understates its own object. Both
|
|
121
|
+
* `reqs['AUTH-7'].params.x` and `d.added['AUTH-7'].params.x` are correct during
|
|
122
|
+
* stage 1; only the first survives `--apply`, which repoints the specifier at
|
|
123
|
+
* the merged registry, and a registry has no `added`. Nothing else catches that:
|
|
124
|
+
* the gate runs before the rename, `check` executes nothing, and the compiler
|
|
125
|
+
* sees whichever half is on disk — so the difference appears one command later,
|
|
126
|
+
* as a `spec-load-failed` naming the file and not the cause.
|
|
127
|
+
*
|
|
128
|
+
* The optional `never` rather than a bare `Omit` is load-bearing.
|
|
129
|
+
* `RegistryDelta`'s four fields are all optional, so an added-only delta with
|
|
130
|
+
* `added` removed has *no* property in common with it and TypeScript's weak-type
|
|
131
|
+
* check refuses `applyDelta(base, delta({ added: … }))` outright — nine call
|
|
132
|
+
* sites in this tree, none of them wrong. An optional property restores the
|
|
133
|
+
* overlap while still refusing the read, and the refusal survives the escape
|
|
134
|
+
* hatches that would make it advisory: `d.added!` and `d.added?.[…]` both land
|
|
135
|
+
* on `never`, and only an explicit `as any` compiles.
|
|
136
|
+
*
|
|
137
|
+
* `modified`, `removed` and `renamed` are left alone on purpose. They vanish at
|
|
138
|
+
* the merge too, but none of them has a by-id equivalent — there is no correct
|
|
139
|
+
* spelling to redirect an author to, so a spec reading one is reading delta-only
|
|
140
|
+
* data rather than misspelling a right thing, which is a different defect and
|
|
141
|
+
* has never been reported.
|
|
118
142
|
*/
|
|
119
|
-
export type DefinedDelta<T extends RegistryDelta> = T &
|
|
143
|
+
export type DefinedDelta<T extends RegistryDelta> = Omit<T, 'added'> & {
|
|
144
|
+
added?: never;
|
|
145
|
+
} & ProposedRequirements<T>;
|
|
120
146
|
/**
|
|
121
147
|
* Declare a registry delta for a change (design §7). Ordered idempotent
|
|
122
148
|
* application (RENAMED -> REMOVED -> MODIFIED -> ADDED) lives in applyDelta.
|
|
@@ -130,6 +156,10 @@ export type DefinedDelta<T extends RegistryDelta> = T & ProposedRequirements<T>;
|
|
|
130
156
|
* exists to reward — and then billed them the rewrite at merge for having
|
|
131
157
|
* complied. Now merging a spec changes its import and nothing else.
|
|
132
158
|
*
|
|
159
|
+
* That rejected expression is now rejected by the compiler rather than by
|
|
160
|
+
* this comment — {@link DefinedDelta} withholds `added`, because it was the
|
|
161
|
+
* one wrong spelling that stage 1 could not tell from the right one.
|
|
162
|
+
*
|
|
133
163
|
* Still no validation, deliberately. A delta carrying an id the registry would
|
|
134
164
|
* refuse is an `add-invalid` / `rename-target-invalid` ERROR from the gate
|
|
135
165
|
* (ATX-41), which is where a reviewer sees it; throwing here would move that
|
package/dist/core/registry.js
CHANGED
|
@@ -48,6 +48,10 @@ export function defineRequirements(input) {
|
|
|
48
48
|
* exists to reward — and then billed them the rewrite at merge for having
|
|
49
49
|
* complied. Now merging a spec changes its import and nothing else.
|
|
50
50
|
*
|
|
51
|
+
* That rejected expression is now rejected by the compiler rather than by
|
|
52
|
+
* this comment — {@link DefinedDelta} withholds `added`, because it was the
|
|
53
|
+
* one wrong spelling that stage 1 could not tell from the right one.
|
|
54
|
+
*
|
|
51
55
|
* Still no validation, deliberately. A delta carrying an id the registry would
|
|
52
56
|
* refuse is an `add-invalid` / `rename-target-invalid` ERROR from the gate
|
|
53
57
|
* (ATX-41), which is where a reviewer sees it; throwing here would move that
|
package/dist/core/skill.js
CHANGED
|
@@ -289,40 +289,59 @@ one stage where the assertion is being authored against a value nobody has
|
|
|
289
289
|
implemented yet. MODIFIED ids are deliberately not readable this way — the end
|
|
290
290
|
state is the base entry with the patch applied, and the base is not in that file.
|
|
291
291
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
Reading a proposed requirement through \`added\` — \`d.added['AUTH-7'].params.x\` —
|
|
293
|
+
is refused by the compiler rather than by this paragraph. It is correct today and
|
|
294
|
+
wrong one command later, because \`--apply\` repoints the import at the registry
|
|
295
|
+
and a registry has no \`added\`; the by-id form above is the one that survives.
|
|
296
|
+
|
|
297
|
+
**Each added scenario must be observed failing, once.** Every added requirement
|
|
298
|
+
needs at least one scenario in a \`*.proposed.spec.ts\`, and the gate has to see
|
|
299
|
+
each of them fail at least once before it will archive the change. Take the
|
|
300
|
+
observation:
|
|
295
301
|
|
|
296
302
|
\`\`\`
|
|
297
303
|
attest archive <name> --json # expect a failing verdict carrying tests-red
|
|
298
304
|
\`\`\`
|
|
299
305
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
306
|
+
That run writes \`changes/<name>/first-run.json\`, recording how each of the
|
|
307
|
+
change's scenarios ended, and the archive gate later refuses any added
|
|
308
|
+
requirement whose scenarios have no recorded failing run (\`never-red\`). A
|
|
309
|
+
scenario that has only ever been seen green does not clear the gate — if it
|
|
310
|
+
passes while the behaviour it describes is absent, it asserts nothing real.
|
|
311
|
+
|
|
312
|
+
**The obligation is that the scenario can fail. It is not an obligation to write
|
|
313
|
+
it first.** The run above is the cheapest way to take the observation, because at
|
|
314
|
+
this point in stage 1 nothing is implemented and the scenarios are red by
|
|
315
|
+
construction. It is not the only way, and the record is what makes the other one
|
|
316
|
+
work: a recorded failure is permanent, a recorded pass is provisional, so a
|
|
317
|
+
failure seen later replaces a pass seen earlier. Two routes, one fact:
|
|
318
|
+
|
|
319
|
+
- **Nothing implemented yet** — run the command above and the record is taken.
|
|
320
|
+
This is the shape stage 1 is written around.
|
|
321
|
+
- **The implementation already exists** — you wrote the intent, the scenario and
|
|
322
|
+
the code in one pass, which is a perfectly good way to work. Remove the
|
|
323
|
+
implementation, run the command above, then restore it. The record that comes
|
|
324
|
+
back is the same evidence obtained by the same command, and the restored code
|
|
325
|
+
makes it green again without erasing it.
|
|
326
|
+
|
|
327
|
+
Do not reorder work you have already done to satisfy this; take the observation
|
|
328
|
+
the second way. What no route excuses is a scenario that stays green with its
|
|
329
|
+
subject removed — that is the defect \`never-red\` exists to name, and it is
|
|
330
|
+
visible only if you actually take the observation.
|
|
331
|
+
|
|
332
|
+
A recorded pass is not a dead end either: give the scenario an assertion that
|
|
333
|
+
discriminates and run the gate again, and the real failure replaces it.
|
|
316
334
|
|
|
317
335
|
Commit \`first-run.json\` with the change. It is evidence a reviewer reads, and
|
|
318
336
|
the gate has to reach the same verdict on a CI checkout as on your machine.
|
|
319
337
|
|
|
320
338
|
**Never write or edit that file yourself.** The gate trusts it and cannot check
|
|
321
|
-
it: everything else Attest writes can be regenerated
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
339
|
+
it: everything else Attest writes can be regenerated from the tree as it is and
|
|
340
|
+
compared, and this cannot — a green tree says nothing about whether a scenario
|
|
341
|
+
could ever have failed, which is the whole reason the record exists. Producing
|
|
342
|
+
it any way other than by running the gate destroys the only thing it is for. If
|
|
343
|
+
it says something you did not expect, take the observation again — remove the
|
|
344
|
+
behaviour and run the gate; a real failing run replaces a recorded pass.
|
|
326
345
|
|
|
327
346
|
To see where the change stands at any point, without paying for a run:
|
|
328
347
|
|
|
@@ -347,8 +366,9 @@ still not a verdict — \`attest archive\` is the only thing that decides.
|
|
|
347
366
|
- **What pins each expectation** — a fixture, a literal, an independently
|
|
348
367
|
derived value; never the param the code under test consumed?
|
|
349
368
|
- **Does each statement carry one obligation**, or several under one SHALL?
|
|
350
|
-
- **Was
|
|
351
|
-
|
|
369
|
+
- **Was each scenario ever observed failing** — with its subject absent, whether
|
|
370
|
+
because it was not written yet or because you removed it? \`never-red\` asks
|
|
371
|
+
for that on the ids you ADD, and asks nothing about when you took it.
|
|
352
372
|
|
|
353
373
|
Then present the proposal, the ids, and the red output, and **stop**. Wait for
|
|
354
374
|
agreement before implementing.
|
|
@@ -442,8 +462,9 @@ failure this framework exists to make visible:
|
|
|
442
462
|
identical content is a no-op.
|
|
443
463
|
4. **Do not write or edit \`first-run.json\`.** It is the one file here the gate
|
|
444
464
|
trusts without being able to check it, so a hand-written \`"fail"\` clears
|
|
445
|
-
\`never-red\` while proving nothing at all. Run the gate
|
|
446
|
-
that is what produces the record honestly
|
|
465
|
+
\`never-red\` while proving nothing at all. Run the gate against a tree where
|
|
466
|
+
the behaviour is absent — that is what produces the record honestly, and it
|
|
467
|
+
is the one step here nothing else can stand in for.
|
|
447
468
|
|
|
448
469
|
### When the gate passes
|
|
449
470
|
|
package/dist/core/splice.d.ts
CHANGED
|
@@ -107,6 +107,26 @@ export declare function spliceModifications(file: string, source: string, change
|
|
|
107
107
|
* property of its module resolution, uniform across the project, and already
|
|
108
108
|
* answered by the specifier sitting in front of us — the third spelling
|
|
109
109
|
* included, where the answer is "none".
|
|
110
|
+
*
|
|
111
|
+
* **A source that does not compile is returned untouched** — ATX-69, over the
|
|
112
|
+
* one writer that reaches the compiler directly. `ts.createSourceFile` recovers:
|
|
113
|
+
* handed a file that does not compile it returns a tree built from what the
|
|
114
|
+
* parser guessed, and every offset below comes out of that tree and is written
|
|
115
|
+
* back over the bytes of a `*.spec.ts` the user cannot regenerate. The two other
|
|
116
|
+
* writers in this file ask the same question through the reader that hands them
|
|
117
|
+
* their offsets; this one has to ask it itself.
|
|
118
|
+
*
|
|
119
|
+
* It is unreachable through the command today — the gate refuses a spec it
|
|
120
|
+
* could not parse before a merge starts (`pipeline.ts`) — and that is exactly
|
|
121
|
+
* why it is checked here: a defence that holds only because something upstream
|
|
122
|
+
* holds is not a defence, which is the standard `keySource` above is written to.
|
|
123
|
+
*
|
|
124
|
+
* The refusal is the source unchanged rather than a throw or an issue, because
|
|
125
|
+
* there is no failure channel to use and this function's whole discipline is
|
|
126
|
+
* that it touches one string literal or nothing. `applyMerge` writes only when
|
|
127
|
+
* the text moved, so an unparseable spec is renamed with the import it already
|
|
128
|
+
* had — which is the state a re-run finds and reports, rather than a file
|
|
129
|
+
* rewritten at offsets nobody can trust.
|
|
110
130
|
*/
|
|
111
131
|
export declare function repointImport(file: string, source: string, from: string, to: string): string;
|
|
112
132
|
//# sourceMappingURL=splice.d.ts.map
|
package/dist/core/splice.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
// reach this file because the gate validated the delta before `--apply` ran.
|
|
35
35
|
import ts from 'typescript';
|
|
36
36
|
import { dirname, relative, resolve } from 'node:path';
|
|
37
|
+
import { parseSource } from './compiler.js';
|
|
37
38
|
import { registryEntryLayouts, registryInsertionPoint } from './static-registry.js';
|
|
38
39
|
import { toPosixPath } from './paths.js';
|
|
39
40
|
import { byCodeUnit, sortDeep } from './order.js';
|
|
@@ -470,9 +471,32 @@ function sameValue(a, b) {
|
|
|
470
471
|
* property of its module resolution, uniform across the project, and already
|
|
471
472
|
* answered by the specifier sitting in front of us — the third spelling
|
|
472
473
|
* included, where the answer is "none".
|
|
474
|
+
*
|
|
475
|
+
* **A source that does not compile is returned untouched** — ATX-69, over the
|
|
476
|
+
* one writer that reaches the compiler directly. `ts.createSourceFile` recovers:
|
|
477
|
+
* handed a file that does not compile it returns a tree built from what the
|
|
478
|
+
* parser guessed, and every offset below comes out of that tree and is written
|
|
479
|
+
* back over the bytes of a `*.spec.ts` the user cannot regenerate. The two other
|
|
480
|
+
* writers in this file ask the same question through the reader that hands them
|
|
481
|
+
* their offsets; this one has to ask it itself.
|
|
482
|
+
*
|
|
483
|
+
* It is unreachable through the command today — the gate refuses a spec it
|
|
484
|
+
* could not parse before a merge starts (`pipeline.ts`) — and that is exactly
|
|
485
|
+
* why it is checked here: a defence that holds only because something upstream
|
|
486
|
+
* holds is not a defence, which is the standard `keySource` above is written to.
|
|
487
|
+
*
|
|
488
|
+
* The refusal is the source unchanged rather than a throw or an issue, because
|
|
489
|
+
* there is no failure channel to use and this function's whole discipline is
|
|
490
|
+
* that it touches one string literal or nothing. `applyMerge` writes only when
|
|
491
|
+
* the text moved, so an unparseable spec is renamed with the import it already
|
|
492
|
+
* had — which is the state a re-run finds and reports, rather than a file
|
|
493
|
+
* rewritten at offsets nobody can trust.
|
|
473
494
|
*/
|
|
474
495
|
export function repointImport(file, source, from, to) {
|
|
475
|
-
const
|
|
496
|
+
const parsed = parseSource(file, source);
|
|
497
|
+
if ('error' in parsed)
|
|
498
|
+
return source;
|
|
499
|
+
const sf = parsed.sf;
|
|
476
500
|
const dir = dirname(file);
|
|
477
501
|
const edits = [];
|
|
478
502
|
for (const statement of sf.statements) {
|
|
@@ -217,7 +217,7 @@ export function readDeltaSource(file, source) {
|
|
|
217
217
|
*/
|
|
218
218
|
export function declaredIdsFromSource(file, source) {
|
|
219
219
|
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
220
|
-
const names =
|
|
220
|
+
const names = authoringNames(sf, DEFINE);
|
|
221
221
|
const ids = new Set();
|
|
222
222
|
// Any `defineRequirements({ … })` in the file, not only the exported one: the
|
|
223
223
|
// failure being diagnosed is often that the call is not where it should be —
|
|
@@ -309,41 +309,105 @@ function constInitializer(sf, name) {
|
|
|
309
309
|
function authoringCall(expr, sf, fn) {
|
|
310
310
|
if (!ts.isCallExpression(expr) || expr.arguments.length !== 1)
|
|
311
311
|
return undefined;
|
|
312
|
-
if (!callsAuthoringFn(expr,
|
|
312
|
+
if (!callsAuthoringFn(expr, authoringNames(sf, fn), fn))
|
|
313
313
|
return undefined;
|
|
314
314
|
return unwrap(expr.arguments[0]);
|
|
315
315
|
}
|
|
316
316
|
/**
|
|
317
|
-
* Whether a call is a call of `fn`, by the
|
|
317
|
+
* Whether a call is a call of `fn`, by what the file binds the callee to.
|
|
318
318
|
*
|
|
319
319
|
* One predicate for both readers of this question — the extraction above and the
|
|
320
320
|
* id recovery below — because they must not be able to disagree about what
|
|
321
321
|
* counts as the authoring call. An alias rule taught to one and not the other
|
|
322
322
|
* would make recovery silently miss exactly the files the reader refuses.
|
|
323
|
+
*
|
|
324
|
+
* **A name this file binds to something other than an import is not `fn`.**
|
|
325
|
+
* Matching by spelling alone lets a file hand the two readers different
|
|
326
|
+
* registries while contradicting itself in plain sight — `const
|
|
327
|
+
* defineRequirements = (r) => ({ ...r, 'ADMIN-1': … })` in front of the export
|
|
328
|
+
* leaves this reader with the literal argument and the evaluator with whatever
|
|
329
|
+
* that function returns. A wrong answer rather than a refusal, of the shape the
|
|
330
|
+
* `let` guard above is kept for, on the commands a reviewer runs *because* they
|
|
331
|
+
* execute nothing (design §5.2, ATX-16).
|
|
332
|
+
*
|
|
333
|
+
* So the rule is about the binding: an import binding is trusted, a name this
|
|
334
|
+
* file never binds is trusted (it comes from a global or a harness, which is
|
|
335
|
+
* what the bare name is for), and a `const`/`let`/`var`/`function`/`class` in
|
|
336
|
+
* this file is not — whether the callee is that name or a property reached
|
|
337
|
+
* through it. Refusing is all that is needed: the file becomes
|
|
338
|
+
* `registry-not-static` with `--eval` named as the way back, which is ATX-17
|
|
339
|
+
* unchanged.
|
|
340
|
+
*
|
|
341
|
+
* **What this still cannot see is the module graph**, and it is not written to:
|
|
342
|
+
* `import { defineRequirements } from './wrapper.js'` binds the name by an
|
|
343
|
+
* import, so it is accepted, and what that module exports is not a question the
|
|
344
|
+
* source text of *this* file answers. Resolving it would mean reading — and
|
|
345
|
+
* then trusting — another file, which is the evaluator's job and the reason
|
|
346
|
+
* `--eval` exists. The line drawn here is what the file contradicts about itself.
|
|
323
347
|
*/
|
|
324
348
|
function callsAuthoringFn(call, names, fn) {
|
|
325
349
|
const callee = call.expression;
|
|
326
|
-
|
|
327
|
-
|
|
350
|
+
if (ts.isIdentifier(callee))
|
|
351
|
+
return names.called.has(callee.text);
|
|
352
|
+
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== fn)
|
|
353
|
+
return false;
|
|
354
|
+
// `attest.defineRequirements(…)`. The object is held to the same rule as the
|
|
355
|
+
// bare name, which is what keeps the two forms from having two answers: a
|
|
356
|
+
// namespace import is trusted, a name this file never binds is trusted, and
|
|
357
|
+
// anything this file built is not. A deeper access — `a.b.defineRequirements`
|
|
358
|
+
// — is not an identifier and so is not one of those, which no namespace
|
|
359
|
+
// import can produce anyway.
|
|
360
|
+
const object = callee.expression;
|
|
361
|
+
return ts.isIdentifier(object) && !names.declared.has(object.text);
|
|
328
362
|
}
|
|
329
|
-
/**
|
|
330
|
-
function
|
|
331
|
-
const
|
|
363
|
+
/** How the file binds the names a callee could be written with. */
|
|
364
|
+
function authoringNames(sf, fn) {
|
|
365
|
+
const called = new Set();
|
|
366
|
+
const declared = new Set();
|
|
332
367
|
for (const statement of sf.statements) {
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
368
|
+
if (ts.isImportDeclaration(statement)) {
|
|
369
|
+
const bindings = statement.importClause?.namedBindings;
|
|
370
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
371
|
+
for (const el of bindings.elements) {
|
|
372
|
+
if ((el.propertyName ?? el.name).text === fn)
|
|
373
|
+
called.add(el.name.text);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
337
376
|
continue;
|
|
338
|
-
for (const el of bindings.elements) {
|
|
339
|
-
if ((el.propertyName ?? el.name).text === fn)
|
|
340
|
-
names.add(el.name.text);
|
|
341
377
|
}
|
|
378
|
+
for (const name of declaredNames(statement))
|
|
379
|
+
declared.add(name);
|
|
342
380
|
}
|
|
343
381
|
// A file that never imported it can still be read: the import may come from a
|
|
344
382
|
// global or a test harness, and the callee name is the only signal we need.
|
|
345
|
-
|
|
346
|
-
|
|
383
|
+
// Withdrawn exactly when the file itself says the name is something else.
|
|
384
|
+
if (!declared.has(fn))
|
|
385
|
+
called.add(fn);
|
|
386
|
+
return { called, declared };
|
|
387
|
+
}
|
|
388
|
+
/** The module-scope names one statement binds, if it is not an import. */
|
|
389
|
+
function* declaredNames(statement) {
|
|
390
|
+
if (ts.isVariableStatement(statement)) {
|
|
391
|
+
for (const decl of statement.declarationList.declarations)
|
|
392
|
+
yield* bindingNames(decl.name);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) {
|
|
396
|
+
yield statement.name.text;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** Every name a binding introduces, destructuring included. */
|
|
400
|
+
function* bindingNames(name) {
|
|
401
|
+
if (ts.isIdentifier(name)) {
|
|
402
|
+
yield name.text;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
// `const { defineRequirements } = evil;` binds the name as surely as an
|
|
406
|
+
// assignment does, and reads less like a shadow rather than more.
|
|
407
|
+
for (const element of name.elements) {
|
|
408
|
+
if (ts.isBindingElement(element))
|
|
409
|
+
yield* bindingNames(element.name);
|
|
410
|
+
}
|
|
347
411
|
}
|
|
348
412
|
/** Strip the type-level wrappers that do not change the value (`as const`, `satisfies`, parens). */
|
|
349
413
|
function unwrap(node) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@am_shork/attest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"attest": "node bin/attest.js",
|
|
57
57
|
"check:self": "node bin/attest.js check self",
|
|
58
58
|
"verify:self": "node bin/attest.js verify self",
|
|
59
|
+
"debug:self": "vitest run --config vitest.self.config.ts",
|
|
59
60
|
"cover:self": "node bin/attest.js cover self",
|
|
60
61
|
"render:self": "node bin/attest.js render self --out self/requirements/SPEC.md",
|
|
61
62
|
"render:self:check": "node bin/attest.js render self --out self/requirements/SPEC.md --check"
|