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 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.1.0",
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
@@ -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 keys configuráveis do trackfw.yaml')
252
- cmd.argument('[key]', 'key específica para detalhar')
253
- cmd.action((key) => {
254
- if (!key) {
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
- const output = describeKey(key)
259
- if (!output) {
260
- console.error(`chave desconhecida: ${key}`)
261
- process.exit(1)
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.log(output)
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
@@ -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())