@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/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.0.0/docs/en/troubleshooting.md#registry-not-static
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
@@ -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
@@ -54,6 +54,7 @@ export const ISSUE_CODES = [
54
54
  'spec-in-change-dir',
55
55
  'spec-load-failed',
56
56
  'stale-spec-doc',
57
+ 'stale-workflow-doc',
57
58
  'tests-red',
58
59
  'unbound-param',
59
60
  'uncovered-requirement',
@@ -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: 'Some tests are failing; the change cannot be archived.',
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
@@ -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({ level: 'ERROR', code: 'tests-red', message: 'Some tests are failing.' });
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
@@ -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;
@@ -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 is honest and small: an old file describes an old
34
- // workflow to an agent that trusts it. The backstop is that every mistake it
35
- // could cause is a diagnostic with a fix hint — a registry written the old way
36
- // is `registry-not-static`, and the agent corrects itself from the report.
37
- // It has happened twice, the second time on a diagnostic that was not added but
38
- // *redefined* the code table and the prose below it disagreed, and an agent
39
- // branches on the table. So: read this file when a release adds a diagnostic
40
- // **or changes what one means**, and grep this string for the code rather than
41
- // trusting that the section you edited was its only mention. Nothing gates it
42
- // `ATX-57` catches a code the engine cannot emit, never one it can, and its
43
- // rationale records that one-directionality as deliberate.
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
 
@@ -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
@@ -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
@@ -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.0.0",
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",