ai-runtime-kit 0.9.0 → 0.10.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 +75 -0
- package/README.md +10 -0
- package/bin/cli.js +4 -0
- package/package.json +1 -1
- package/src/validate.js +235 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,81 @@ kit.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.10.0] - 2026-05-14
|
|
14
|
+
|
|
15
|
+
**First M4 data point.** Kit's first `src/**` feature shipped
|
|
16
|
+
under the v0.6.0+ PRD → Feature → Spec → Plan → Task → TDD →
|
|
17
|
+
Implement → Verify → Review workflow with `TDD-Applies: true`
|
|
18
|
+
tasks. Two TDD pairs (test-commit-before-impl-commit) at 100%
|
|
19
|
+
test-first ordering.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **`validate` command** — checks `.ai/project/` tree
|
|
23
|
+
structural integrity. Walks every artifact (PRD / feature /
|
|
24
|
+
spec / plan / task / review) and verifies:
|
|
25
|
+
- Required `## Parent <Type>` sections per v0.9.0 INDEX §
|
|
26
|
+
Traceability rules.
|
|
27
|
+
- Cited parent paths resolve to real files on disk.
|
|
28
|
+
- `Status` values are in the allowed set per artifact type.
|
|
29
|
+
Reports errors (missing-parent / empty-parent /
|
|
30
|
+
unresolved-parent) and warnings (unexpected-status). Exits
|
|
31
|
+
0 on clean, 1 on any error.
|
|
32
|
+
- `--json` flag for machine-parseable output.
|
|
33
|
+
- `--cwd <dir>` flag for non-default project root.
|
|
34
|
+
- `src/validate.js` (NEW) — pure validator module.
|
|
35
|
+
- `test/validate.test.js` (NEW) — 6 unit tests (clean tree,
|
|
36
|
+
missing parent, broken path, `(none — ...)` rendering,
|
|
37
|
+
JSON output, live dogfood smoke).
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- `bin/cli.js` — dispatcher gains `validate` subcommand;
|
|
41
|
+
HELP lists it alongside `init` and `upgrade`.
|
|
42
|
+
- 7 historical specs (v0.4.0 / v0.5.0 / v0.5.1 / v0.6.0 /
|
|
43
|
+
v0.7.0 / v0.8.0 / v0.8.1) retrofitted with `## Parent
|
|
44
|
+
Feature` sections (pre-v0.6.0 use `(none —
|
|
45
|
+
pre-feature-layer)`; v0.8.1 uses `(none —
|
|
46
|
+
engineering-only)`; the rest cite real feature paths).
|
|
47
|
+
- 7 historical reviews retrofitted with `## Parent Spec`
|
|
48
|
+
sections. v0.9.0's spec/review already had theirs (the
|
|
49
|
+
convention F4 shipped was in use immediately on its own
|
|
50
|
+
review). Retrofit is project-side and not subject to
|
|
51
|
+
preflight.
|
|
52
|
+
|
|
53
|
+
### Process
|
|
54
|
+
- **Kit's first `src/**` feature** under the new pipeline.
|
|
55
|
+
Branch was `feat/validate-cli` (not `chore/runtime-*`)
|
|
56
|
+
because no `runtime/**` paths in scope.
|
|
57
|
+
- **First non-runtime-scoped feature in the v0.6.0+
|
|
58
|
+
workflow** — preflight hook did not fire (correctly; no
|
|
59
|
+
`runtime/**` paths to gate on). The kit's discipline now
|
|
60
|
+
cleanly distinguishes governance-scoped (preflight gated)
|
|
61
|
+
from kit-code-scoped (regular feature branches).
|
|
62
|
+
- **First M4 data point.** Two TDD pairs:
|
|
63
|
+
- T1 (src/validate.js): test commit 16:07:26 → impl
|
|
64
|
+
16:08:21 (Δ +55s).
|
|
65
|
+
- T2 (bin/cli.js): test commit 16:09:47 → impl 16:11:53
|
|
66
|
+
(Δ +2m6s).
|
|
67
|
+
Both pairs satisfy "test-commit precedes impl-commit." M4
|
|
68
|
+
score for this feature: 2/2 = 100%.
|
|
69
|
+
- **Validator caught a real concern during dogfood.** The
|
|
70
|
+
first run reported `TASK_STATUS.md` as a missing-parent
|
|
71
|
+
error; this file is a top-level status tracker (output of
|
|
72
|
+
`init`), not a task instance. Fix landed in C4: validator
|
|
73
|
+
now skips `EXCLUDED_FILENAMES`. The tool catching its own
|
|
74
|
+
edge case in its first ship cycle is the dogfood loop
|
|
75
|
+
working as intended.
|
|
76
|
+
- **Spec amended mid-implementation** (third occurrence —
|
|
77
|
+
v0.5.1, v0.7.0, and now v0.10.0). Original spec excluded
|
|
78
|
+
retrofitting historical artifacts; the validator's
|
|
79
|
+
credibility depends on its first dogfood pass being
|
|
80
|
+
clean, so the spec was amended pre-impl to include
|
|
81
|
+
retrofit work. Recorded in spec § Status with a comment.
|
|
82
|
+
- **24/24 tests pass** (18 prior + 6 new).
|
|
83
|
+
- **`validate` against this repo's `.ai/` tree**:
|
|
84
|
+
3 PRDs / 5 features / 9 specs / 0 plans / 0 tasks /
|
|
85
|
+
8 reviews / **0 errors / 0 warnings / Result: PASS**.
|
|
86
|
+
VM1 satisfied live on first ship.
|
|
87
|
+
|
|
13
88
|
## [0.9.0] - 2026-05-14
|
|
14
89
|
|
|
15
90
|
**Closes the v0.6.0 nine-phase-workflow PRD's 4-feature
|
package/README.md
CHANGED
|
@@ -10,6 +10,16 @@ npx ai-runtime-kit upgrade # existing kit consumer
|
|
|
10
10
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
|
+
**v0.10.0** — Adds the **`validate` command** — checks
|
|
14
|
+
`.ai/project/` tree structural integrity (every artifact carries
|
|
15
|
+
its required `## Parent <Type>` section; every cited parent path
|
|
16
|
+
resolves on disk; Status values are valid). Reports errors and
|
|
17
|
+
warnings; exits 0 on clean / 1 on errors; `--json` for machine
|
|
18
|
+
output. **First `src/**` feature shipped under the v0.6.0+
|
|
19
|
+
PRD → Feature → Spec → Plan → Task → TDD → Implement → Verify
|
|
20
|
+
→ Review pipeline; first M4 data point — 2/2 TDD pairs at
|
|
21
|
+
100% test-first ordering.**
|
|
22
|
+
|
|
13
23
|
**v0.9.0** — Formalizes the **`## Parent <Type>` traceability
|
|
14
24
|
convention** across all artifact templates. Every kit artifact
|
|
15
25
|
(spec / plan / task / review) now carries a structural
|
package/bin/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ Usage: ai-runtime-kit <command> [options]
|
|
|
10
10
|
Commands:
|
|
11
11
|
init Initialize .ai/ runtime in the current directory.
|
|
12
12
|
upgrade Upgrade .ai/runtime/ to the kit's current version.
|
|
13
|
+
validate Check .ai/project/ tree structural integrity.
|
|
13
14
|
--help, -h Show this help.
|
|
14
15
|
--version Show kit version.
|
|
15
16
|
|
|
@@ -38,6 +39,9 @@ async function main() {
|
|
|
38
39
|
case 'upgrade':
|
|
39
40
|
await require('../src/upgrade').run(rest);
|
|
40
41
|
return;
|
|
42
|
+
case 'validate':
|
|
43
|
+
require('../src/validate').run(rest);
|
|
44
|
+
return;
|
|
41
45
|
default:
|
|
42
46
|
console.error(`ai-runtime-kit: unknown command '${cmd}'`);
|
|
43
47
|
console.error('');
|
package/package.json
CHANGED
package/src/validate.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { parseArgs } = require('node:util');
|
|
6
|
+
|
|
7
|
+
// Per v0.9.0 INDEX § Traceability:
|
|
8
|
+
// PRD — no parent required (chain root)
|
|
9
|
+
// Feature — ## Parent PRD
|
|
10
|
+
// Spec — ## Parent Feature
|
|
11
|
+
// Plan — ## Parent Spec
|
|
12
|
+
// Task — ## Parent Spec + ## Parent Plan
|
|
13
|
+
// Review — ## Parent Spec
|
|
14
|
+
const ARTIFACT_RULES = {
|
|
15
|
+
prds: { dir: 'prds', required: [] },
|
|
16
|
+
features: { dir: 'features', required: ['Parent PRD'] },
|
|
17
|
+
specs: { dir: 'specs', required: ['Parent Feature'] },
|
|
18
|
+
plans: { dir: 'plans', required: ['Parent Spec'] },
|
|
19
|
+
tasks: { dir: 'tasks', required: ['Parent Spec', 'Parent Plan'] },
|
|
20
|
+
reviews: { dir: 'reviews', required: ['Parent Spec'] },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const VALID_STATUSES = {
|
|
24
|
+
prds: ['DRAFT', 'APPROVED', 'REJECTED', 'SUPERSEDED'],
|
|
25
|
+
features: ['DRAFT', 'APPROVED', 'REJECTED', 'SUPERSEDED'],
|
|
26
|
+
specs: ['DRAFT', 'APPROVED', 'REJECTED', 'SUPERSEDED'],
|
|
27
|
+
plans: ['PLANNED', 'IN_PROGRESS', 'DONE'],
|
|
28
|
+
tasks: ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Files within an artifact dir that are NOT instances of that
|
|
32
|
+
// artifact type. TASK_STATUS.md is a top-level status tracker
|
|
33
|
+
// produced by `init`, not a per-task instance.
|
|
34
|
+
const EXCLUDED_FILENAMES = new Set(['TASK_STATUS.md']);
|
|
35
|
+
|
|
36
|
+
function walkMd(dir) {
|
|
37
|
+
if (!fs.existsSync(dir)) return [];
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
40
|
+
if (EXCLUDED_FILENAMES.has(entry.name)) continue;
|
|
41
|
+
const p = path.join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) out.push(...walkMd(p));
|
|
43
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(p);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseSections(content) {
|
|
49
|
+
// Returns { sectionName: bodyString }.
|
|
50
|
+
// Sections are top-level `## ` headings.
|
|
51
|
+
const lines = content.split('\n');
|
|
52
|
+
const sections = {};
|
|
53
|
+
let cur = null;
|
|
54
|
+
let buf = [];
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const m = line.match(/^##\s+(.+?)\s*$/);
|
|
57
|
+
if (m) {
|
|
58
|
+
if (cur !== null) sections[cur] = buf.join('\n').trim();
|
|
59
|
+
cur = m[1].trim();
|
|
60
|
+
buf = [];
|
|
61
|
+
} else if (cur !== null) {
|
|
62
|
+
buf.push(line);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (cur !== null) sections[cur] = buf.join('\n').trim();
|
|
66
|
+
return sections;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function firstValueLine(body) {
|
|
70
|
+
// Returns first non-empty, non-comment line; strips surrounding backticks.
|
|
71
|
+
for (const raw of body.split('\n')) {
|
|
72
|
+
const line = raw.trim();
|
|
73
|
+
if (!line) continue;
|
|
74
|
+
if (line.startsWith('<!--')) continue;
|
|
75
|
+
return line.replace(/^`/, '').replace(/`$/, '');
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isNoneRendering(value) {
|
|
81
|
+
return value !== null && value.startsWith('(none');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validate(projectRoot, _options = {}) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
const warnings = [];
|
|
87
|
+
const summary = { prds: 0, features: 0, specs: 0, plans: 0, tasks: 0, reviews: 0 };
|
|
88
|
+
|
|
89
|
+
const aiProject = path.join(projectRoot, '.ai', 'project');
|
|
90
|
+
if (!fs.existsSync(aiProject)) {
|
|
91
|
+
return { errors, warnings, summary };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const [artifact, rule] of Object.entries(ARTIFACT_RULES)) {
|
|
95
|
+
const dir = path.join(aiProject, rule.dir);
|
|
96
|
+
const files = walkMd(dir);
|
|
97
|
+
summary[artifact] = files.length;
|
|
98
|
+
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const relFile = path.relative(projectRoot, file);
|
|
101
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
102
|
+
const sections = parseSections(content);
|
|
103
|
+
|
|
104
|
+
for (const parentName of rule.required) {
|
|
105
|
+
const sectionBody = sections[parentName];
|
|
106
|
+
if (sectionBody === undefined) {
|
|
107
|
+
errors.push({
|
|
108
|
+
artifact,
|
|
109
|
+
file: relFile,
|
|
110
|
+
type: 'missing-parent',
|
|
111
|
+
message: `Missing required ## ${parentName} section`,
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const value = firstValueLine(sectionBody);
|
|
116
|
+
if (value === null) {
|
|
117
|
+
errors.push({
|
|
118
|
+
artifact,
|
|
119
|
+
file: relFile,
|
|
120
|
+
type: 'empty-parent',
|
|
121
|
+
message: `## ${parentName} section is empty`,
|
|
122
|
+
});
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (isNoneRendering(value)) continue;
|
|
126
|
+
const resolved = path.join(projectRoot, value);
|
|
127
|
+
if (!fs.existsSync(resolved)) {
|
|
128
|
+
errors.push({
|
|
129
|
+
artifact,
|
|
130
|
+
file: relFile,
|
|
131
|
+
type: 'unresolved-parent',
|
|
132
|
+
message: `## ${parentName} points to non-existent path: ${value}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const allowedStatuses = VALID_STATUSES[artifact];
|
|
138
|
+
if (allowedStatuses && sections.Status !== undefined) {
|
|
139
|
+
const statusValue = firstValueLine(sections.Status);
|
|
140
|
+
if (statusValue !== null && !allowedStatuses.includes(statusValue)) {
|
|
141
|
+
warnings.push({
|
|
142
|
+
artifact,
|
|
143
|
+
file: relFile,
|
|
144
|
+
type: 'unexpected-status',
|
|
145
|
+
message: `Status "${statusValue}" not in allowed values: ${allowedStatuses.join(', ')}`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { errors, warnings, summary };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function run(argv) {
|
|
156
|
+
let parsed;
|
|
157
|
+
try {
|
|
158
|
+
parsed = parseArgs({
|
|
159
|
+
args: argv,
|
|
160
|
+
options: {
|
|
161
|
+
cwd: { type: 'string' },
|
|
162
|
+
json: { type: 'boolean', default: false },
|
|
163
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
164
|
+
},
|
|
165
|
+
strict: true,
|
|
166
|
+
allowPositionals: false,
|
|
167
|
+
});
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error(`validate: ${e.message}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (parsed.values.help) {
|
|
174
|
+
printHelp();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const cwd = path.resolve(parsed.values.cwd ?? process.cwd());
|
|
179
|
+
const result = validate(cwd);
|
|
180
|
+
const status = result.errors.length === 0 ? 'PASS' : 'FAIL';
|
|
181
|
+
|
|
182
|
+
if (parsed.values.json) {
|
|
183
|
+
console.log(JSON.stringify({ ...result, result: status }, null, 2));
|
|
184
|
+
} else {
|
|
185
|
+
console.log(`Validating .ai/project/ at ${cwd}\n`);
|
|
186
|
+
for (const [name, count] of Object.entries(result.summary)) {
|
|
187
|
+
console.log(` ${name.padEnd(10)} ${count}`);
|
|
188
|
+
}
|
|
189
|
+
console.log('');
|
|
190
|
+
if (result.errors.length > 0) {
|
|
191
|
+
console.log(`Errors: ${result.errors.length}`);
|
|
192
|
+
for (const e of result.errors) {
|
|
193
|
+
console.log(` - ${e.file}: ${e.type}: ${e.message}`);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
console.log('Errors: none');
|
|
197
|
+
}
|
|
198
|
+
if (result.warnings.length > 0) {
|
|
199
|
+
console.log(`Warnings: ${result.warnings.length}`);
|
|
200
|
+
for (const w of result.warnings) {
|
|
201
|
+
console.log(` - ${w.file}: ${w.type}: ${w.message}`);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
console.log('Warnings: none');
|
|
205
|
+
}
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(
|
|
208
|
+
`Result: ${status === 'PASS' ? 'PASS (clean tree)' : `FAIL (${result.errors.length} errors)`}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
process.exit(result.errors.length === 0 ? 0 : 1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function printHelp() {
|
|
216
|
+
console.log(`ai-runtime-kit validate [options]
|
|
217
|
+
|
|
218
|
+
Validate the .ai/project/ tree's structural integrity. Walks
|
|
219
|
+
every artifact and checks that:
|
|
220
|
+
|
|
221
|
+
- Required ## Parent <Type> sections are present per the
|
|
222
|
+
v0.9.0 INDEX § Traceability rules.
|
|
223
|
+
- Cited parent paths resolve to real files on disk.
|
|
224
|
+
- Status values are in the allowed set per artifact type.
|
|
225
|
+
|
|
226
|
+
Options:
|
|
227
|
+
--cwd <dir> Target project root (default: process.cwd())
|
|
228
|
+
--json Output structured JSON instead of human text
|
|
229
|
+
-h, --help Show this help
|
|
230
|
+
|
|
231
|
+
Exits 0 on no errors (warnings allowed and printed); 1 on any
|
|
232
|
+
error. See .ai/runtime/INDEX.md § Traceability for the rules.`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { validate, run, printHelp };
|