trackfw 7.2.0 → 7.4.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 +67 -1
- package/package.json +1 -1
- package/src/commands/adr.js +2 -1
- package/src/commands/audit-surface.js +520 -0
- package/src/commands/barrier.js +400 -34
- package/src/commands/context.js +42 -36
- package/src/commands/discover.js +72 -21
- package/src/commands/doctor.js +37 -4
- package/src/commands/index.js +1 -0
- package/src/commands/init.js +2 -1
- package/src/commands/integrations.js +22 -4
- package/src/commands/req.js +2 -1
- package/src/commands/thirdparty.js +8 -2
- package/src/commands/update-harness.js +6 -3
- package/src/commands/update.js +147 -10
- package/src/config/index.js +82 -2
- package/src/generators/hooks.js +139 -6
- package/src/generators/init.js +173 -21
- package/src/generators/req.js +15 -53
- package/src/generators/roadmap.js +132 -28
- package/src/homedir.js +30 -0
- package/src/integrations/assets/agents/architect.md +5 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/doctor_remote.js +224 -0
- package/src/integrations/index.js +2 -1
- package/src/integrations/manager.js +10 -2
- package/src/integrations/render.js +39 -8
- package/src/integrations/scaffold_doctor.js +395 -0
- package/src/lib/pathfmt.js +43 -0
- package/src/lib/update-engine.js +28 -3
- package/src/serve/api_board.js +11 -10
- package/src/serve/api_chain.js +13 -17
- package/src/serve/api_metrics.js +2 -8
- package/src/validator/index.js +768 -167
package/src/generators/init.js
CHANGED
|
@@ -4,6 +4,17 @@ const fs = require('fs')
|
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const os = require('os')
|
|
6
6
|
const { readAgentConventions } = require('../config/index.js')
|
|
7
|
+
const { homedir } = require('../homedir');
|
|
8
|
+
|
|
9
|
+
// PACKAGE_VERSION — the version of the trackfw binary generating the CI
|
|
10
|
+
// workflow templates. Read once from package.json (never a literal), so
|
|
11
|
+
// the pin written into the workflow always tracks the generator that
|
|
12
|
+
// wrote it (ADR-2026-08-28: gate de CI nasce pinado na versão que o
|
|
13
|
+
// gerou). Reused by buildGitHubActionsWorkflowContent /
|
|
14
|
+
// buildGitLabCIWorkflowContent AND by scaffold doctor's comparison
|
|
15
|
+
// (which calls the same functions), so drift between write and compare
|
|
16
|
+
// paths is structurally impossible.
|
|
17
|
+
const { version: PACKAGE_VERSION } = require('../../package.json')
|
|
7
18
|
|
|
8
19
|
const GOV_DIRS = [
|
|
9
20
|
'docs/adr',
|
|
@@ -31,6 +42,7 @@ async function scaffold(cfg, cwd) {
|
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
generateVaultIndex(root)
|
|
45
|
+
generateGitAttributes(root)
|
|
34
46
|
writeTrackfwConfig(cfg, root)
|
|
35
47
|
generateValidateScript(cfg, root)
|
|
36
48
|
generateAttentionScripts(cfg, root)
|
|
@@ -45,6 +57,60 @@ async function scaffold(cfg, cwd) {
|
|
|
45
57
|
injectHooksDetected(root)
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
// gitAttributesRuleTarget — primeiro campo da regra mantida por este gerador.
|
|
61
|
+
// Casa o BASENAME (padrão sem barra) de propósito: `roadmap_dir` e `req_dir` são
|
|
62
|
+
// configuráveis por projeto (trackfw.yaml) e ambos carregam um `.trackfw-log`,
|
|
63
|
+
// então caminho fixo nasceria quebrado em quem configurou diretório diferente.
|
|
64
|
+
const GITATTRIBUTES_RULE_TARGET = '.trackfw-log'
|
|
65
|
+
|
|
66
|
+
// GITATTRIBUTES_BLOCK — byte-idêntico ao de internal/generators/scaffold.go e
|
|
67
|
+
// pypi/trackfw/generators/init_gen.py (regra dura de paridade).
|
|
68
|
+
const GITATTRIBUTES_BLOCK = `# trackfw: .trackfw-log is append-only — every write lands on the last line, so
|
|
69
|
+
# two parallel branches conflict on every merge. merge=union keeps the lines
|
|
70
|
+
# from both sides (chronological order is not guaranteed). The pattern has no
|
|
71
|
+
# slash, so it matches the file in any directory — roadmap_dir and req_dir both
|
|
72
|
+
# carry one, and both are configurable per project.
|
|
73
|
+
.trackfw-log merge=union
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* hasGitAttributesRule — verdadeiro quando ALGUMA linha não-comentário tem
|
|
78
|
+
* `.trackfw-log` como primeiro campo. Predicado sobre o CAMPO, não sobre a
|
|
79
|
+
* string literal da linha inteira: espaçamento diferente ou outro atributo já
|
|
80
|
+
* são "a regra existe", e reescrever por cima sobrescreveria decisão do projeto.
|
|
81
|
+
* @param {string} content
|
|
82
|
+
*/
|
|
83
|
+
function hasGitAttributesRule(content) {
|
|
84
|
+
for (const line of content.split('\n')) {
|
|
85
|
+
const trimmed = line.trim()
|
|
86
|
+
if (trimmed === '' || trimmed.startsWith('#')) continue
|
|
87
|
+
if (trimmed.split(/\s+/)[0] === GITATTRIBUTES_RULE_TARGET) return true
|
|
88
|
+
}
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* generateGitAttributes — garante `merge=union` para o `.trackfw-log` no
|
|
94
|
+
* `.gitattributes` da raiz. Três ramos, todos idempotentes: ausente → cria;
|
|
95
|
+
* existe sem a regra → APPEND (nunca sobrescreve); existe com a regra → no-op.
|
|
96
|
+
* @param {string} root
|
|
97
|
+
*/
|
|
98
|
+
function generateGitAttributes(root) {
|
|
99
|
+
const filePath = path.join(root || process.cwd(), '.gitattributes')
|
|
100
|
+
if (!fs.existsSync(filePath)) {
|
|
101
|
+
fs.writeFileSync(filePath, GITATTRIBUTES_BLOCK, 'utf8')
|
|
102
|
+
console.log(' ✓ .gitattributes')
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const existing = fs.readFileSync(filePath, 'utf8')
|
|
106
|
+
if (hasGitAttributesRule(existing)) return
|
|
107
|
+
// Arquivo preexistente sem newline final: emendar direto grudaria a primeira
|
|
108
|
+
// linha do bloco na última linha do projeto, corrompendo a config em silêncio.
|
|
109
|
+
const prefix = existing !== '' && !existing.endsWith('\n') ? existing + '\n' : existing
|
|
110
|
+
fs.writeFileSync(filePath, prefix + GITATTRIBUTES_BLOCK, 'utf8')
|
|
111
|
+
console.log(' ✓ .gitattributes')
|
|
112
|
+
}
|
|
113
|
+
|
|
48
114
|
/**
|
|
49
115
|
* generateVaultIndex — cria vault/notes/index.md se ainda não existir.
|
|
50
116
|
* @param {string} root
|
|
@@ -123,6 +189,10 @@ function generateValidateScript(cfg, cwd) {
|
|
|
123
189
|
const script = buildValidateScript(cfg)
|
|
124
190
|
const scriptPath = path.join(root, 'scripts', 'trackfw-validate.sh')
|
|
125
191
|
fs.writeFileSync(scriptPath, script, { encoding: 'utf8', mode: 0o755 })
|
|
192
|
+
// AC9 (REQ-2026-08-28): writeFileSync's {mode} option applies only on O_CREAT;
|
|
193
|
+
// for an existing file the mode is not changed. fs.chmodSync restores 0o755
|
|
194
|
+
// unconditionally, matching Python's os.chmod behavior (which was already correct).
|
|
195
|
+
fs.chmodSync(scriptPath, 0o755)
|
|
126
196
|
console.log(` ✓ ${path.join('scripts', 'trackfw-validate.sh')}`)
|
|
127
197
|
}
|
|
128
198
|
|
|
@@ -201,17 +271,34 @@ function generateCIWorkflow(cfg) {
|
|
|
201
271
|
}
|
|
202
272
|
}
|
|
203
273
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
274
|
+
// buildGitHubActionsWorkflowContent returns the byte-identical content the GitHub Actions
|
|
275
|
+
// workflow generator would write. Exported for scaffold doctor (ADR-2026-08-27): the
|
|
276
|
+
// comparison uses the same function the write path uses, so drift is structurally impossible.
|
|
277
|
+
// The cfg parameter is accepted for API symmetry with Go's buildGitHubActionsWorkflowContent
|
|
278
|
+
// (which also ignores cfg for now — the template is cfg-independent but, since
|
|
279
|
+
// ADR-2026-08-28, NOT version-independent: it pins TRACKFW_VERSION to the version of the
|
|
280
|
+
// trackfw binary that generated it, read from package.json — never a literal).
|
|
281
|
+
//
|
|
282
|
+
// Job id is `governance-install-script` (ML-1A, ROADMAP-2026-09-01): was `governance`,
|
|
283
|
+
// colliding with the discover-installed sibling workflow's job id (trackfw-validate.yml,
|
|
284
|
+
// buildDiscoverGitHubActionsWorkflowContent in commands/discover.js) — both previously
|
|
285
|
+
// `governance`, producing two identically-named check-runs on any PR of a project with
|
|
286
|
+
// both workflows installed, which makes required_status_checks ambiguous (GitHub
|
|
287
|
+
// matches by name). Named after the install mechanism (install.sh here, `go install`
|
|
288
|
+
// there) rather than the workflow file, since that's what a required_status_checks
|
|
289
|
+
// reader needs to tell the two apart without opening the YAML.
|
|
290
|
+
function buildGitHubActionsWorkflowContent(_cfg) {
|
|
291
|
+
return `name: trackfw-gate
|
|
208
292
|
on:
|
|
209
293
|
pull_request:
|
|
210
294
|
branches: [main]
|
|
211
295
|
|
|
212
296
|
jobs:
|
|
213
|
-
governance:
|
|
297
|
+
governance-install-script:
|
|
214
298
|
runs-on: ubuntu-latest
|
|
299
|
+
timeout-minutes: 10
|
|
300
|
+
env:
|
|
301
|
+
TRACKFW_VERSION: "${PACKAGE_VERSION}"
|
|
215
302
|
steps:
|
|
216
303
|
- uses: actions/checkout@v4
|
|
217
304
|
|
|
@@ -222,17 +309,18 @@ jobs:
|
|
|
222
309
|
- name: Governance gate
|
|
223
310
|
run: trackfw validate
|
|
224
311
|
`
|
|
225
|
-
|
|
226
|
-
const filePath = '.github/workflows/trackfw-gate.yml'
|
|
227
|
-
fs.writeFileSync(filePath, content, 'utf8')
|
|
228
|
-
console.log(` ✓ ${filePath}`)
|
|
229
312
|
}
|
|
230
313
|
|
|
231
|
-
|
|
232
|
-
|
|
314
|
+
// buildGitLabCIWorkflowContent mirrors buildGitHubActionsWorkflowContent for GitLab CI.
|
|
315
|
+
// Same version-pin rationale as above — see buildGitHubActionsWorkflowContent.
|
|
316
|
+
function buildGitLabCIWorkflowContent(_cfg) {
|
|
317
|
+
return `# trackfw governance gate
|
|
233
318
|
trackfw-gate:
|
|
234
319
|
stage: test
|
|
235
320
|
image: alpine:latest
|
|
321
|
+
timeout: 10 minutes
|
|
322
|
+
variables:
|
|
323
|
+
TRACKFW_VERSION: "${PACKAGE_VERSION}"
|
|
236
324
|
before_script:
|
|
237
325
|
- apk add --no-cache curl
|
|
238
326
|
- curl -sSfL https://github.com/kgsaran/trackfw/releases/latest/download/install.sh | sh
|
|
@@ -241,8 +329,17 @@ trackfw-gate:
|
|
|
241
329
|
only:
|
|
242
330
|
- merge_requests
|
|
243
331
|
`
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function generateGitHubActionsWorkflow() {
|
|
335
|
+
fs.mkdirSync('.github/workflows', { recursive: true })
|
|
336
|
+
const filePath = '.github/workflows/trackfw-gate.yml'
|
|
337
|
+
fs.writeFileSync(filePath, buildGitHubActionsWorkflowContent(null), 'utf8')
|
|
338
|
+
console.log(` ✓ ${filePath}`)
|
|
339
|
+
}
|
|
244
340
|
|
|
245
|
-
|
|
341
|
+
function generateGitLabCIWorkflow() {
|
|
342
|
+
fs.writeFileSync('.gitlab-ci-trackfw.yml', buildGitLabCIWorkflowContent(null), 'utf8')
|
|
246
343
|
console.log(' ✓ .gitlab-ci-trackfw.yml')
|
|
247
344
|
}
|
|
248
345
|
|
|
@@ -320,9 +417,12 @@ function generateCommitMsgHook(cfg) {
|
|
|
320
417
|
// ---------------------------------------------------------------------------
|
|
321
418
|
|
|
322
419
|
function generatePomXml(cfg) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
420
|
+
// Usa o toSlug compartilhado em vez de uma variante inline: a versao anterior nao
|
|
421
|
+
// fazia NFKD e PERDIA a letra acentuada — "Cafe App" (com acento) virava "caf-app"
|
|
422
|
+
// aqui e "cafe-app" no Go, que usa o toSlug dele. Ver a secao
|
|
423
|
+
// "Artifact slug contract" em docs/cli-parity.md.
|
|
424
|
+
const { toSlug } = require('./adr')
|
|
425
|
+
const slug = cfg.projectName ? toSlug(cfg.projectName) : 'my-app'
|
|
326
426
|
const name = cfg.projectName || 'My App'
|
|
327
427
|
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
328
428
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
@@ -452,7 +552,7 @@ Delete the file when resolved. Visible as a live banner in \`trackfw serve\`.
|
|
|
452
552
|
- **Docker + .env from day 1:** containerize early; all config via env vars
|
|
453
553
|
- **2-layer validation:** frontend (UX) + backend (security) — never only one
|
|
454
554
|
- **API-first:** define OpenAPI contract before coding frontend/backend integration
|
|
455
|
-
- **
|
|
555
|
+
- **Threat model waves:** every feature roadmap opens with a Wave 0 threat model (before implementation) and closes with a red-team review wave (before release)
|
|
456
556
|
- **Test coverage:** TDD for critical logic; min 60% (prototype) / 80% (production)
|
|
457
557
|
- Use \`/trackfw:architect\` to define stack before the first REQ
|
|
458
558
|
` + conventionsSection + `
|
|
@@ -776,6 +876,13 @@ function backendCommands(cfg) {
|
|
|
776
876
|
// (missing roadmap.md, implement.md, barrier.md) undetected.
|
|
777
877
|
// ---------------------------------------------------------------------------
|
|
778
878
|
|
|
879
|
+
// CLAUDE_COMMANDS "trackfw não está instalado" blurbs (AC13) are deliberately
|
|
880
|
+
// OUT of the version pin introduced by ADR-2026-08-28. This text is shown when
|
|
881
|
+
// the `trackfw` command fails because it is not installed — recommending the
|
|
882
|
+
// exact old version that generated the project would be worse UX than
|
|
883
|
+
// pointing at `releases/latest`, since the reason to (re)install is usually
|
|
884
|
+
// being outdated. Do not pin these on a future pass without a product
|
|
885
|
+
// decision; see ROADMAP-2026-08-28 Wave 0 §3 (AC13 row).
|
|
779
886
|
const CLAUDE_COMMANDS = {
|
|
780
887
|
'adr.md': `Execute o seguinte comando bash: \`trackfw adr new "$ARGUMENTS"\`
|
|
781
888
|
|
|
@@ -855,7 +962,7 @@ trackfw não está instalado. Instale com:
|
|
|
855
962
|
3. **Gerar o roadmap**
|
|
856
963
|
Com base no conteúdo da REQ, gere um roadmap seguindo **estritamente** este formato:
|
|
857
964
|
|
|
858
|
-
|
|
965
|
+
\`\`\`\`markdown
|
|
859
966
|
---
|
|
860
967
|
status: backlog
|
|
861
968
|
date: <YYYY-MM-DD>
|
|
@@ -870,6 +977,28 @@ trackfw não está instalado. Instale com:
|
|
|
870
977
|
## Diagnóstico / Contexto
|
|
871
978
|
<resumo do problema, motivação e escopo extraídos da REQ>
|
|
872
979
|
|
|
980
|
+
## Wave 0 — Threat Model
|
|
981
|
+
> Dependencies: none. Blocks all implementation.
|
|
982
|
+
|
|
983
|
+
### ML-0A — Threat model for this roadmap
|
|
984
|
+
**Status:** pending
|
|
985
|
+
**Files affected:**
|
|
986
|
+
**Actions:**
|
|
987
|
+
1. Enumeration completeness — is the list of surfaces in this roadmap complete? Name what is missing, or show the list is closed. Do not limit the search to the files already named by the REQ — before declaring the list closed, search the repository for other places that emit the same artifact or the same pattern (for example, grep for the literal the final artifact contains).
|
|
988
|
+
2. Threat model — who empties this Wave 0 without breaking any written rule, and how?
|
|
989
|
+
3. Falsification targets in both directions — for each surface, what breaks when the behavior regresses, and what breaks when it regresses the opposite way?
|
|
990
|
+
4. Declared residual — what this design accepts not covering.
|
|
991
|
+
**Acceptance criteria:**
|
|
992
|
+
- [ ] The four sections above answered with evidence, not a one-line assertion
|
|
993
|
+
- [ ] No implementation line written for this ML
|
|
994
|
+
|
|
995
|
+
**Gates da wave:**
|
|
996
|
+
\`\`\`bash
|
|
997
|
+
# Wave 0 gate — replace this placeholder with a project-specific check before
|
|
998
|
+
# marking ML-0A done. Do not remove the gate; replace its command (AC13).
|
|
999
|
+
exit 1 # placeholder gate fails closed until ML-0A replaces it — see docs/cli-parity.md
|
|
1000
|
+
\`\`\`
|
|
1001
|
+
|
|
873
1002
|
## Wave 1 — <nome descritivo> (<N> MLs em paralelo)
|
|
874
1003
|
> Dependências: Independente
|
|
875
1004
|
|
|
@@ -890,7 +1019,7 @@ trackfw não está instalado. Instale com:
|
|
|
890
1019
|
## Wave 2 — <nome> (depende de Wave 1)
|
|
891
1020
|
> Dependências: Wave 1 completa
|
|
892
1021
|
...
|
|
893
|
-
|
|
1022
|
+
\`\`\`\`
|
|
894
1023
|
|
|
895
1024
|
**Princípios obrigatórios:**
|
|
896
1025
|
- MLs dentro da mesma Wave são **independentes** (arquivos distintos, sem conflito)
|
|
@@ -1032,9 +1161,19 @@ Próximo passo: abrir PR com gh pr create
|
|
|
1032
1161
|
|
|
1033
1162
|
Execute primeiro:
|
|
1034
1163
|
\`\`\`bash
|
|
1035
|
-
trackfw barrier <roadmap> --wave <n> --json
|
|
1164
|
+
trackfw barrier <roadmap> --wave <n> --trust-local-gates --json
|
|
1036
1165
|
\`\`\`
|
|
1037
1166
|
|
|
1167
|
+
\`--trust-local-gates\` é obrigatório aqui: roadmaps WIP (modificados localmente, ainda não commitados em
|
|
1168
|
+
\`origin/main\`) são marcados como não confiáveis pela CLI direta por padrão, como proteção contra a
|
|
1169
|
+
execução de gates de roadmaps chegados por PR de terceiro. O slash command aplica esse flag porque
|
|
1170
|
+
ele representa o fluxo legítimo do arquiteto operando no próprio repositório — não porque os gates
|
|
1171
|
+
são inspecionados previamente (o diff ainda é responsabilidade do checklist abaixo).
|
|
1172
|
+
|
|
1173
|
+
⚠️ **Não use \`--trust-local-gates\` ao revisar um roadmap chegado por PR de terceiro** — use a CLI
|
|
1174
|
+
direta sem o flag (\`trackfw barrier <roadmap> --wave <n> --json\`) para que os gates sejam marcados
|
|
1175
|
+
como \`not_evaluated\` e não executados.
|
|
1176
|
+
|
|
1038
1177
|
Este comando é **necessário mas não suficiente**. Ele verifica MLs concluídos, evidências e \`trackfw validate\`, mas não substitui as inspeções especializadas nem a auditoria de diff abaixo — nenhuma delas é avaliada pelo binário. Consulte a seção \`trackfw barrier\` em \`docs/cli-parity.md\` para o contrato completo (estados, exit codes, saída JSON).
|
|
1039
1178
|
|
|
1040
1179
|
Se o comando retornar exit code não-zero (\`blocked\` ou erro de resolução): pare, reporte a falha ao usuário e não prossiga no checklist até que a wave passe.
|
|
@@ -1258,8 +1397,11 @@ async function installAmazonQ(cwd = process.cwd()) {
|
|
|
1258
1397
|
*/
|
|
1259
1398
|
async function installIntegrationTarget(target, cwd = process.cwd(), scope = 'project', { onSkip } = {}) {
|
|
1260
1399
|
const { execute, buildPlans } = require('../integrations')
|
|
1400
|
+
const { resolveAgentModels } = require('../config')
|
|
1401
|
+
const { models: agentModels, warning: agentModelsWarning } = resolveAgentModels(scope, homedir(), cwd)
|
|
1402
|
+
if (agentModelsWarning) process.stderr.write(agentModelsWarning + '\n')
|
|
1261
1403
|
const roots = { projectRoot: cwd }
|
|
1262
|
-
const options = { targets: [target], scope, onSkip }
|
|
1404
|
+
const options = { targets: [target], scope, onSkip, agentModels }
|
|
1263
1405
|
// D5 — transparency: print resolved destinations before writing anything.
|
|
1264
1406
|
// buildPlans has no side effects, so it is safe to call here purely to
|
|
1265
1407
|
// enumerate destinations; the actual write happens below via execute().
|
|
@@ -1292,7 +1434,7 @@ function generateClaudeCommandsForce(rootDir) {
|
|
|
1292
1434
|
* installSkillsForce — sobrescreve ~/.claude/skills/trackfw/SKILL.md sempre.
|
|
1293
1435
|
*/
|
|
1294
1436
|
function installSkillsForce() {
|
|
1295
|
-
const skillDir = path.join(
|
|
1437
|
+
const skillDir = path.join(homedir(), '.claude', 'skills', 'trackfw')
|
|
1296
1438
|
fs.mkdirSync(skillDir, { recursive: true })
|
|
1297
1439
|
|
|
1298
1440
|
const content = `---
|
|
@@ -1370,6 +1512,8 @@ module.exports = {
|
|
|
1370
1512
|
generateClaudeCommands,
|
|
1371
1513
|
generateClaudeCommandsForce,
|
|
1372
1514
|
generateVaultIndex,
|
|
1515
|
+
generateGitAttributes,
|
|
1516
|
+
GITATTRIBUTES_BLOCK,
|
|
1373
1517
|
installSkillsForce,
|
|
1374
1518
|
installAgents,
|
|
1375
1519
|
installGemini,
|
|
@@ -1383,4 +1527,12 @@ module.exports = {
|
|
|
1383
1527
|
injectHooksDetected,
|
|
1384
1528
|
trackfwRulesBlock,
|
|
1385
1529
|
printArchitectNextSteps,
|
|
1530
|
+
// Template constants and builders — exported for scaffold doctor (ADR-2026-08-27).
|
|
1531
|
+
// Same single-source-of-truth principle as hooks.js exports: comparing against the
|
|
1532
|
+
// same object the write path uses makes drift structurally impossible.
|
|
1533
|
+
CLAUDE_COMMANDS,
|
|
1534
|
+
buildValidateScript,
|
|
1535
|
+
// CI workflow content builders (inline content from generateGitHub/GitLabCIWorkflow)
|
|
1536
|
+
buildGitHubActionsWorkflowContent,
|
|
1537
|
+
buildGitLabCIWorkflowContent,
|
|
1386
1538
|
}
|
package/src/generators/req.js
CHANGED
|
@@ -4,66 +4,21 @@ const path = require('path')
|
|
|
4
4
|
const { localDateISO } = require('./date')
|
|
5
5
|
const roadmapGen = require('./roadmap')
|
|
6
6
|
const config = require('../config')
|
|
7
|
+
const { resolveAgentNamespaces, resolveReqFiles, reqWriteDir } = require('../validator')
|
|
8
|
+
const { normalizeCRLF } = require('../integrations/render')
|
|
7
9
|
|
|
8
10
|
const VALID_STATES = roadmapGen.VALID_STATES
|
|
9
11
|
const STATE_ORDER = roadmapGen.STATE_ORDER
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
* listREQFiles —
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Os três conjuntos não são mutuamente exclusivos — concatena todos.
|
|
14
|
+
* listREQFiles — NÃO reimplementa a descoberta: delega ao ponto único de leitura do validador
|
|
15
|
+
* (resolveReqFiles — ADR-2026-09-03, D3/D4), o mesmo consumido pelas regras de validate. Duas
|
|
16
|
+
* noções de layout no mesmo runtime foram a causa do defeito da REQ-2026-08-30.
|
|
16
17
|
* @param {object} cfg — config completo (ver npm/src/config)
|
|
17
|
-
* @returns {string[]} paths completos,
|
|
18
|
+
* @returns {string[]} paths completos, união dos 4 layouts, deduplicados e ordenados
|
|
18
19
|
*/
|
|
19
20
|
function listREQFiles(cfg) {
|
|
20
|
-
|
|
21
|
-
const files = []
|
|
22
|
-
|
|
23
|
-
// (a) flat legado — reqDir/*.md
|
|
24
|
-
try {
|
|
25
|
-
for (const f of fs.readdirSync(reqDir)) {
|
|
26
|
-
if (!f.endsWith('.md')) continue
|
|
27
|
-
const full = path.join(reqDir, f)
|
|
28
|
-
try {
|
|
29
|
-
if (!fs.statSync(full).isDirectory()) files.push(full)
|
|
30
|
-
} catch (_) { /* ignora */ }
|
|
31
|
-
}
|
|
32
|
-
} catch (_) { /* reqDir não existe */ }
|
|
33
|
-
|
|
34
|
-
// (b) por-estado, sem agente — reqDir/<estado>/*.md
|
|
35
|
-
for (const state of STATE_ORDER) {
|
|
36
|
-
const dir = path.join(reqDir, state)
|
|
37
|
-
try {
|
|
38
|
-
for (const f of fs.readdirSync(dir)) {
|
|
39
|
-
if (f.endsWith('.md')) files.push(path.join(dir, f))
|
|
40
|
-
}
|
|
41
|
-
} catch (_) { /* estado não existe */ }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// (c) by_agent — reqDir/<agente>/<estado>/*.md
|
|
45
|
-
if (cfg.roadmapNamespacing === config.NAMESPACING_BY_AGENT) {
|
|
46
|
-
let agents = cfg.agents || []
|
|
47
|
-
if (agents.length === 0) {
|
|
48
|
-
try {
|
|
49
|
-
agents = fs.readdirSync(reqDir).filter(f => {
|
|
50
|
-
try { return fs.statSync(path.join(reqDir, f)).isDirectory() } catch (_) { return false }
|
|
51
|
-
})
|
|
52
|
-
} catch (_) { agents = [] }
|
|
53
|
-
}
|
|
54
|
-
for (const agent of agents) {
|
|
55
|
-
for (const state of STATE_ORDER) {
|
|
56
|
-
const dir = path.join(reqDir, agent, state)
|
|
57
|
-
try {
|
|
58
|
-
for (const f of fs.readdirSync(dir)) {
|
|
59
|
-
if (f.endsWith('.md')) files.push(path.join(dir, f))
|
|
60
|
-
}
|
|
61
|
-
} catch (_) { /* agente/estado não existe */ }
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return files
|
|
21
|
+
return resolveReqFiles(cfg)
|
|
67
22
|
}
|
|
68
23
|
|
|
69
24
|
/**
|
|
@@ -114,7 +69,11 @@ function parseREQStatus(filepath) {
|
|
|
114
69
|
return 'unknown'
|
|
115
70
|
}
|
|
116
71
|
|
|
72
|
+
// D1/D3 (ADR-2026-09-04-parser-de-frontmatter-tolera-crlf-na-fronteira-de-entrada, ML-5B):
|
|
73
|
+
// normaliza CRLF -> LF via normalizeCRLF na própria entrada, antes de qualquer casamento de
|
|
74
|
+
// "---\n" — reaproveita a única implementação Node, sem criar uma segunda cópia.
|
|
117
75
|
function rewriteREQStatus(source, status) {
|
|
76
|
+
source = normalizeCRLF(String(source))
|
|
118
77
|
if (!source.startsWith('---\n')) return { content: source, changed: false }
|
|
119
78
|
const end = source.slice(4).indexOf('\n---')
|
|
120
79
|
if (end < 0) return { content: source, changed: false }
|
|
@@ -286,7 +245,10 @@ function toSlug(s) {
|
|
|
286
245
|
* @returns {Promise<void>}
|
|
287
246
|
*/
|
|
288
247
|
async function newREQ(content) {
|
|
289
|
-
|
|
248
|
+
// Ponto único de decisão de caminho de ESCRITA (ADR-2026-09-03, D2/D4): by_agent grava no
|
|
249
|
+
// canônico req_dir/<agente>/; flat grava em req_dir/ — o mesmo ponto que alimenta a união de leitura.
|
|
250
|
+
const cfg = require('../config').load()
|
|
251
|
+
const reqDir = reqWriteDir(cfg) || cfg.reqDir
|
|
290
252
|
fs.mkdirSync(reqDir, { recursive: true })
|
|
291
253
|
|
|
292
254
|
const slug = toSlug(content.title)
|