trackfw 3.1.0 → 4.0.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 +85 -1
- package/package.json +1 -1
- package/src/commands/barrier.js +358 -0
- package/src/commands/help.js +99 -9
- package/src/commands/index.js +1 -0
- package/src/commands/update-harness.js +208 -0
- package/src/commands/update.js +206 -72
- package/src/generators/init.js +124 -36
- package/src/integrations/assets/agents/architect.md +4 -1
- package/src/integrations/assets/agents/backend.md +3 -3
- package/src/integrations/assets/agents/code-quality.md +3 -0
- package/src/integrations/assets/agents/data.md +3 -3
- package/src/integrations/assets/agents/dba.md +3 -3
- package/src/integrations/assets/agents/frontend.md +3 -3
- package/src/integrations/assets/agents/iac.md +3 -3
- package/src/integrations/assets/agents/infra.md +3 -3
- package/src/integrations/assets/agents/qa.md +3 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/assets/agents/tooling.md +3 -3
- package/src/integrations/assets/agents/ux.md +3 -0
- package/src/integrations/catalog.js +25 -1
- package/src/integrations/index.js +20 -2
- package/src/lib/update-engine.js +217 -0
- package/src/validator/index.js +2 -0
package/README.md
CHANGED
|
@@ -150,6 +150,7 @@ trackfw ship -m "feat(auth): add login flow"
|
|
|
150
150
|
| `trackfw roadmap move <name> <state>` | Move roadmap between states |
|
|
151
151
|
| `trackfw roadmap list` | List all roadmaps grouped by state |
|
|
152
152
|
| `trackfw validate` | Check governance consistency (use as CI gate) |
|
|
153
|
+
| `trackfw barrier <roadmap> --wave <n>` | Deterministic wave-release gate — stack-agnostic, checks MLs, acceptance evidence, project-declared gates, and governance |
|
|
153
154
|
| `trackfw ship -m "msg"` | Governed `git commit + push + open PR/MR` — enforces branch pattern and governance gate; resolves forge (GitHub/GitLab/Bitbucket/Azure) automatically or via `--forge`; falls back to a browser URL when the forge CLI is absent |
|
|
154
155
|
| `trackfw context` | Print a structured summary of the project's governance state (REQs, Roadmaps, ADRs with counts and statuses) |
|
|
155
156
|
| `trackfw serve` | Start a local governance dashboard (no cloud, no accounts) |
|
|
@@ -164,7 +165,6 @@ trackfw ship -m "feat(auth): add login flow"
|
|
|
164
165
|
| `trackfw agents uninstall` | Remove selected owned agent deployments |
|
|
165
166
|
| `trackfw skills list` | List available governance skills and deployment state |
|
|
166
167
|
| `trackfw skills install/update/uninstall` | Manage selected governance skills |
|
|
167
|
-
| `trackfw gemini`, `cursor`, `copilot`, `windsurf`, `amazonq` | Historical aliases in the Go distribution only; use `agents`/`skills` |
|
|
168
168
|
| `trackfw version` | Print version |
|
|
169
169
|
|
|
170
170
|
The same lifecycle contract is available from the Go/Homebrew, npm, and PyPI
|
|
@@ -330,6 +330,90 @@ $ trackfw status
|
|
|
330
330
|
|
|
331
331
|
---
|
|
332
332
|
|
|
333
|
+
## `trackfw barrier` — deterministic wave-release gate
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
trackfw barrier <roadmap> --wave <n> [--json]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`trackfw barrier` is the stack-agnostic core of the wave-release gate: it never assumes a build
|
|
340
|
+
tool, a test runner, or a parity rule. Every check either comes from the roadmap itself (the wave's
|
|
341
|
+
declared gates) or from `trackfw validate` run in-process. Point it at a roadmap basename (with or
|
|
342
|
+
without `.md`, resolved against `wip/` then `done/`) and a wave number, and it tells you whether
|
|
343
|
+
that wave is ready to release.
|
|
344
|
+
|
|
345
|
+
A wave passes only when **all four** built-in checks are green:
|
|
346
|
+
|
|
347
|
+
| Check | Passes when |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `mls_complete` | The wave has at least one ML and every ML is marked `**Status:** ✅` |
|
|
350
|
+
| `acceptance_evidence` | Every ML has a non-empty `**Critérios de aceite:**` block with no unchecked `- [ ]` line |
|
|
351
|
+
| `gates` | Every command declared under the wave's `**Gates da wave:**` fenced block exits 0 — a wave with no such block declares zero gates, and the barrier never invents one |
|
|
352
|
+
| `validate` | `trackfw validate --json` reports `violations: 0` |
|
|
353
|
+
|
|
354
|
+
### Exit codes
|
|
355
|
+
|
|
356
|
+
| Exit | Meaning |
|
|
357
|
+
|---|---|
|
|
358
|
+
| `0` | `status: "passed"` — every check is green, the wave may release |
|
|
359
|
+
| `1` | `status: "blocked"` — at least one check failed; the JSON/text report says which |
|
|
360
|
+
| `2` | Usage/resolution error — the roadmap or the wave number could not be resolved. This is **not** `blocked`: a barrier that could not run is distinct from one that ran and failed |
|
|
361
|
+
|
|
362
|
+
### Correcting a blocked wave
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
$ trackfw barrier ROADMAP-example --wave 2
|
|
366
|
+
✗ mls_complete: ML-2C: not complete (status: 🔄)
|
|
367
|
+
✗ acceptance_evidence: ML-2C: 2 unmet acceptance criteria
|
|
368
|
+
wave 2: blocked
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Fix the roadmap (mark the ML `✅`, check off the remaining criteria) and rerun the exact same
|
|
372
|
+
command — the barrier is not a one-shot denial; a corrected wave passes on the next invocation:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
$ trackfw barrier ROADMAP-example --wave 2
|
|
376
|
+
✓ mls_complete
|
|
377
|
+
✓ acceptance_evidence
|
|
378
|
+
✓ gates
|
|
379
|
+
✓ validate
|
|
380
|
+
wave 2: passed
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### JSON output
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
trackfw barrier ROADMAP-example --wave 2 --json
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```json
|
|
390
|
+
{
|
|
391
|
+
"roadmap": "ROADMAP-example.md",
|
|
392
|
+
"wave": 2,
|
|
393
|
+
"status": "blocked",
|
|
394
|
+
"started_at": "2026-07-29T10:30:00Z",
|
|
395
|
+
"finished_at": "2026-07-29T10:30:04Z",
|
|
396
|
+
"checks": [
|
|
397
|
+
{ "name": "mls_complete", "status": "passed", "evidence": ["ML-2A: ✅"], "failures": [] },
|
|
398
|
+
{ "name": "acceptance_evidence", "status": "blocked", "evidence": [], "failures": ["ML-2C: 2 unmet acceptance criteria"] },
|
|
399
|
+
{ "name": "gates", "status": "passed", "commands": ["make quality"], "evidence": ["make quality: exit 0"], "failures": [] },
|
|
400
|
+
{ "name": "validate", "status": "passed", "evidence": ["0 violations, 0 warnings"], "failures": [] }
|
|
401
|
+
],
|
|
402
|
+
"failures": ["acceptance_evidence: ML-2C: 2 unmet acceptance criteria"]
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### `trackfw barrier` vs. `/trackfw:barrier`
|
|
407
|
+
|
|
408
|
+
`trackfw barrier` is the deterministic, reproducible CLI — it never invokes agents and never
|
|
409
|
+
performs Git operations. The `/trackfw:barrier` slash command wraps it with the parts a binary
|
|
410
|
+
cannot evaluate: dispatching `code-quality`/`security` reviews, auditing the diff against scope,
|
|
411
|
+
and — only for `trackfw_architect`, the sole Git authority in this workflow — committing and
|
|
412
|
+
pushing once every check and review is green. A green CLI barrier is necessary but not sufficient
|
|
413
|
+
to release a wave. Full contract: `docs/cli-parity.md` → `## trackfw barrier`.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
333
417
|
## AI assistant integration
|
|
334
418
|
|
|
335
419
|
`trackfw init` can install initial AI integrations. The `agents` and `skills`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Governance CLI for AI-native software delivery and AI coding agents. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, and Amazon Q.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// trackfw barrier <roadmap> --wave <n> [--json]
|
|
4
|
+
//
|
|
5
|
+
// Deterministic core of the wave-release barrier. Stack-agnostic: every executable
|
|
6
|
+
// check comes from the roadmap itself (see docs/cli-parity.md → `## trackfw barrier`).
|
|
7
|
+
// This command never assumes a build tool, a test runner or a parity rule, and it
|
|
8
|
+
// never performs git operations or invokes specialist agents — that orchestration
|
|
9
|
+
// lives in the `/trackfw:barrier` slash command.
|
|
10
|
+
|
|
11
|
+
const { Command } = require('commander')
|
|
12
|
+
const fs = require('fs')
|
|
13
|
+
const path = require('path')
|
|
14
|
+
const { spawnSync } = require('child_process')
|
|
15
|
+
const config = require('../config')
|
|
16
|
+
const validator = require('../validator')
|
|
17
|
+
|
|
18
|
+
// UsageError signals a resolution/parsing failure that must map to exit code 2 —
|
|
19
|
+
// distinct from a "blocked" (exit 1) evaluation result.
|
|
20
|
+
class UsageError extends Error {}
|
|
21
|
+
|
|
22
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Roadmap resolution
|
|
24
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
// resolveRoadmapFile finds <roadmapArg> (basename with or without .md) under
|
|
27
|
+
// wip/ then done/ (both flat and by_agent layouts). Throws UsageError naming the
|
|
28
|
+
// roadmap exactly as the user typed it (no .md normalization) when not found —
|
|
29
|
+
// pinned literally by docs/cli-parity.md (`## trackfw barrier`).
|
|
30
|
+
function resolveRoadmapFile(cfg, roadmapArg) {
|
|
31
|
+
const base = roadmapArg.endsWith('.md') ? roadmapArg : `${roadmapArg}.md`
|
|
32
|
+
const dirs = [...validator.resolveWIPDirs(cfg), ...validator.resolveDoneDirs(cfg)]
|
|
33
|
+
for (const dir of dirs) {
|
|
34
|
+
const candidate = path.join(config.expandPath(dir), base)
|
|
35
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
36
|
+
return { path: candidate, basename: base }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new UsageError(`roadmap "${roadmapArg}" not found in wip/ nor done/ under ${cfg.roadmapDir}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Roadmap parsing rules (string-level, see docs/cli-parity.md)
|
|
44
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
// findWave locates the `## Wave <n> ` heading matching waveNumber and returns its
|
|
47
|
+
// line range [startLine, endLine) within `lines`. Throws UsageError naming the wave
|
|
48
|
+
// number and the resolved roadmap basename when not found (pinned literally by
|
|
49
|
+
// docs/cli-parity.md), or naming the offending line number for a malformed heading.
|
|
50
|
+
// roadmapBasename is optional so existing unit tests that only exercise parsing can
|
|
51
|
+
// omit it; the CLI path always supplies it.
|
|
52
|
+
function findWave(lines, waveNumber, roadmapBasename) {
|
|
53
|
+
let startLine = -1
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
const line = lines[i]
|
|
56
|
+
const attempt = /^## Wave (\S+)/.exec(line)
|
|
57
|
+
if (!attempt) continue
|
|
58
|
+
if (!/^\d+$/.test(attempt[1])) {
|
|
59
|
+
throw new UsageError(`malformed wave heading at line ${i + 1}: "${line}"`)
|
|
60
|
+
}
|
|
61
|
+
if (parseInt(attempt[1], 10) === waveNumber) {
|
|
62
|
+
startLine = i
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (startLine === -1) {
|
|
67
|
+
throw new UsageError(`wave ${waveNumber} not found in roadmap "${roadmapBasename}"`)
|
|
68
|
+
}
|
|
69
|
+
let endLine = lines.length
|
|
70
|
+
for (let j = startLine + 1; j < lines.length; j++) {
|
|
71
|
+
if (/^## /.test(lines[j])) { endLine = j; break }
|
|
72
|
+
}
|
|
73
|
+
return { startLine, endLine }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// findMLs locates every `### ML-` heading inside [startLine, endLine) and returns
|
|
77
|
+
// { id, lines } for each, where `lines` is the ML body (heading excluded).
|
|
78
|
+
function findMLs(lines, startLine, endLine) {
|
|
79
|
+
const headings = []
|
|
80
|
+
for (let i = startLine; i < endLine; i++) {
|
|
81
|
+
if (/^### ML-/.test(lines[i])) {
|
|
82
|
+
const m = /^### (\S+)/.exec(lines[i])
|
|
83
|
+
headings.push({ id: m[1], headingLine: i })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return headings.map((h, idx) => {
|
|
87
|
+
let end = endLine
|
|
88
|
+
for (let j = h.headingLine + 1; j < endLine; j++) {
|
|
89
|
+
if (/^### /.test(lines[j]) || /^## /.test(lines[j])) { end = j; break }
|
|
90
|
+
}
|
|
91
|
+
return { id: h.id, lines: lines.slice(h.headingLine + 1, end) }
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// mlCompletionStatus applies rule 3 — completion is a `**Status:**` line whose
|
|
96
|
+
// remainder contains ✅. Returns { complete, marker } where marker is "missing"
|
|
97
|
+
// when no `**Status:**` line exists at all.
|
|
98
|
+
function mlCompletionStatus(mlLines) {
|
|
99
|
+
for (const raw of mlLines) {
|
|
100
|
+
const line = raw.trim()
|
|
101
|
+
const m = /^\*\*Status:\*\*(.*)$/.exec(line)
|
|
102
|
+
if (m) {
|
|
103
|
+
const marker = m[1].trim()
|
|
104
|
+
return { complete: marker.includes('✅'), marker: marker.length > 0 ? marker : 'missing' }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { complete: false, marker: 'missing' }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// mlAcceptanceEvidence applies rule 4. Returns { hasBlock, total, unmet }.
|
|
111
|
+
function mlAcceptanceEvidence(mlLines) {
|
|
112
|
+
let blockStart = -1
|
|
113
|
+
for (let i = 0; i < mlLines.length; i++) {
|
|
114
|
+
if (/^\*\*Crit[ée]rios de aceite:\*\*/.test(mlLines[i].trim())) { blockStart = i; break }
|
|
115
|
+
}
|
|
116
|
+
if (blockStart === -1) return { hasBlock: false, total: 0, unmet: 0 }
|
|
117
|
+
|
|
118
|
+
let blockEnd = mlLines.length
|
|
119
|
+
for (let j = blockStart + 1; j < mlLines.length; j++) {
|
|
120
|
+
if (/^\*\*/.test(mlLines[j].trim())) { blockEnd = j; break }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const blockLines = mlLines.slice(blockStart + 1, blockEnd)
|
|
124
|
+
.map(l => l.trim())
|
|
125
|
+
.filter(l => l.length > 0)
|
|
126
|
+
|
|
127
|
+
const criteria = blockLines.filter(l => /^- \[/.test(l))
|
|
128
|
+
if (criteria.length === 0) return { hasBlock: false, total: 0, unmet: 0 }
|
|
129
|
+
|
|
130
|
+
const unmet = criteria.filter(l => /^- \[ \]/.test(l))
|
|
131
|
+
return { hasBlock: true, total: criteria.length, unmet: unmet.length }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// parseGates applies rule 5. Returns { commands }. Throws UsageError naming the
|
|
135
|
+
// offending line number for an unterminated fence.
|
|
136
|
+
function parseGates(lines, startLine, endLine) {
|
|
137
|
+
let markerLine = -1
|
|
138
|
+
for (let i = startLine; i < endLine; i++) {
|
|
139
|
+
if (lines[i].trim() === '**Gates da wave:**') { markerLine = i; break }
|
|
140
|
+
}
|
|
141
|
+
if (markerLine === -1) return { commands: [] }
|
|
142
|
+
|
|
143
|
+
let i = markerLine + 1
|
|
144
|
+
while (i < endLine && lines[i].trim() === '') i++
|
|
145
|
+
if (i >= endLine || lines[i].trim() !== '```bash') {
|
|
146
|
+
throw new UsageError(`malformed gates block at line ${markerLine + 1}: expected a \`\`\`bash fence`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const fenceStart = i
|
|
150
|
+
i++
|
|
151
|
+
const commands = []
|
|
152
|
+
let closed = false
|
|
153
|
+
for (; i < endLine; i++) {
|
|
154
|
+
const trimmed = lines[i].trim()
|
|
155
|
+
if (trimmed === '```') { closed = true; break }
|
|
156
|
+
if (trimmed.length === 0 || trimmed.startsWith('#')) continue
|
|
157
|
+
commands.push(trimmed)
|
|
158
|
+
}
|
|
159
|
+
if (!closed) {
|
|
160
|
+
throw new UsageError(`unterminated gates fence starting at line ${fenceStart + 1}`)
|
|
161
|
+
}
|
|
162
|
+
return { commands }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
// Checks (fixed evaluation order: mls_complete, acceptance_evidence, gates, validate)
|
|
167
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function evalMlsComplete(mls, waveNumber) {
|
|
170
|
+
const evidence = []
|
|
171
|
+
const failures = []
|
|
172
|
+
for (const ml of mls) {
|
|
173
|
+
const { complete, marker } = mlCompletionStatus(ml.lines)
|
|
174
|
+
if (complete) evidence.push(`${ml.id}: ✅`)
|
|
175
|
+
else failures.push(`${ml.id}: not complete (status: ${marker})`)
|
|
176
|
+
}
|
|
177
|
+
const status = mls.length > 0 && failures.length === 0 ? 'passed' : 'blocked'
|
|
178
|
+
// Pinned literally by docs/cli-parity.md ("Wave contains zero MLs" case).
|
|
179
|
+
if (mls.length === 0) failures.push(`wave ${waveNumber}: no ML found`)
|
|
180
|
+
return { name: 'mls_complete', status, evidence, failures }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function evalAcceptanceEvidence(mls) {
|
|
184
|
+
const evidence = []
|
|
185
|
+
const failures = []
|
|
186
|
+
for (const ml of mls) {
|
|
187
|
+
const ev = mlAcceptanceEvidence(ml.lines)
|
|
188
|
+
if (!ev.hasBlock) {
|
|
189
|
+
failures.push(`${ml.id}: no acceptance block`)
|
|
190
|
+
} else if (ev.unmet > 0) {
|
|
191
|
+
failures.push(`${ml.id}: ${ev.unmet} unmet acceptance criteria`)
|
|
192
|
+
} else {
|
|
193
|
+
evidence.push(`${ml.id}: ${ev.total} criteria met`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const status = failures.length === 0 ? 'passed' : 'blocked'
|
|
197
|
+
return { name: 'acceptance_evidence', status, evidence, failures }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function evalGates(commands, cwd) {
|
|
201
|
+
const evidence = []
|
|
202
|
+
const failures = []
|
|
203
|
+
for (const command of commands) {
|
|
204
|
+
const result = spawnSync(command, {
|
|
205
|
+
shell: true,
|
|
206
|
+
cwd,
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
stdio: 'pipe',
|
|
209
|
+
})
|
|
210
|
+
const code = result.status === null || result.status === undefined ? 1 : result.status
|
|
211
|
+
if (code === 0) evidence.push(`${command}: exit 0`)
|
|
212
|
+
else failures.push(`${command}: exit ${code}`)
|
|
213
|
+
}
|
|
214
|
+
const status = failures.length === 0 ? 'passed' : 'blocked'
|
|
215
|
+
return { name: 'gates', status, commands, evidence, failures }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function evalValidate() {
|
|
219
|
+
const { violations, warnings } = await validator.validate()
|
|
220
|
+
const summary = `${violations.length} violations, ${warnings.length} warnings`
|
|
221
|
+
if (violations.length === 0) {
|
|
222
|
+
return { name: 'validate', status: 'passed', evidence: [summary], failures: [] }
|
|
223
|
+
}
|
|
224
|
+
return { name: 'validate', status: 'blocked', evidence: [], failures: [summary] }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
228
|
+
// Result document
|
|
229
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
function isoSeconds(date) {
|
|
232
|
+
return date.toISOString().replace(/\.\d{3}Z$/, 'Z')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildDoc(roadmapBasename, waveNumber, checks, startedAt, finishedAt) {
|
|
236
|
+
const status = checks.every(c => c.status === 'passed') ? 'passed' : 'blocked'
|
|
237
|
+
const failures = []
|
|
238
|
+
for (const check of checks) {
|
|
239
|
+
for (const f of check.failures) failures.push(`${check.name}: ${f}`)
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
roadmap: roadmapBasename,
|
|
243
|
+
wave: waveNumber,
|
|
244
|
+
status,
|
|
245
|
+
started_at: isoSeconds(startedAt),
|
|
246
|
+
finished_at: isoSeconds(finishedAt),
|
|
247
|
+
checks: checks.map(c => {
|
|
248
|
+
const doc = { name: c.name, status: c.status, evidence: c.evidence, failures: c.failures }
|
|
249
|
+
if (c.name === 'gates') {
|
|
250
|
+
return { name: c.name, status: c.status, commands: c.commands, evidence: c.evidence, failures: c.failures }
|
|
251
|
+
}
|
|
252
|
+
return doc
|
|
253
|
+
}),
|
|
254
|
+
failures,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function printTextReport(doc) {
|
|
259
|
+
console.log(`Roadmap: ${doc.roadmap}`)
|
|
260
|
+
console.log(`Wave: ${doc.wave}`)
|
|
261
|
+
console.log(`Status: ${doc.status}`)
|
|
262
|
+
console.log('')
|
|
263
|
+
for (const check of doc.checks) {
|
|
264
|
+
console.log(`[${check.status}] ${check.name}`)
|
|
265
|
+
for (const e of check.evidence) console.log(` ✓ ${e}`)
|
|
266
|
+
for (const f of check.failures) console.log(` ✗ ${f}`)
|
|
267
|
+
}
|
|
268
|
+
if (doc.failures.length > 0) {
|
|
269
|
+
console.log('')
|
|
270
|
+
console.log('Failures:')
|
|
271
|
+
for (const f of doc.failures) console.log(` • ${f}`)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
276
|
+
// Command
|
|
277
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
async function runBarrier(roadmapArg, waveOption, jsonOutput) {
|
|
280
|
+
if (waveOption === undefined || waveOption === null || String(waveOption).trim() === '') {
|
|
281
|
+
throw new UsageError('--wave is required')
|
|
282
|
+
}
|
|
283
|
+
if (!/^\d+$/.test(String(waveOption).trim()) || parseInt(waveOption, 10) < 1) {
|
|
284
|
+
throw new UsageError(`invalid --wave value: "${waveOption}" (must be an integer >= 1)`)
|
|
285
|
+
}
|
|
286
|
+
const waveNumber = parseInt(waveOption, 10)
|
|
287
|
+
|
|
288
|
+
const cfg = config.load()
|
|
289
|
+
const resolved = resolveRoadmapFile(cfg, roadmapArg)
|
|
290
|
+
const content = fs.readFileSync(resolved.path, 'utf8')
|
|
291
|
+
const lines = content.split('\n')
|
|
292
|
+
|
|
293
|
+
const startedAt = new Date()
|
|
294
|
+
const wave = findWave(lines, waveNumber, resolved.basename)
|
|
295
|
+
const mls = findMLs(lines, wave.startLine, wave.endLine)
|
|
296
|
+
const gates = parseGates(lines, wave.startLine, wave.endLine)
|
|
297
|
+
|
|
298
|
+
const checks = []
|
|
299
|
+
checks.push(evalMlsComplete(mls, waveNumber))
|
|
300
|
+
checks.push(evalAcceptanceEvidence(mls))
|
|
301
|
+
checks.push(evalGates(gates.commands, process.cwd()))
|
|
302
|
+
checks.push(await evalValidate())
|
|
303
|
+
const finishedAt = new Date()
|
|
304
|
+
|
|
305
|
+
const doc = buildDoc(resolved.basename, waveNumber, checks, startedAt, finishedAt)
|
|
306
|
+
|
|
307
|
+
if (jsonOutput) {
|
|
308
|
+
console.log(JSON.stringify(doc, null, 2))
|
|
309
|
+
} else {
|
|
310
|
+
printTextReport(doc)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return doc.status === 'passed' ? 0 : 1
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createBarrierCommand() {
|
|
317
|
+
const cmd = new Command('barrier')
|
|
318
|
+
cmd
|
|
319
|
+
.description(
|
|
320
|
+
'Deterministic core of the wave-release barrier: evaluates ML completion, ' +
|
|
321
|
+
'acceptance evidence, wave gates and governance validation for a single wave ' +
|
|
322
|
+
'declared in a roadmap.'
|
|
323
|
+
)
|
|
324
|
+
.argument('<roadmap>', 'Roadmap basename, with or without .md')
|
|
325
|
+
.option('--wave <n>', 'Wave number to evaluate (integer >= 1)')
|
|
326
|
+
.option('--json', 'Emit the result document as JSON instead of a text report')
|
|
327
|
+
.action(async (roadmapArg, options) => {
|
|
328
|
+
try {
|
|
329
|
+
const exitCode = await runBarrier(roadmapArg, options.wave, !!options.json)
|
|
330
|
+
process.exit(exitCode)
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (err instanceof UsageError) {
|
|
333
|
+
process.stderr.write(`trackfw barrier: ${err.message}\n`)
|
|
334
|
+
process.exit(2)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
process.stderr.write(`trackfw barrier: unexpected error: ${err && err.message ? err.message : err}\n`)
|
|
338
|
+
process.exit(2)
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
return cmd
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = createBarrierCommand()
|
|
346
|
+
module.exports.UsageError = UsageError
|
|
347
|
+
module.exports.resolveRoadmapFile = resolveRoadmapFile
|
|
348
|
+
module.exports.findWave = findWave
|
|
349
|
+
module.exports.findMLs = findMLs
|
|
350
|
+
module.exports.mlCompletionStatus = mlCompletionStatus
|
|
351
|
+
module.exports.mlAcceptanceEvidence = mlAcceptanceEvidence
|
|
352
|
+
module.exports.parseGates = parseGates
|
|
353
|
+
module.exports.evalMlsComplete = evalMlsComplete
|
|
354
|
+
module.exports.evalAcceptanceEvidence = evalAcceptanceEvidence
|
|
355
|
+
module.exports.evalGates = evalGates
|
|
356
|
+
module.exports.evalValidate = evalValidate
|
|
357
|
+
module.exports.buildDoc = buildDoc
|
|
358
|
+
module.exports.runBarrier = runBarrier
|
package/src/commands/help.js
CHANGED
|
@@ -247,22 +247,112 @@ function describeKey(key) {
|
|
|
247
247
|
].join('\n')
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Retorna a listagem de comandos disponíveis a partir do programa raiz.
|
|
252
|
+
* @param {import('commander').Command} root
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
function listCommands(root) {
|
|
256
|
+
const visible = root.commands.filter((sub) => !sub._hidden)
|
|
257
|
+
const nameWidth = Math.max(...visible.map((sub) => sub.name().length)) + 2
|
|
258
|
+
const lines = ['Comandos disponíveis:']
|
|
259
|
+
for (const sub of visible) {
|
|
260
|
+
lines.push(` ${sub.name().padEnd(nameWidth)}${sub.description()}`)
|
|
261
|
+
}
|
|
262
|
+
return lines.join('\n')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Distância de edição (Levenshtein) entre duas strings.
|
|
267
|
+
* @param {string} a
|
|
268
|
+
* @param {string} b
|
|
269
|
+
* @returns {number}
|
|
270
|
+
*/
|
|
271
|
+
function levenshtein(a, b) {
|
|
272
|
+
const la = a.length
|
|
273
|
+
const lb = b.length
|
|
274
|
+
let prev = new Array(lb + 1)
|
|
275
|
+
let curr = new Array(lb + 1)
|
|
276
|
+
for (let j = 0; j <= lb; j++) prev[j] = j
|
|
277
|
+
for (let i = 1; i <= la; i++) {
|
|
278
|
+
curr[0] = i
|
|
279
|
+
for (let j = 1; j <= lb; j++) {
|
|
280
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
281
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost)
|
|
282
|
+
}
|
|
283
|
+
const tmp = prev
|
|
284
|
+
prev = curr
|
|
285
|
+
curr = tmp
|
|
286
|
+
}
|
|
287
|
+
return prev[lb]
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Retorna o comando ou chave de configuração conhecida mais próxima de
|
|
292
|
+
* topic (distância de edição <= 3), ou null se nenhuma for próxima o
|
|
293
|
+
* suficiente.
|
|
294
|
+
* @param {string} topic
|
|
295
|
+
* @param {import('commander').Command} root
|
|
296
|
+
* @returns {string|null}
|
|
297
|
+
*/
|
|
298
|
+
function suggestTopic(topic, root) {
|
|
299
|
+
const threshold = 3
|
|
300
|
+
const candidates = [
|
|
301
|
+
...root.commands.filter((sub) => !sub._hidden).map((sub) => sub.name()),
|
|
302
|
+
...Object.keys(configDocs)
|
|
303
|
+
]
|
|
304
|
+
let best = null
|
|
305
|
+
let bestDist = threshold + 1
|
|
306
|
+
for (const candidate of candidates) {
|
|
307
|
+
const dist = levenshtein(topic, candidate)
|
|
308
|
+
if (dist < bestDist) {
|
|
309
|
+
bestDist = dist
|
|
310
|
+
best = candidate
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return bestDist <= threshold ? best : null
|
|
314
|
+
}
|
|
315
|
+
|
|
250
316
|
const cmd = new Command('help')
|
|
251
|
-
cmd.description('Exibe documentação das
|
|
252
|
-
cmd.argument('[
|
|
253
|
-
cmd.action((
|
|
254
|
-
|
|
317
|
+
cmd.description('Exibe ajuda de comandos e documentação das chaves de configuração do trackfw.yaml')
|
|
318
|
+
cmd.argument('[topic]', 'comando ou chave de configuração para detalhar')
|
|
319
|
+
cmd.action(function (topic, options, thisCommand) {
|
|
320
|
+
const self = thisCommand || this
|
|
321
|
+
const root = self.parent || self
|
|
322
|
+
|
|
323
|
+
if (!topic) {
|
|
324
|
+
console.log(listCommands(root))
|
|
325
|
+
console.log()
|
|
255
326
|
console.log(listKeys())
|
|
256
327
|
return
|
|
257
328
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
329
|
+
|
|
330
|
+
// 1) comando conhecido → ajuda do comando.
|
|
331
|
+
const sub = root.commands.find((c) => c.name() === topic)
|
|
332
|
+
if (sub) {
|
|
333
|
+
console.log(sub.helpInformation().trimEnd())
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 2) chave de configuração conhecida → documentação da chave.
|
|
338
|
+
const output = describeKey(topic)
|
|
339
|
+
if (output) {
|
|
340
|
+
console.log(output)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 3) assunto desconhecido → erro com sugestão útil.
|
|
345
|
+
let msg = `assunto desconhecido: ${topic}`
|
|
346
|
+
const suggestion = suggestTopic(topic, root)
|
|
347
|
+
if (suggestion) {
|
|
348
|
+
msg += `\nVocê quis dizer: ${suggestion}?`
|
|
262
349
|
}
|
|
263
|
-
console.
|
|
350
|
+
console.error(msg)
|
|
351
|
+
process.exitCode = 1
|
|
264
352
|
})
|
|
265
353
|
|
|
266
354
|
module.exports = cmd
|
|
267
355
|
module.exports.listKeys = listKeys
|
|
268
356
|
module.exports.describeKey = describeKey
|
|
357
|
+
module.exports.listCommands = listCommands
|
|
358
|
+
module.exports.suggestTopic = suggestTopic
|
package/src/commands/index.js
CHANGED
|
@@ -31,6 +31,7 @@ function createProgram() {
|
|
|
31
31
|
program.addCommand(require('./skills'))
|
|
32
32
|
program.addCommand(require('./note'))
|
|
33
33
|
program.addCommand(require('./ship'))
|
|
34
|
+
program.addCommand(require('./barrier'))
|
|
34
35
|
|
|
35
36
|
const { createServeCommand } = require('./serve')
|
|
36
37
|
program.addCommand(createServeCommand())
|