@am_shork/attest 0.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/bin/attest.js +3 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +69 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/report.d.ts +7 -0
  10. package/dist/cli/report.d.ts.map +1 -0
  11. package/dist/cli/report.js +45 -0
  12. package/dist/cli/report.js.map +1 -0
  13. package/dist/core/apply.d.ts +13 -0
  14. package/dist/core/apply.d.ts.map +1 -0
  15. package/dist/core/apply.js +94 -0
  16. package/dist/core/apply.js.map +1 -0
  17. package/dist/core/gate.d.ts +22 -0
  18. package/dist/core/gate.d.ts.map +1 -0
  19. package/dist/core/gate.js +44 -0
  20. package/dist/core/gate.js.map +1 -0
  21. package/dist/core/loader.d.ts +6 -0
  22. package/dist/core/loader.d.ts.map +1 -0
  23. package/dist/core/loader.js +39 -0
  24. package/dist/core/loader.js.map +1 -0
  25. package/dist/core/locate.d.ts +22 -0
  26. package/dist/core/locate.d.ts.map +1 -0
  27. package/dist/core/locate.js +117 -0
  28. package/dist/core/locate.js.map +1 -0
  29. package/dist/core/parser.d.ts +3 -0
  30. package/dist/core/parser.d.ts.map +1 -0
  31. package/dist/core/parser.js +0 -0
  32. package/dist/core/parser.js.map +1 -0
  33. package/dist/core/pipeline.d.ts +22 -0
  34. package/dist/core/pipeline.d.ts.map +1 -0
  35. package/dist/core/pipeline.js +135 -0
  36. package/dist/core/pipeline.js.map +1 -0
  37. package/dist/core/registry.d.ts +35 -0
  38. package/dist/core/registry.d.ts.map +1 -0
  39. package/dist/core/registry.js +41 -0
  40. package/dist/core/registry.js.map +1 -0
  41. package/dist/core/runner.d.ts +15 -0
  42. package/dist/core/runner.d.ts.map +1 -0
  43. package/dist/core/runner.js +59 -0
  44. package/dist/core/runner.js.map +1 -0
  45. package/dist/core/schema.d.ts +42 -0
  46. package/dist/core/schema.d.ts.map +1 -0
  47. package/dist/core/schema.js +20 -0
  48. package/dist/core/schema.js.map +1 -0
  49. package/dist/core/types.d.ts +39 -0
  50. package/dist/core/types.d.ts.map +1 -0
  51. package/dist/core/types.js +5 -0
  52. package/dist/core/types.js.map +1 -0
  53. package/dist/core/validator.d.ts +15 -0
  54. package/dist/core/validator.d.ts.map +1 -0
  55. package/dist/core/validator.js +73 -0
  56. package/dist/core/validator.js.map +1 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +5 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/runtime.d.ts +11 -0
  62. package/dist/runtime.d.ts.map +1 -0
  63. package/dist/runtime.js +32 -0
  64. package/dist/runtime.js.map +1 -0
  65. package/package.json +76 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-07-21
11
+
12
+ Initial release.
13
+
14
+ ### Added
15
+
16
+ - **Runtime authoring API** — `requirement(id, body)` / `scenario(name, fn)` that
17
+ wrap Vitest (`describe`/`it`) as a thin, pure delegation. The requirement's
18
+ `describe` block is named `[id]` so runtime coverage can be reconstructed from
19
+ the Vitest task tree rather than an in-process singleton.
20
+ - **Requirement registry** — `defineRequirements()` validated by a Zod
21
+ `RegistrySchema`: RFC-2119 keyword (`SHALL`/`MUST`) enforced, non-empty
22
+ rationale, scalar-only `params`, and `AUTH-3`-shaped IDs. `params` are the
23
+ single source for values that change.
24
+ - **AST parser** — `parseSpecFile()` uses the TypeScript Compiler API (never
25
+ regex) to extract `requirement()`/`scenario()` calls, ids, 1-based line
26
+ numbers, and `.params` references into a strongly-typed `AttestPlan`.
27
+ - **Structural validation** — `validateStructure()` reports `orphan-test`,
28
+ `uncovered-requirement`, and `unbound-param` as graded ERROR issues;
29
+ `detectPotentialDrift()` emits the `possible-drift` INFO heuristic.
30
+ - **Executable verification** — `runAndCollect()` drives Vitest via its Node API
31
+ and reconstructs `{ passed, runtimeCoverage }` from the task tree.
32
+ - **Delta engine** — `applyDelta()` applies a change delta in the order
33
+ RENAMED → REMOVED → MODIFIED → ADDED, idempotently, with content-compare on
34
+ ADDED and already-synced no-ops on RENAMED.
35
+ - **Archive gate** — `evaluateGate()` blocks unless the end state is
36
+ structurally valid, all tests are green, and every declared scenario actually
37
+ ran (`declared-not-run`).
38
+ - **CLI** — `attest check | verify | cover | archive <change>`, a thin shell over
39
+ the tool-agnostic core, with chalk-rendered graded reports and exit codes.
40
+ Users' `.reqs.ts` / delta modules are loaded at runtime via Vite.
41
+ - **`changes/` isolation** — proposed and archived changes are excluded from
42
+ normal `check`/`verify`; `attest archive <name>` includes only that change.
43
+ - **Dogfooding** — Attest describes its own behaviour under `self/`;
44
+ `attest verify self` is green.
45
+ - **Acknowledgments** — README credits [OpenSpec](https://github.com/Fission-AI/OpenSpec)
46
+ (MIT), whose four-stage engine and diff-first change model Attest's
47
+ architecture is adapted from (re-implemented from scratch, no source copied).
48
+
49
+ [Unreleased]: https://gitlab.com/Pseudorca/attest/-/compare/v0.1.0...main
50
+ [0.1.0]: https://gitlab.com/Pseudorca/attest/-/tags/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 am_shork
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Attest
2
+
3
+ A TDD-native spec framework. **Tests are the source of truth for verification;
4
+ ID-bound requirements are the source of truth for intent.** Attest binds the two
5
+ by a stable ID and continuously detects drift.
6
+
7
+ - **Coverage** — does every requirement have ≥1 scenario? (from runtime coverage)
8
+ - **Result** — is every test green? (from the test runner)
9
+ - **Drift** — do intent and assertions still agree? (static + runtime cross-check)
10
+
11
+ The killer move against drift: values that change (timeouts, limits) live **once**
12
+ in a requirement's `params`, and tests read them from there — so a number is
13
+ physically impossible to drift between the spec and the assertion.
14
+
15
+ See [`docs/attest-design.md`](docs/attest-design.md) for the authoritative design.
16
+
17
+ ## Requirements
18
+
19
+ - Node ≥ 20.19
20
+ - pnpm, plus `vitest` + `vite` (peer dependencies)
21
+
22
+ ## Getting started
23
+
24
+ **Install** (the framework plus its `vitest` + `vite` peers):
25
+
26
+ ```bash
27
+ pnpm add -D @am_shork/attest vitest vite
28
+ ```
29
+
30
+ **1. Declare a requirement** (`requirements/auth.reqs.ts`):
31
+
32
+ ```ts
33
+ import { defineRequirements } from '@am_shork/attest';
34
+
35
+ export default defineRequirements({
36
+ 'AUTH-3': {
37
+ statement:
38
+ 'The system SHALL expire a session after {idleTimeoutMin} minutes of inactivity.',
39
+ rationale: 'Security: limit the exposure window of an unattended session.',
40
+ params: { idleTimeoutMin: 30 }, // the single source for this number
41
+ },
42
+ });
43
+ ```
44
+
45
+ **2. Attest it with a scenario** (`auth/session.spec.ts`):
46
+
47
+ ```ts
48
+ import { expect } from 'vitest';
49
+ import { requirement, scenario } from '@am_shork/attest';
50
+ import reqs from '../requirements/auth.reqs.js';
51
+ import { createSession, advance, isValid } from './session.js';
52
+
53
+ requirement('AUTH-3', () => {
54
+ scenario('idle timeout invalidates the session', () => {
55
+ const t = reqs['AUTH-3'].params.idleTimeoutMin; // read from the single source
56
+ const s = createSession();
57
+ advance(s, t + 1, 'minutes');
58
+ expect(isValid(s, t)).toBe(false);
59
+ });
60
+ });
61
+ ```
62
+
63
+ **3. Run the engine:**
64
+
65
+ ```bash
66
+ attest check # static: orphan tests, uncovered requirements, unbound params
67
+ attest verify # run tests + coverage + drift, graded report
68
+ attest cover # which requirements lack a scenario
69
+ attest archive <change> # gate a proposed change: green + covered + no drift
70
+ ```
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ pnpm install
76
+ pnpm build # tsc -> dist/
77
+ pnpm test # vitest run (the framework's own unit tests)
78
+ pnpm typecheck
79
+ pnpm lint
80
+ ```
81
+
82
+ ### Dogfooding
83
+
84
+ Attest describes its own behaviour under [`self/`](self/) and verifies itself:
85
+
86
+ ```bash
87
+ pnpm verify:self # attest verify self -> all green
88
+ ```
89
+
90
+ ## Acknowledgments
91
+
92
+ Attest's architecture — the four-stage truth engine (AST parse → graded
93
+ validation → diff-first delta apply → archive gate) and its diff-first change
94
+ model — is adapted from [OpenSpec](https://github.com/Fission-AI/OpenSpec) (MIT),
95
+ swapping two parts: parsing moves to the TypeScript Compiler API, and `verify`
96
+ becomes running the tests. The design is re-implemented from scratch; no
97
+ OpenSpec source code is included.
98
+
99
+ ## License
100
+
101
+ [MIT](LICENSE)
package/bin/attest.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // Thin launcher: delegates to the built CLI shell (src/cli -> dist/cli).
3
+ import '../dist/cli/index.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,69 @@
1
+ // CLI shell — a thin veneer over the core pipeline (design §9). All real work
2
+ // lives in src/core; this file parses args, calls a pipeline function, renders
3
+ // the result, and sets the exit code.
4
+ import { Command } from 'commander';
5
+ import chalk from 'chalk';
6
+ import { runCheck, runVerify, runCover, runArchive } from '../core/pipeline.js';
7
+ import { formatIssues, summarize, formatCoverage, hasError } from './report.js';
8
+ const program = new Command();
9
+ program
10
+ .name('attest')
11
+ .description('TDD-native spec framework: tests attest that requirements hold.')
12
+ .version('0.0.0');
13
+ function root(dir) {
14
+ return dir ? dir : process.cwd();
15
+ }
16
+ program
17
+ .command('check')
18
+ .description('Static structural validation (fast CI pre-check).')
19
+ .argument('[dir]', 'project root', undefined)
20
+ .action(async (dir) => {
21
+ const issues = await runCheck(root(dir));
22
+ console.log(formatIssues(issues));
23
+ console.log(summarize(issues));
24
+ process.exitCode = hasError(issues) ? 1 : 0;
25
+ });
26
+ program
27
+ .command('verify')
28
+ .description('Run tests + coverage + drift, emit a graded report.')
29
+ .argument('[dir]', 'project root', undefined)
30
+ .action(async (dir) => {
31
+ const { issues, ok } = await runVerify(root(dir));
32
+ console.log(formatIssues(issues));
33
+ console.log(summarize(issues));
34
+ process.exitCode = ok ? 0 : 1;
35
+ });
36
+ program
37
+ .command('cover')
38
+ .description('Coverage report: which requirements lack a scenario.')
39
+ .argument('[dir]', 'project root', undefined)
40
+ .action(async (dir) => {
41
+ const rows = await runCover(root(dir));
42
+ console.log(formatCoverage(rows));
43
+ const uncovered = rows.filter((r) => !r.covered).length;
44
+ console.log(chalk.dim(`— ${rows.length} requirement(s), ${uncovered} uncovered`));
45
+ process.exitCode = uncovered > 0 ? 1 : 0;
46
+ });
47
+ program
48
+ .command('archive')
49
+ .argument('<change>', 'change name to archive')
50
+ .argument('[dir]', 'project root', undefined)
51
+ .description('Run the archive gate; merge only if it passes.')
52
+ .action(async (change, dir) => {
53
+ const blocking = await runArchive(root(dir), change);
54
+ if (blocking.length === 0) {
55
+ console.log(chalk.green(`✓ gate 通過:change "${change}" 可以 archive。`));
56
+ process.exitCode = 0;
57
+ }
58
+ else {
59
+ console.log(chalk.red(`✗ gate 未通過:change "${change}"`));
60
+ console.log(formatIssues(blocking));
61
+ console.log(summarize(blocking));
62
+ process.exitCode = 1;
63
+ }
64
+ });
65
+ program.parseAsync().catch((err) => {
66
+ console.error(chalk.red(err instanceof Error ? err.stack ?? err.message : String(err)));
67
+ process.exitCode = 1;
68
+ });
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,+EAA+E;AAC/E,sCAAsC;AAEtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEhF,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,QAAQ,CAAC;KACd,WAAW,CAAC,iEAAiE,CAAC;KAC9E,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,SAAS,IAAI,CAAC,GAAY;IACxB,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;AACnC,CAAC;AAED,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,mDAAmD,CAAC;KAChE,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,CAAC;KAC5C,MAAM,CAAC,KAAK,EAAE,GAAY,EAAE,EAAE;IAC7B,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACzC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;IAClC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/B,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,qDAAqD,CAAC;KAClE,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,CAAC;KAC5C,MAAM,CAAC,KAAK,EAAE,GAAY,EAAE,EAAE;IAC7B,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAClD,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;IAClC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/B,OAAO,CAAC,QAAQ,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChC,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,sDAAsD,CAAC;KACnE,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,CAAC;KAC5C,MAAM,CAAC,KAAK,EAAE,GAAY,EAAE,EAAE;IAC7B,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC;IAClC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACxD,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,MAAM,oBAAoB,SAAS,YAAY,CAAC,CACrE,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3C,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,SAAS,CAAC;KAClB,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC;KAC9C,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,CAAC;KAC5C,WAAW,CAAC,gDAAgD,CAAC;KAC7D,MAAM,CAAC,KAAK,EAAE,MAAc,EAAE,GAAY,EAAE,EAAE;IAC7C,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,qBAAqB,MAAM,eAAe,CAAC,CAAC,CAAC;QACrE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,sBAAsB,MAAM,GAAG,CAAC,CAAC,CAAC;QACxD,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;QACjC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC1C,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACxF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { CoverageRow } from '../core/pipeline.js';
2
+ import type { Issue } from '../core/types.js';
3
+ export declare function formatIssues(issues: Issue[]): string;
4
+ export declare function summarize(issues: Issue[]): string;
5
+ export declare function formatCoverage(rows: CoverageRow[]): string;
6
+ export declare function hasError(issues: Issue[]): boolean;
7
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,KAAK,EAAS,MAAM,kBAAkB,CAAC;AAUrD,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAWpD;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAMjD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,CAW1D;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAEjD"}
@@ -0,0 +1,45 @@
1
+ // Terminal rendering of graded issues (design §5.3 levels). Pure formatting —
2
+ // no process side effects — so it stays testable and the CLI stays thin.
3
+ import chalk from 'chalk';
4
+ const LEVEL_TAG = {
5
+ ERROR: (s) => chalk.red.bold(s),
6
+ WARNING: (s) => chalk.yellow.bold(s),
7
+ INFO: (s) => chalk.blueBright(s),
8
+ };
9
+ const ORDER = ['ERROR', 'WARNING', 'INFO'];
10
+ export function formatIssues(issues) {
11
+ if (issues.length === 0)
12
+ return chalk.green('✓ 沒有問題。');
13
+ const lines = [];
14
+ for (const level of ORDER) {
15
+ for (const i of issues.filter((x) => x.level === level)) {
16
+ const where = i.file ? `${i.file}${i.line ? `:${i.line}` : ''}` : '';
17
+ const loc = where ? chalk.dim(` (${where})`) : '';
18
+ lines.push(`${LEVEL_TAG[level](level.padEnd(7))} ${chalk.cyan(i.code)}${loc}\n ${i.message}`);
19
+ }
20
+ }
21
+ return lines.join('\n');
22
+ }
23
+ export function summarize(issues) {
24
+ const counts = { ERROR: 0, WARNING: 0, INFO: 0 };
25
+ for (const i of issues)
26
+ counts[i.level]++;
27
+ return chalk.dim(`— ${counts.ERROR} error, ${counts.WARNING} warning, ${counts.INFO} info`);
28
+ }
29
+ export function formatCoverage(rows) {
30
+ if (rows.length === 0)
31
+ return chalk.dim('(registry 沒有任何需求)');
32
+ return rows
33
+ .map((r) => {
34
+ const mark = r.covered ? chalk.green('✓') : chalk.red('✗');
35
+ const detail = r.covered
36
+ ? chalk.dim(`${r.scenarioCount} scenario${r.scenarioCount === 1 ? '' : 's'}`)
37
+ : chalk.red('no scenario');
38
+ return ` ${mark} ${chalk.bold(r.reqId)} ${detail}`;
39
+ })
40
+ .join('\n');
41
+ }
42
+ export function hasError(issues) {
43
+ return issues.some((i) => i.level === 'ERROR');
44
+ }
45
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { RegistryDelta } from './registry.js';
2
+ import type { Issue, Registry } from './types.js';
3
+ export interface ApplyResult {
4
+ registry: Registry;
5
+ issues: Issue[];
6
+ }
7
+ /**
8
+ * Apply a change delta to a base registry, returning the merged registry plus
9
+ * any apply-level issues (conflicts, missing targets, invalid results). The
10
+ * base registry is never mutated. Applying the same delta twice is a no-op.
11
+ */
12
+ export declare function applyDelta(base: Registry, d: RegistryDelta): ApplyResult;
13
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/core/apply.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAE/D,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,aAAa,GAAG,WAAW,CAiExE"}
@@ -0,0 +1,94 @@
1
+ // Delta application engine (design §7). Ordered + idempotent, mirroring
2
+ // OpenSpec's specs-apply: RENAMED -> REMOVED -> MODIFIED -> ADDED over a
3
+ // Map<id, Requirement>, with content-compare on ADDED and already-synced
4
+ // no-ops on RENAMED.
5
+ import { RequirementSchema } from './schema.js';
6
+ /**
7
+ * Apply a change delta to a base registry, returning the merged registry plus
8
+ * any apply-level issues (conflicts, missing targets, invalid results). The
9
+ * base registry is never mutated. Applying the same delta twice is a no-op.
10
+ */
11
+ export function applyDelta(base, d) {
12
+ const map = new Map(Object.entries(base));
13
+ const issues = [];
14
+ // 1) RENAMED
15
+ for (const { from, to } of d.renamed ?? []) {
16
+ if (map.has(from)) {
17
+ if (map.has(to)) {
18
+ issues.push(err('rename-target-exists', `改名 "${from}" -> "${to}" 失敗:目標 "${to}" 已存在。`));
19
+ continue;
20
+ }
21
+ map.set(to, map.get(from));
22
+ map.delete(from);
23
+ }
24
+ else if (!map.has(to)) {
25
+ // source gone AND target absent -> nothing to rename.
26
+ issues.push(err('rename-source-missing', `改名來源 "${from}" 不存在,且目標 "${to}" 也不存在。`));
27
+ }
28
+ // source gone but target present -> already-synced no-op (idempotent).
29
+ }
30
+ // 2) REMOVED (missing = idempotent no-op)
31
+ for (const id of d.removed ?? []) {
32
+ map.delete(id);
33
+ }
34
+ // 3) MODIFIED (partial merge; re-validate the result)
35
+ for (const [id, patch] of Object.entries(d.modified ?? {})) {
36
+ const existing = map.get(id);
37
+ if (!existing) {
38
+ issues.push(err('modify-missing', `無法修改 "${id}":需求不存在。`));
39
+ continue;
40
+ }
41
+ const merged = {
42
+ ...existing,
43
+ ...patch,
44
+ params: { ...existing.params, ...(patch.params ?? {}) },
45
+ outOfScope: patch.outOfScope ?? existing.outOfScope,
46
+ };
47
+ const parsed = RequirementSchema.safeParse(merged);
48
+ if (!parsed.success) {
49
+ issues.push(err('modify-invalid', `修改 "${id}" 後不合法:${firstMessage(parsed.error)}`));
50
+ continue;
51
+ }
52
+ map.set(id, parsed.data);
53
+ }
54
+ // 4) ADDED (content-compare on collision)
55
+ for (const [id, value] of Object.entries(d.added ?? {})) {
56
+ const parsed = RequirementSchema.safeParse(value);
57
+ if (!parsed.success) {
58
+ issues.push(err('add-invalid', `新增 "${id}" 不合法:${firstMessage(parsed.error)}`));
59
+ continue;
60
+ }
61
+ const existing = map.get(id);
62
+ if (existing) {
63
+ if (!sameRequirement(existing, parsed.data)) {
64
+ issues.push(err('add-conflict', `新增 "${id}" 與現有需求內容不同(衝突)。`));
65
+ }
66
+ // identical content -> idempotent no-op.
67
+ continue;
68
+ }
69
+ map.set(id, parsed.data);
70
+ }
71
+ return { registry: Object.fromEntries(map), issues };
72
+ }
73
+ function err(code, message) {
74
+ return { level: 'ERROR', code, message };
75
+ }
76
+ function firstMessage(error) {
77
+ return error.issues[0]?.message ?? 'unknown error';
78
+ }
79
+ /** Content equality via canonical JSON (params key order does not matter). */
80
+ function sameRequirement(a, b) {
81
+ return canonical(a) === canonical(b);
82
+ }
83
+ function canonical(req) {
84
+ return JSON.stringify({
85
+ statement: req.statement,
86
+ rationale: req.rationale,
87
+ params: sortKeys(req.params),
88
+ outOfScope: [...req.outOfScope],
89
+ });
90
+ }
91
+ function sortKeys(obj) {
92
+ return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
93
+ }
94
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.js","sourceRoot":"","sources":["../../src/core/apply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { AttestPlan, Issue, Registry, RunResult } from './types.js';
2
+ export interface GateInputs {
3
+ /** End-state registry (base with the change's delta already applied). */
4
+ registry: Registry;
5
+ /** Static plan over the merged spec suite. */
6
+ plan: AttestPlan;
7
+ /** Executable verification result (design §5.4). */
8
+ run: RunResult;
9
+ }
10
+ /**
11
+ * Decide whether a change may be archived. Returns the blocking issues; an
12
+ * empty array means the gate passes (design §8). Checks, in order:
13
+ * 1. Structural ERRORs on the end-state registry (no orphan / uncovered /
14
+ * unbound-param). This is also the design's step-4 final re-validation:
15
+ * the registry+plan here are already the applied result, so one pass
16
+ * validates the end state.
17
+ * 2. Executable: all tests green.
18
+ * 3. Static coverage vs runtime coverage: every declared scenario actually
19
+ * ran (catches skip/only false coverage).
20
+ */
21
+ export declare function evaluateGate({ registry, plan, run }: GateInputs): Issue[];
22
+ //# sourceMappingURL=gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/core/gate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEzE,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,QAAQ,EAAE,QAAQ,CAAC;IACnB,8CAA8C;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,oDAAoD;IACpD,GAAG,EAAE,SAAS,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,UAAU,GAAG,KAAK,EAAE,CA+BzE"}
@@ -0,0 +1,44 @@
1
+ // Archive gate: the executable "done" bar (design §8). Unlike OpenSpec's
2
+ // structure-only archive, this gate also requires green tests and static/runtime
3
+ // coverage agreement. evaluateGate is a pure decision over already-computed
4
+ // inputs; the file orchestration (load registry+delta, parse all specs, run)
5
+ // is wired in the Locate layer / CLI (see pipeline.ts).
6
+ import { validateStructure } from './validator.js';
7
+ /**
8
+ * Decide whether a change may be archived. Returns the blocking issues; an
9
+ * empty array means the gate passes (design §8). Checks, in order:
10
+ * 1. Structural ERRORs on the end-state registry (no orphan / uncovered /
11
+ * unbound-param). This is also the design's step-4 final re-validation:
12
+ * the registry+plan here are already the applied result, so one pass
13
+ * validates the end state.
14
+ * 2. Executable: all tests green.
15
+ * 3. Static coverage vs runtime coverage: every declared scenario actually
16
+ * ran (catches skip/only false coverage).
17
+ */
18
+ export function evaluateGate({ registry, plan, run }) {
19
+ const blocking = [];
20
+ // 1) Structure (end-state re-validation).
21
+ blocking.push(...validateStructure(registry, plan).filter((i) => i.level === 'ERROR'));
22
+ // 2) Executable: all green.
23
+ if (!run.passed) {
24
+ blocking.push({
25
+ level: 'ERROR',
26
+ code: 'tests-red',
27
+ message: '尚有測試未通過,不可 archive。',
28
+ });
29
+ }
30
+ // 3) Declared-not-run: static coverage claimed, runtime never executed it.
31
+ for (const s of plan.scenarios) {
32
+ if (!run.runtimeCoverage.get(s.reqId)?.has(s.name)) {
33
+ blocking.push({
34
+ level: 'ERROR',
35
+ code: 'declared-not-run',
36
+ file: s.file,
37
+ line: s.line,
38
+ message: `scenario "${s.name}" 靜態宣告覆蓋 ${s.reqId},執行期卻沒跑到(被 skip/only?)。`,
39
+ });
40
+ }
41
+ }
42
+ return blocking;
43
+ }
44
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/core/gate.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,iFAAiF;AACjF,4EAA4E;AAC5E,6EAA6E;AAC7E,wDAAwD;AAExD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAYnD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAc;IAC9D,MAAM,QAAQ,GAAY,EAAE,CAAC;IAE7B,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,CACX,GAAG,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,CACxE,CAAC;IAEF,4BAA4B;IAC5B,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;QAChB,QAAQ,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,OAAO;YACd,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,qBAAqB;SAC/B,CAAC,CAAC;IACL,CAAC;IAED,2EAA2E;IAC3E,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,QAAQ,CAAC,IAAI,CAAC;gBACZ,KAAK,EAAE,OAAO;gBACd,IAAI,EAAE,kBAAkB;gBACxB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,OAAO,EAAE,aAAa,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC,KAAK,yBAAyB;aACzE,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,6 @@
1
+ export interface Loader {
2
+ load<T = unknown>(absPath: string): Promise<T>;
3
+ close(): Promise<void>;
4
+ }
5
+ export declare function createLoader(): Promise<Loader>;
6
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/core/loader.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAuBpD"}
@@ -0,0 +1,39 @@
1
+ // Runtime TS module loader (design §10 loading strategy). Uses vite — already
2
+ // in the vitest/vite stack — to transform + evaluate users' `.reqs.ts` and
3
+ // `requirements.delta.ts` so the CLI can read runtime values (params, etc.).
4
+ //
5
+ // `vitest` is aliased to a no-op stub: loading a registry never calls
6
+ // requirement()/scenario(), but the `attest` barrel transitively imports
7
+ // runtime.ts (which imports vitest), and importing vitest outside a test run
8
+ // throws. The stub sidesteps that without affecting any read value.
9
+ import { createServer } from 'vite';
10
+ import { mkdtempSync, writeFileSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ const VITEST_STUB = 'export const describe=()=>{};export const it=()=>{};export const test=()=>{};' +
14
+ 'export const expect=()=>{};export const beforeAll=()=>{};export const afterAll=()=>{};' +
15
+ 'export const beforeEach=()=>{};export const afterEach=()=>{};export const vi={};' +
16
+ 'export default {};';
17
+ export async function createLoader() {
18
+ const dir = mkdtempSync(join(tmpdir(), 'attest-loader-'));
19
+ const stub = join(dir, 'vitest-stub.mjs');
20
+ writeFileSync(stub, VITEST_STUB);
21
+ const server = await createServer({
22
+ configFile: false,
23
+ logLevel: 'error',
24
+ appType: 'custom',
25
+ server: { middlewareMode: true },
26
+ resolve: { alias: { vitest: stub } },
27
+ ssr: { noExternal: ['vitest'] },
28
+ optimizeDeps: { noDiscovery: true },
29
+ });
30
+ return {
31
+ async load(absPath) {
32
+ return (await server.ssrLoadModule(absPath));
33
+ },
34
+ async close() {
35
+ await server.close();
36
+ },
37
+ };
38
+ }
39
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/core/loader.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,6EAA6E;AAC7E,EAAE;AACF,sEAAsE;AACtE,yEAAyE;AACzE,6EAA6E;AAC7E,oEAAoE;AAEpE,OAAO,EAAE,YAAY,EAAsB,MAAM,MAAM,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,WAAW,GACf,+EAA+E;IAC/E,wFAAwF;IACxF,kFAAkF;IAClF,oBAAoB,CAAC;AAOvB,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAC1C,aAAa,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAEjC,MAAM,MAAM,GAAkB,MAAM,YAAY,CAAC;QAC/C,UAAU,EAAE,KAAK;QACjB,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE;QAChC,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;QACpC,GAAG,EAAE,EAAE,UAAU,EAAE,CAAC,QAAQ,CAAC,EAAE;QAC/B,YAAY,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE;KACpC,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,CAAC,IAAI,CAAI,OAAe;YAC3B,OAAO,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAM,CAAC;QACpD,CAAC;QACD,KAAK,CAAC,KAAK;YACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { Loader } from './loader.js';
2
+ import type { AttestPlan, Issue, Registry } from './types.js';
3
+ export declare const isReqsFile: (name: string) => boolean;
4
+ export declare const isSpecFile: (name: string) => boolean;
5
+ /** Recursively find files under root whose basename matches `match`. */
6
+ export declare function findFiles(root: string, match: (name: string) => boolean): Promise<string[]>;
7
+ /**
8
+ * Load and merge every `*.reqs.ts` registry under root. A malformed registry
9
+ * (defineRequirements throws) becomes a registry-invalid ERROR; a duplicate id
10
+ * across files becomes a duplicate-requirement ERROR.
11
+ */
12
+ export declare function loadRegistry(root: string, loader: Loader): Promise<{
13
+ registry: Registry;
14
+ issues: Issue[];
15
+ }>;
16
+ /** Parse every `*.spec.ts` under root into one merged plan (file paths shown relative to root). */
17
+ export declare function parseAllSpecFiles(root: string): Promise<AttestPlan>;
18
+ /** Parse the spec files belonging to a single proposed change (design §8). */
19
+ export declare function parseChangeSpecs(root: string, changeName: string): Promise<AttestPlan>;
20
+ /** List the names of proposed changes under `root/changes`. */
21
+ export declare function listChangeNames(root: string): Promise<string[]>;
22
+ //# sourceMappingURL=locate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locate.d.ts","sourceRoot":"","sources":["../../src/core/locate.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAe,MAAM,YAAY,CAAC;AAc3E,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OAAoC,CAAC;AAC/E,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OAAoC,CAAC;AAE/E,wEAAwE;AACxE,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAC/B,OAAO,CAAC,MAAM,EAAE,CAAC,CAenB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC,CA4ClD;AAED,mGAAmG;AACnG,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAEzE;AAED,8EAA8E;AAC9E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,UAAU,CAAC,CAErB;AAED,+DAA+D;AAC/D,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAOrE"}