@am_shork/attest 1.0.0 → 1.1.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 +611 -205
- package/README.md +1 -1
- package/dist/core/docs.d.ts +1 -1
- package/dist/core/docs.js +1 -0
- package/dist/core/gate.d.ts +21 -0
- package/dist/core/gate.js +27 -1
- package/dist/core/pipeline.js +42 -3
- package/dist/core/runner.js +34 -1
- package/dist/core/skill.js +32 -11
- package/dist/core/targets.d.ts +29 -0
- package/dist/core/targets.js +46 -0
- package/dist/core/types.d.ts +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ Every diagnostic carries a `code`, and every code has a section in
|
|
|
159
159
|
```
|
|
160
160
|
ERROR registry-not-static (requirements/upload.reqs.ts:5)
|
|
161
161
|
Value is not a literal.
|
|
162
|
-
→ https://gitlab.com/Pseudorca/attest/-/blob/v1.
|
|
162
|
+
→ https://gitlab.com/Pseudorca/attest/-/blob/v1.1.0/docs/en/troubleshooting.md#registry-not-static
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
The anchor **is** the code, so the link cannot point somewhere the section
|
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", "non-scalar-interpolation", "open-unresolved", "orphan-from-failed-registry", "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", "requirement-open", "spec-in-change-dir", "spec-load-failed", "stale-spec-doc", "tests-red", "unbound-param", "uncovered-requirement", "unknown-target", "unreadable-file", "unsafe-target-path"];
|
|
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", "open-unresolved", "orphan-from-failed-registry", "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", "requirement-open", "spec-in-change-dir", "spec-load-failed", "stale-spec-doc", "stale-workflow-doc", "tests-red", "unbound-param", "uncovered-requirement", "unknown-target", "unreadable-file", "unsafe-target-path"];
|
|
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/gate.d.ts
CHANGED
|
@@ -101,6 +101,27 @@ export declare function specLoadFailedIssues(run: RunResult, already?: ReadonlyS
|
|
|
101
101
|
* Those files are left alone here, so one file carries one finding.
|
|
102
102
|
*/
|
|
103
103
|
export declare function notRunIssues(plan: AttestPlan, run: RunResult, specific?: readonly Issue[]): Issue[];
|
|
104
|
+
/**
|
|
105
|
+
* What `tests-red` adds when the clock, rather than an assertion, is what the
|
|
106
|
+
* run disagreed about. Empty when nothing reached the ceiling, so an ordinary
|
|
107
|
+
* red suite reads exactly as it always has.
|
|
108
|
+
*
|
|
109
|
+
* Here rather than at either call site because both commands that run a suite
|
|
110
|
+
* ask it, which is the same reason `notRunIssues` is here — and here rather
|
|
111
|
+
* than in `runner.ts` because that module names `vitest` and this one must not
|
|
112
|
+
* acquire it (`tests/import-boundary.spec.ts`).
|
|
113
|
+
*
|
|
114
|
+
* **Why a report says this at all.** The standing answer to a red `verify` is
|
|
115
|
+
* to run the suite yourself under your own reporter, and that answer is
|
|
116
|
+
* circular in exactly the case that needs it: when the direct run is green, the
|
|
117
|
+
* disagreement between the two runs *is* the failure, and reproducing it
|
|
118
|
+
* reproduces nothing. The child run is isolated, so it drops the config that
|
|
119
|
+
* would have raised the timeout — which makes "did anything run out the clock"
|
|
120
|
+
* the one question the direct run cannot answer and this one can. It is a count
|
|
121
|
+
* rather than a list because naming the scenarios is the pass-through decision,
|
|
122
|
+
* which is open and is not this.
|
|
123
|
+
*/
|
|
124
|
+
export declare function timeoutClause(run: RunResult): string;
|
|
104
125
|
/**
|
|
105
126
|
* Never-red: a scenario attesting a requirement this change ADDs, which no run
|
|
106
127
|
* the gate observed has seen fail (design §6, mechanism 2).
|
package/dist/core/gate.js
CHANGED
|
@@ -131,6 +131,32 @@ export function notRunIssues(plan, run, specific = []) {
|
|
|
131
131
|
...declaredNotRunIssues(plan, run).filter((i) => !i.file || !diagnosed.has(i.file)),
|
|
132
132
|
];
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* What `tests-red` adds when the clock, rather than an assertion, is what the
|
|
136
|
+
* run disagreed about. Empty when nothing reached the ceiling, so an ordinary
|
|
137
|
+
* red suite reads exactly as it always has.
|
|
138
|
+
*
|
|
139
|
+
* Here rather than at either call site because both commands that run a suite
|
|
140
|
+
* ask it, which is the same reason `notRunIssues` is here — and here rather
|
|
141
|
+
* than in `runner.ts` because that module names `vitest` and this one must not
|
|
142
|
+
* acquire it (`tests/import-boundary.spec.ts`).
|
|
143
|
+
*
|
|
144
|
+
* **Why a report says this at all.** The standing answer to a red `verify` is
|
|
145
|
+
* to run the suite yourself under your own reporter, and that answer is
|
|
146
|
+
* circular in exactly the case that needs it: when the direct run is green, the
|
|
147
|
+
* disagreement between the two runs *is* the failure, and reproducing it
|
|
148
|
+
* reproduces nothing. The child run is isolated, so it drops the config that
|
|
149
|
+
* would have raised the timeout — which makes "did anything run out the clock"
|
|
150
|
+
* the one question the direct run cannot answer and this one can. It is a count
|
|
151
|
+
* rather than a list because naming the scenarios is the pass-through decision,
|
|
152
|
+
* which is open and is not this.
|
|
153
|
+
*/
|
|
154
|
+
export function timeoutClause(run) {
|
|
155
|
+
if (run.timedOut === 0)
|
|
156
|
+
return '';
|
|
157
|
+
const subject = run.timedOut === 1 ? '1 scenario' : `${run.timedOut} scenarios`;
|
|
158
|
+
return ` ${subject} reached the ${run.testTimeout} ms timeout this run applied, rather than failing an assertion.`;
|
|
159
|
+
}
|
|
134
160
|
/**
|
|
135
161
|
* Never-red: a scenario attesting a requirement this change ADDs, which no run
|
|
136
162
|
* the gate observed has seen fail (design §6, mechanism 2).
|
|
@@ -227,7 +253,7 @@ export function evaluateGate({ registry, plan, run, addedIds, unmergedAddedIds,
|
|
|
227
253
|
blocking.push({
|
|
228
254
|
level: 'ERROR',
|
|
229
255
|
code: 'tests-red',
|
|
230
|
-
message:
|
|
256
|
+
message: `Some tests are failing; the change cannot be archived.${timeoutClause(run)}`,
|
|
231
257
|
});
|
|
232
258
|
}
|
|
233
259
|
// 3) and 4) in one push, because the order between them is not this
|
package/dist/core/pipeline.js
CHANGED
|
@@ -6,10 +6,10 @@ import { byCodeUnit } from './order.js';
|
|
|
6
6
|
import { applyDelta, addedIds, claimedIds } from './apply.js';
|
|
7
7
|
import { readDeltaSource } from './static-registry.js';
|
|
8
8
|
import { statusRows, statusCounts } from './status.js';
|
|
9
|
-
import { evaluateGate, notRunIssues } from './gate.js';
|
|
9
|
+
import { evaluateGate, notRunIssues, timeoutClause } from './gate.js';
|
|
10
10
|
import { renderMarkdown, staleIssue } from './render.js';
|
|
11
11
|
import { mergeRedRecord, readRedRecord, redRecordPath, serialiseRedRecord, } from './red-record.js';
|
|
12
|
-
import { DEFAULT_TARGET, resolveTargets } from './targets.js';
|
|
12
|
+
import { DEFAULT_TARGET, TARGETS, resolveTargets, staleWorkflowIssue } from './targets.js';
|
|
13
13
|
import { writeAtomic } from './write.js';
|
|
14
14
|
import { applyMerge, mergedSpecPath } from './merge.js';
|
|
15
15
|
import { compilerIssue } from './compiler.js';
|
|
@@ -126,9 +126,39 @@ export async function runCheck(root, options = {}) {
|
|
|
126
126
|
...(await unclaimedProposedSpecIssues(root, scan, options, loader)),
|
|
127
127
|
...(await changeDirSpecIssues(root)),
|
|
128
128
|
...proposedNameTakenIssues(root, scan),
|
|
129
|
+
...(await staleWorkflowIssues(root)),
|
|
129
130
|
];
|
|
130
131
|
});
|
|
131
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Every installed workflow document that is no longer the one `init` writes
|
|
135
|
+
* (ATX-89). Reads files and executes nothing, so it sits inside ATX-16 exactly
|
|
136
|
+
* as the rest of `check` does.
|
|
137
|
+
*
|
|
138
|
+
* Reported by `check` alone among the four static commands: it is the one an
|
|
139
|
+
* adopter runs unconditionally and the one the workflow document itself tells
|
|
140
|
+
* the agent to run, while `status` takes a change name and is therefore silent
|
|
141
|
+
* during exactly the brownfield adoption whose document is stalest. That this
|
|
142
|
+
* reports on the installed build rather than on the project's intent is not a
|
|
143
|
+
* widening of the command — `compilerIssue()` is already its first statement.
|
|
144
|
+
*
|
|
145
|
+
* Every target, not only the default one: an adopter who ran
|
|
146
|
+
* `init --target cursor` committed that file and no other, and it goes stale on
|
|
147
|
+
* the same upgrade. Absent targets fall out in `staleWorkflowIssue`, so the loop
|
|
148
|
+
* needs no filter and a project that ran `init` for none of them gets nothing.
|
|
149
|
+
*/
|
|
150
|
+
async function staleWorkflowIssues(root) {
|
|
151
|
+
const issues = [];
|
|
152
|
+
for (const target of TARGETS) {
|
|
153
|
+
// `target.file` is POSIX-spelled and Node takes that separator everywhere,
|
|
154
|
+
// so this needs no normalisation on the way in (ATX-28).
|
|
155
|
+
const current = await readFile(join(root, target.file), 'utf8').catch(() => undefined);
|
|
156
|
+
const issue = staleWorkflowIssue(target, current);
|
|
157
|
+
if (issue)
|
|
158
|
+
issues.push(issue);
|
|
159
|
+
}
|
|
160
|
+
return issues;
|
|
161
|
+
}
|
|
132
162
|
/**
|
|
133
163
|
* A proposed spec whose merged name is already taken (design §7).
|
|
134
164
|
*
|
|
@@ -332,6 +362,11 @@ export async function runVerify(root, options = {}) {
|
|
|
332
362
|
runtimeCoverage: new Map(),
|
|
333
363
|
outcomes: new Map(),
|
|
334
364
|
unloadedFiles: [],
|
|
365
|
+
// No run happened, so there is no timeout in force and nothing
|
|
366
|
+
// reached one. Zero rather than Vitest's default: this stub must not
|
|
367
|
+
// claim a number no run produced.
|
|
368
|
+
testTimeout: 0,
|
|
369
|
+
timedOut: 0,
|
|
335
370
|
}
|
|
336
371
|
: await runAndCollect({
|
|
337
372
|
root,
|
|
@@ -340,7 +375,11 @@ export async function runVerify(root, options = {}) {
|
|
|
340
375
|
vitestConfig: options.vitestConfig,
|
|
341
376
|
});
|
|
342
377
|
if (!run.passed) {
|
|
343
|
-
issues.push({
|
|
378
|
+
issues.push({
|
|
379
|
+
level: 'ERROR',
|
|
380
|
+
code: 'tests-red',
|
|
381
|
+
message: `Some tests are failing.${timeoutClause(run)}`,
|
|
382
|
+
});
|
|
344
383
|
}
|
|
345
384
|
// The load failures first, then the absences they caused — by the same
|
|
346
385
|
// function the gate calls, which is the point. `verify` used to ask only for
|
package/dist/core/runner.js
CHANGED
|
@@ -69,6 +69,12 @@ export async function runAndCollect(options = {}) {
|
|
|
69
69
|
try {
|
|
70
70
|
const runtimeCoverage = new Map();
|
|
71
71
|
const outcomes = new Map();
|
|
72
|
+
// Off the *resolved* config, so it is the timeout that actually applied:
|
|
73
|
+
// Vitest's default when the run is isolated, and the supplied config's value
|
|
74
|
+
// when one was given. Reading it is what keeps the number out of a message
|
|
75
|
+
// template, where it would be a constant about somebody else's release.
|
|
76
|
+
const testTimeout = vitest.config.testTimeout;
|
|
77
|
+
let timedOut = 0;
|
|
72
78
|
// Reconstruct coverage from the task tree (not an in-process singleton).
|
|
73
79
|
//
|
|
74
80
|
// `file` is threaded down from the file task rather than read off the suite,
|
|
@@ -76,6 +82,8 @@ export async function runAndCollect(options = {}) {
|
|
|
76
82
|
// two spec files declaring the same scenario name under one `requirement()`
|
|
77
83
|
// from collapsing into a single entry that either of them could satisfy.
|
|
78
84
|
const walk = (task, file) => {
|
|
85
|
+
if (task.type === 'test' && ranOutTheClock(task, testTimeout))
|
|
86
|
+
timedOut += 1;
|
|
79
87
|
if (task.type === 'suite') {
|
|
80
88
|
const id = requirementIdOf(task.name);
|
|
81
89
|
if (id !== undefined) {
|
|
@@ -128,7 +136,7 @@ export async function runAndCollect(options = {}) {
|
|
|
128
136
|
// missing API as zero failures and turning a red suite green. This is the
|
|
129
137
|
// one boolean in the engine that must never fail open.
|
|
130
138
|
const passed = vitest.state.getCountOfFailedTests() === 0;
|
|
131
|
-
return { passed, runtimeCoverage, outcomes, unloadedFiles };
|
|
139
|
+
return { passed, runtimeCoverage, outcomes, unloadedFiles, testTimeout, timedOut };
|
|
132
140
|
}
|
|
133
141
|
finally {
|
|
134
142
|
await vitest.close();
|
|
@@ -167,6 +175,31 @@ function* scenariosUnder(suite) {
|
|
|
167
175
|
function failedToLoad(file) {
|
|
168
176
|
return (file.result?.errors?.length ?? 0) > 0;
|
|
169
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Did this scenario fail by reaching the timeout rather than by asserting?
|
|
180
|
+
*
|
|
181
|
+
* Duration against the timeout, never the error text. Vitest does say "Test
|
|
182
|
+
* timed out in 5000ms." and matching that would be simpler, but it is the same
|
|
183
|
+
* category error this project refuses in its own diagnostics — a message is
|
|
184
|
+
* prose, and a detector that reads one is a detector the next release of
|
|
185
|
+
* somebody else's tool can silence in a way no test here would notice.
|
|
186
|
+
*
|
|
187
|
+
* `>=` rather than a tolerance below it: a run that reached the ceiling is
|
|
188
|
+
* stopped *at* the ceiling and recorded a hair past it (measured at 5015 ms
|
|
189
|
+
* against 5000), so there is no gap to bridge, and a margin would start
|
|
190
|
+
* claiming timeouts for slow assertions that failed honestly.
|
|
191
|
+
*
|
|
192
|
+
* A missing `duration` is not a timeout. That is the reading that fails
|
|
193
|
+
* quietly rather than loudly: a task tree that stopped carrying durations would
|
|
194
|
+
* otherwise report every failure as a timeout, which is a wrong diagnosis
|
|
195
|
+
* printed with confidence — the failure mode `spec-load-failed` exists to undo.
|
|
196
|
+
*/
|
|
197
|
+
function ranOutTheClock(task, testTimeout) {
|
|
198
|
+
if (task.result?.state !== 'fail')
|
|
199
|
+
return false;
|
|
200
|
+
const duration = task.result.duration;
|
|
201
|
+
return duration !== undefined && duration >= testTimeout;
|
|
202
|
+
}
|
|
170
203
|
/** The outcome of a task that ran, or `undefined` when it did not run at all. */
|
|
171
204
|
function executedOutcome(task) {
|
|
172
205
|
const state = task.result?.state;
|
package/dist/core/skill.js
CHANGED
|
@@ -30,17 +30,23 @@
|
|
|
30
30
|
// actually needs it, and one fewer command beats saving an upgrade a diff.
|
|
31
31
|
// `attest init` is idempotent: re-run it after an upgrade.
|
|
32
32
|
//
|
|
33
|
-
// The remaining exposure
|
|
34
|
-
// workflow to an agent that trusts it
|
|
35
|
-
// could cause is a diagnostic with a fix hint — a registry written
|
|
36
|
-
// is `registry-not-static`, and the agent corrects itself from the
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
33
|
+
// The remaining exposure was called honest and small: an old file describes an
|
|
34
|
+
// old workflow to an agent that trusts it, and the backstop was that every
|
|
35
|
+
// mistake it could cause is a diagnostic with a fix hint — a registry written
|
|
36
|
+
// the old way is `registry-not-static`, and the agent corrects itself from the
|
|
37
|
+
// report. **That backstop is withdrawn (design §9).** A stale document producing
|
|
38
|
+
// bad input is caught; one producing unnecessary work is not, and nothing fires
|
|
39
|
+
// at any severity for the second. So the decision above stands on the chore
|
|
40
|
+
// argument alone, and must not be re-derived from the backstop — that is the
|
|
41
|
+
// part a rewrite has to keep. The measurement that withdrew it, and the pricing
|
|
42
|
+
// of the adopter-facing form it reopens, are in CHANGELOG.md.
|
|
43
|
+
// The exposure has happened twice, the second time on a diagnostic that was
|
|
44
|
+
// not added but *redefined* — the code table and the prose below it disagreed,
|
|
45
|
+
// and an agent branches on the table. So: read this file when a release adds a
|
|
46
|
+
// diagnostic **or changes what one means**, and grep this string for the code
|
|
47
|
+
// rather than trusting that the section you edited was its only mention.
|
|
48
|
+
// Nothing gates it — `ATX-57` catches a code the engine cannot emit, never one
|
|
49
|
+
// it can, and its rationale records that one-directionality as deliberate.
|
|
44
50
|
/**
|
|
45
51
|
* The one sentence that decides whether the workflow is ever loaded.
|
|
46
52
|
*
|
|
@@ -428,6 +434,21 @@ Two things make that safe, and one keeps it from being a verdict:
|
|
|
428
434
|
under the gate — that gap is the first thing to suspect when it does. See
|
|
429
435
|
\`tests-red\` in the troubleshooting document, and \`--vitest-config\` if your
|
|
430
436
|
specs need that environment.
|
|
437
|
+
- **Your config's \`testTimeout\` is dropped along with the rest of it**, so the
|
|
438
|
+
isolated run applies Vitest's own default of five seconds. A scenario that
|
|
439
|
+
starts a subprocess, a container or a build can fit inside that on one machine
|
|
440
|
+
and not on another, which reads as a gate that fails intermittently rather than
|
|
441
|
+
as a clock. Give any scenario that goes near it an explicit timeout — the third
|
|
442
|
+
argument to \`scenario\` — rather than relying on a number the gate does not
|
|
443
|
+
take from you:
|
|
444
|
+
|
|
445
|
+
\`\`\`ts
|
|
446
|
+
scenario('builds the container', async () => { /* … */ }, 30_000);
|
|
447
|
+
\`\`\`
|
|
448
|
+
|
|
449
|
+
When it is the cause, \`tests-red\` says so: it names the timeout that applied
|
|
450
|
+
and how many scenarios reached it, which is the one thing running the file
|
|
451
|
+
yourself cannot tell you, because your run had your timeout.
|
|
431
452
|
|
|
432
453
|
### Four things you must not do
|
|
433
454
|
|
package/dist/core/targets.d.ts
CHANGED
|
@@ -49,4 +49,33 @@ export declare function resolveTargets(names: readonly string[]): {
|
|
|
49
49
|
targets: AgentTarget[];
|
|
50
50
|
issues: Issue[];
|
|
51
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* The freshness verdict for one installed workflow document: `undefined` when
|
|
54
|
+
* the file on disk is the document `init` writes now, an INFO otherwise.
|
|
55
|
+
*
|
|
56
|
+
* **`current` being `undefined` is silence, not a second code**, and that is the
|
|
57
|
+
* whole of ATX-90. `render`'s `staleIssue` is the obvious model and reports a
|
|
58
|
+
* rendering that was never generated under `missing-spec-doc`; copying that here
|
|
59
|
+
* would fire on every project that has not installed the workflow — two of this
|
|
60
|
+
* repository's own three targets, and the majority of adopters, since `init`
|
|
61
|
+
* writes one target by default and nothing obliges a project to run it at all.
|
|
62
|
+
* An absent file is indistinguishable from a repository that never adopted the
|
|
63
|
+
* workflow half, which is the observation ATX-84 makes from the other side.
|
|
64
|
+
*
|
|
65
|
+
* **The comparison is the document and not the bytes (ATX-91).** `init` writes
|
|
66
|
+
* `\n` and Git hands the file back as `\r\n` wherever `core.autocrlf` is on, the
|
|
67
|
+
* installer default on Windows, so a byte comparison would call a fresh clone
|
|
68
|
+
* stale. Deliberately the opposite of ATX-84, which compares this repository's
|
|
69
|
+
* own copy byte-for-byte and may, because `.gitattributes` here is committed and
|
|
70
|
+
* pins `eol=lf` — that file is Attest's here and nobody's in an adopter's tree.
|
|
71
|
+
*
|
|
72
|
+
* INFO, and the level is the requirement rather than a presentation choice: the
|
|
73
|
+
* obligation to keep this document current is Attest's, and no adopter is under
|
|
74
|
+
* one to take the update. So the tool says what it knows and asks for nothing —
|
|
75
|
+
* `ok`, the exit code and every gate are untouched at this level.
|
|
76
|
+
*
|
|
77
|
+
* `file` is `target.file`, already spelled with `/` on every platform (ATX-28),
|
|
78
|
+
* so nothing here goes near `path.relative`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function staleWorkflowIssue(target: AgentTarget, current: string | undefined): Issue | undefined;
|
|
52
81
|
//# sourceMappingURL=targets.d.ts.map
|
package/dist/core/targets.js
CHANGED
|
@@ -138,4 +138,50 @@ export function resolveTargets(names) {
|
|
|
138
138
|
issues,
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The freshness verdict for one installed workflow document: `undefined` when
|
|
143
|
+
* the file on disk is the document `init` writes now, an INFO otherwise.
|
|
144
|
+
*
|
|
145
|
+
* **`current` being `undefined` is silence, not a second code**, and that is the
|
|
146
|
+
* whole of ATX-90. `render`'s `staleIssue` is the obvious model and reports a
|
|
147
|
+
* rendering that was never generated under `missing-spec-doc`; copying that here
|
|
148
|
+
* would fire on every project that has not installed the workflow — two of this
|
|
149
|
+
* repository's own three targets, and the majority of adopters, since `init`
|
|
150
|
+
* writes one target by default and nothing obliges a project to run it at all.
|
|
151
|
+
* An absent file is indistinguishable from a repository that never adopted the
|
|
152
|
+
* workflow half, which is the observation ATX-84 makes from the other side.
|
|
153
|
+
*
|
|
154
|
+
* **The comparison is the document and not the bytes (ATX-91).** `init` writes
|
|
155
|
+
* `\n` and Git hands the file back as `\r\n` wherever `core.autocrlf` is on, the
|
|
156
|
+
* installer default on Windows, so a byte comparison would call a fresh clone
|
|
157
|
+
* stale. Deliberately the opposite of ATX-84, which compares this repository's
|
|
158
|
+
* own copy byte-for-byte and may, because `.gitattributes` here is committed and
|
|
159
|
+
* pins `eol=lf` — that file is Attest's here and nobody's in an adopter's tree.
|
|
160
|
+
*
|
|
161
|
+
* INFO, and the level is the requirement rather than a presentation choice: the
|
|
162
|
+
* obligation to keep this document current is Attest's, and no adopter is under
|
|
163
|
+
* one to take the update. So the tool says what it knows and asks for nothing —
|
|
164
|
+
* `ok`, the exit code and every gate are untouched at this level.
|
|
165
|
+
*
|
|
166
|
+
* `file` is `target.file`, already spelled with `/` on every platform (ATX-28),
|
|
167
|
+
* so nothing here goes near `path.relative`.
|
|
168
|
+
*/
|
|
169
|
+
export function staleWorkflowIssue(target, current) {
|
|
170
|
+
if (current === undefined)
|
|
171
|
+
return undefined;
|
|
172
|
+
const eol = (s) => s.replace(/\r\n/g, '\n');
|
|
173
|
+
if (eol(current) === eol(target.content()))
|
|
174
|
+
return undefined;
|
|
175
|
+
return {
|
|
176
|
+
level: 'INFO',
|
|
177
|
+
code: 'stale-workflow-doc',
|
|
178
|
+
file: target.file,
|
|
179
|
+
// The hint names what the command does to the file, not just the command.
|
|
180
|
+
// `init` writes with no merge and no backup, so an adopter who edited these
|
|
181
|
+
// instructions is being pointed at something destructive — defensible,
|
|
182
|
+
// since Attest owns the path (rule 1 above), and worth saying anyway.
|
|
183
|
+
message: `"${target.file}" is not the ${target.tool} workflow document this version of Attest writes. ` +
|
|
184
|
+
`Re-run \`attest init --target ${target.name}\` to update it, which rewrites the file in place.`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
141
187
|
//# sourceMappingURL=targets.js.map
|
package/dist/core/types.d.ts
CHANGED
|
@@ -79,6 +79,35 @@ export interface RunResult {
|
|
|
79
79
|
* separately, which is what this is.
|
|
80
80
|
*/
|
|
81
81
|
unloadedFiles: string[];
|
|
82
|
+
/**
|
|
83
|
+
* The per-test timeout that was actually in force for this run, in
|
|
84
|
+
* milliseconds, read off the resolved child config rather than assumed.
|
|
85
|
+
*
|
|
86
|
+
* Read rather than assumed because it is the one number here Attest does not
|
|
87
|
+
* choose: an isolated run (`config: false`) gets Vitest's own default, and a
|
|
88
|
+
* run given `--vitest-config` gets whatever that config sets. A constant
|
|
89
|
+
* repeated in a message would be right in one of those cases and quietly
|
|
90
|
+
* wrong in the other, and it would go stale on a Vitest release with nothing
|
|
91
|
+
* to notice.
|
|
92
|
+
*/
|
|
93
|
+
testTimeout: number;
|
|
94
|
+
/**
|
|
95
|
+
* How many scenarios failed by reaching {@link RunResult.testTimeout} rather
|
|
96
|
+
* than by failing an assertion.
|
|
97
|
+
*
|
|
98
|
+
* A count, deliberately, and not the names: what a red report is missing is
|
|
99
|
+
* carried in the run's own output, and reproducing it here is a decision this
|
|
100
|
+
* project has not taken. What the count adds is the one thing that output
|
|
101
|
+
* cannot supply when the direct run is green — whether the clock, rather than
|
|
102
|
+
* the code, is what the isolated run disagreed about.
|
|
103
|
+
*
|
|
104
|
+
* Detected by duration against the timeout, never by the child's error text:
|
|
105
|
+
* a message is prose in somebody else's project too, and a detector reading
|
|
106
|
+
* one is a detector a Vitest release can silence. Measured: a scenario that
|
|
107
|
+
* runs out the clock ends `fail` at a duration just past the timeout, while
|
|
108
|
+
* an assertion failure ends `fail` in under a millisecond.
|
|
109
|
+
*/
|
|
110
|
+
timedOut: number;
|
|
82
111
|
}
|
|
83
112
|
/** Severity levels for graded reporting (design §5.3). */
|
|
84
113
|
export type Level = 'ERROR' | 'WARNING' | 'INFO';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@am_shork/attest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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",
|