@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.
- package/CHANGELOG.md +50 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/bin/attest.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +69 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/report.d.ts +7 -0
- package/dist/cli/report.d.ts.map +1 -0
- package/dist/cli/report.js +45 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/core/apply.d.ts +13 -0
- package/dist/core/apply.d.ts.map +1 -0
- package/dist/core/apply.js +94 -0
- package/dist/core/apply.js.map +1 -0
- package/dist/core/gate.d.ts +22 -0
- package/dist/core/gate.d.ts.map +1 -0
- package/dist/core/gate.js +44 -0
- package/dist/core/gate.js.map +1 -0
- package/dist/core/loader.d.ts +6 -0
- package/dist/core/loader.d.ts.map +1 -0
- package/dist/core/loader.js +39 -0
- package/dist/core/loader.js.map +1 -0
- package/dist/core/locate.d.ts +22 -0
- package/dist/core/locate.d.ts.map +1 -0
- package/dist/core/locate.js +117 -0
- package/dist/core/locate.js.map +1 -0
- package/dist/core/parser.d.ts +3 -0
- package/dist/core/parser.d.ts.map +1 -0
- package/dist/core/parser.js +0 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/pipeline.d.ts +22 -0
- package/dist/core/pipeline.d.ts.map +1 -0
- package/dist/core/pipeline.js +135 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/registry.d.ts +35 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +41 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/runner.d.ts +15 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +59 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/schema.d.ts +42 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +20 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/types.d.ts +39 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +5 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/validator.d.ts +15 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +73 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +11 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +32 -0
- package/dist/runtime.js.map +1 -0
- 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 @@
|
|
|
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":"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"}
|
|
@@ -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 @@
|
|
|
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"}
|