atlas-workflow 0.9.0 → 0.9.2
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 +2 -2
- package/VERSION +1 -1
- package/build/cli/atlas-init.mjs +12 -14
- package/build/tests/classify-findings.test.mjs +20 -0
- package/build/tests/etapa3.test.mjs +161 -0
- package/build/tests/test_classify_findings.py +79 -0
- package/hosts/opencode/.opencode/agents/atlas-findings-repair.md +4 -0
- package/hosts/opencode/.opencode/agents/atlas-task-validator.md +18 -1
- package/hosts/opencode/.opencode/atlas/VERSION +1 -1
- package/hosts/opencode/.opencode/atlas/orchestrator/README.md +7 -5
- package/hosts/opencode/.opencode/atlas/orchestrator/commands/workflow.md +1 -1
- package/hosts/opencode/.opencode/atlas/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
- package/hosts/opencode/.opencode/atlas/packages/mcp-server/README.md +1 -1
- package/hosts/opencode/.opencode/atlas/packages/mcp-server/package.json +1 -1
- package/hosts/opencode/.opencode/atlas/packages/mcp-server/server.js +446 -14
- package/hosts/opencode/.opencode/atlas/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
- package/hosts/opencode/.opencode/atlas/packages/templates/PRD_TEMPLATE.md +2 -1
- package/hosts/opencode/.opencode/atlas/packages/templates/STATE_FILE_SCHEMA.md +25 -1
- package/hosts/opencode/.opencode/skills/_shared/references/stack-profiles.md +36 -0
- package/hosts/opencode/.opencode/skills/_shared/scripts/document_quality.mjs +252 -0
- package/hosts/opencode/.opencode/skills/atlas-backlog-generator/SKILL.md +7 -2
- package/hosts/opencode/.opencode/skills/atlas-direct-execute/SKILL.md +6 -2
- package/hosts/opencode/.opencode/skills/atlas-findings-repair/SKILL.md +11 -1
- package/hosts/opencode/.opencode/skills/atlas-plan-execute/SKILL.md +16 -2
- package/hosts/opencode/.opencode/skills/atlas-plan-handoff/SKILL.md +6 -4
- package/hosts/opencode/.opencode/skills/atlas-prd-interview/SKILL.md +7 -2
- package/hosts/opencode/.opencode/skills/atlas-slice-review/SKILL.md +37 -2
- package/hosts/opencode/.opencode/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
- package/hosts/opencode/.opencode/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
- package/hosts/opencode/.opencode/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
- package/hosts/opencode/.opencode/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
- package/hosts/opencode/.opencode/skills/atlas-task-validator/SKILL.md +29 -14
- package/hosts/opencode/.opencode/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
- package/hosts/pi/.pi/agents/atlas-direct-execute.md +6 -2
- package/hosts/pi/.pi/agents/atlas-findings-repair.md +15 -1
- package/hosts/pi/.pi/agents/atlas-plan-execute.md +16 -2
- package/hosts/pi/.pi/agents/atlas-slice-review.md +37 -2
- package/hosts/pi/.pi/agents/atlas-task-validator.md +18 -1
- package/hosts/pi/atlas/VERSION +1 -1
- package/hosts/pi/atlas/orchestrator/README.md +7 -5
- package/hosts/pi/atlas/orchestrator/commands/workflow.md +1 -1
- package/hosts/pi/atlas/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
- package/hosts/pi/atlas/packages/mcp-server/README.md +1 -1
- package/hosts/pi/atlas/packages/mcp-server/package.json +1 -1
- package/hosts/pi/atlas/packages/mcp-server/server.js +446 -14
- package/hosts/pi/atlas/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
- package/hosts/pi/atlas/packages/templates/PRD_TEMPLATE.md +2 -1
- package/hosts/pi/atlas/packages/templates/STATE_FILE_SCHEMA.md +25 -1
- package/hosts/pi/skills/_shared/references/stack-profiles.md +36 -0
- package/hosts/pi/skills/_shared/scripts/document_quality.mjs +252 -0
- package/hosts/pi/skills/atlas-backlog-generator/SKILL.md +7 -2
- package/hosts/pi/skills/atlas-direct-execute/SKILL.md +6 -2
- package/hosts/pi/skills/atlas-findings-repair/SKILL.md +11 -1
- package/hosts/pi/skills/atlas-plan-execute/SKILL.md +16 -2
- package/hosts/pi/skills/atlas-plan-handoff/SKILL.md +6 -4
- package/hosts/pi/skills/atlas-prd-interview/SKILL.md +7 -2
- package/hosts/pi/skills/atlas-slice-review/SKILL.md +37 -2
- package/hosts/pi/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
- package/hosts/pi/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
- package/hosts/pi/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
- package/hosts/pi/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
- package/hosts/pi/skills/atlas-task-validator/SKILL.md +29 -14
- package/hosts/pi/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
- package/package.json +1 -1
- package/plugins/atlas-workflow-orchestrator/.codex/agents/atlas-findings-repair.toml +1 -1
- package/plugins/atlas-workflow-orchestrator/.codex/agents/atlas-task-validator.toml +1 -1
- package/plugins/atlas-workflow-orchestrator/.codex-plugin/plugin.json +1 -1
- package/plugins/atlas-workflow-orchestrator/VERSION +1 -1
- package/plugins/atlas-workflow-orchestrator/agents/atlas-findings-repair.md +4 -0
- package/plugins/atlas-workflow-orchestrator/agents/atlas-task-validator.md +18 -1
- package/plugins/atlas-workflow-orchestrator/orchestrator/README.md +7 -5
- package/plugins/atlas-workflow-orchestrator/orchestrator/commands/workflow.md +1 -1
- package/plugins/atlas-workflow-orchestrator/orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
- package/plugins/atlas-workflow-orchestrator/packages/mcp-server/README.md +1 -1
- package/plugins/atlas-workflow-orchestrator/packages/mcp-server/package.json +1 -1
- package/plugins/atlas-workflow-orchestrator/packages/mcp-server/server.js +446 -14
- package/plugins/atlas-workflow-orchestrator/packages/skills/_shared/references/stack-profiles.md +36 -0
- package/plugins/atlas-workflow-orchestrator/packages/skills/_shared/scripts/document_quality.mjs +252 -0
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-backlog-generator/SKILL.md +7 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-direct-execute/SKILL.md +6 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-findings-repair/SKILL.md +11 -1
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-plan-execute/SKILL.md +16 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-plan-handoff/SKILL.md +6 -4
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-prd-interview/SKILL.md +7 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/SKILL.md +37 -2
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
- package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-task-validator/SKILL.md +29 -14
- package/plugins/atlas-workflow-orchestrator/packages/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
- package/plugins/atlas-workflow-orchestrator/packages/templates/PRD_TEMPLATE.md +2 -1
- package/plugins/atlas-workflow-orchestrator/packages/templates/STATE_FILE_SCHEMA.md +25 -1
- package/plugins/atlas-workflow-orchestrator/skills/_shared/references/stack-profiles.md +36 -0
- package/plugins/atlas-workflow-orchestrator/skills/_shared/scripts/document_quality.mjs +252 -0
- package/plugins/atlas-workflow-orchestrator/skills/atlas-backlog-generator/SKILL.md +7 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-direct-execute/SKILL.md +6 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-findings-repair/SKILL.md +11 -1
- package/plugins/atlas-workflow-orchestrator/skills/atlas-plan-execute/SKILL.md +16 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-plan-handoff/SKILL.md +6 -4
- package/plugins/atlas-workflow-orchestrator/skills/atlas-prd-interview/SKILL.md +7 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/SKILL.md +37 -2
- package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/references/scenario-lenses.md +8 -0
- package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/scripts/classify_findings.mjs +60 -0
- package/plugins/atlas-workflow-orchestrator/skills/atlas-slice-review/scripts/classify_findings.py +9 -41
- package/plugins/atlas-workflow-orchestrator/skills/atlas-sprint-prd-generator/SKILL.md +7 -4
- package/plugins/atlas-workflow-orchestrator/skills/atlas-task-validator/SKILL.md +29 -14
- package/plugins/atlas-workflow-orchestrator/skills/atlas-workflow-orchestrator/SKILL.md +22 -17
- package/plugins/atlas-workflow-orchestrator/templates/BACKLOG_MESTRE_TEMPLATE.md +14 -3
- package/plugins/atlas-workflow-orchestrator/templates/PRD_TEMPLATE.md +2 -1
- package/plugins/atlas-workflow-orchestrator/templates/STATE_FILE_SCHEMA.md +25 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export const SEVERITY_ORDER = Object.freeze({ P0: 0, P1: 1, P2: 2, P3: 3 });
|
|
6
|
+
export const REQUIRED_TEXT_FIELDS = Object.freeze([
|
|
7
|
+
'task_id', 'title', 'file', 'failure_mode', 'evidence', 'recommendation', 'fix_validation',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export function normalizeFinding(finding, index) {
|
|
11
|
+
if (!finding || typeof finding !== 'object' || Array.isArray(finding)) {
|
|
12
|
+
throw new Error(`Finding ${index} must be a JSON object`);
|
|
13
|
+
}
|
|
14
|
+
if (!(finding.severity in SEVERITY_ORDER)) {
|
|
15
|
+
throw new Error(`Finding ${index} has invalid severity: ${JSON.stringify(finding.severity)}`);
|
|
16
|
+
}
|
|
17
|
+
const missing = REQUIRED_TEXT_FIELDS.filter(
|
|
18
|
+
(field) => typeof finding[field] !== 'string' || !finding[field].trim(),
|
|
19
|
+
);
|
|
20
|
+
if (missing.length) throw new Error(`Finding ${index} missing required fields: ${missing.join(', ')}`);
|
|
21
|
+
if (!Number.isInteger(finding.line) || finding.line < 1) {
|
|
22
|
+
throw new Error(`Finding ${index} has invalid line: ${JSON.stringify(finding.line)}`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
severity: finding.severity,
|
|
26
|
+
task_id: finding.task_id,
|
|
27
|
+
title: finding.title,
|
|
28
|
+
file: finding.file,
|
|
29
|
+
line: finding.line,
|
|
30
|
+
summary: typeof finding.summary === 'string' ? finding.summary : '',
|
|
31
|
+
failure_mode: finding.failure_mode,
|
|
32
|
+
evidence: finding.evidence,
|
|
33
|
+
recommendation: finding.recommendation,
|
|
34
|
+
fix_validation: finding.fix_validation,
|
|
35
|
+
diff_attributed: finding.diff_attributed !== false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function classifyFindings(payload) {
|
|
40
|
+
if (!Array.isArray(payload)) throw new Error('Findings input must be a JSON array');
|
|
41
|
+
return payload.map(normalizeFinding).sort((a, b) => (
|
|
42
|
+
SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
|
|
43
|
+
|| a.task_id.localeCompare(b.task_id)
|
|
44
|
+
|| a.file.localeCompare(b.file)
|
|
45
|
+
|| a.line - b.line
|
|
46
|
+
));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function run(argv = process.argv.slice(2)) {
|
|
50
|
+
if (argv.length !== 1) throw new Error('Usage: node classify_findings.mjs <findings.json>');
|
|
51
|
+
const payload = JSON.parse(fs.readFileSync(argv[0], 'utf8'));
|
|
52
|
+
process.stdout.write(`${JSON.stringify(classifyFindings(payload), null, 2)}\n`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
56
|
+
try { run(); } catch (error) {
|
|
57
|
+
process.stderr.write(`${error.message}\n`);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,56 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Wrapper legado: delega ao gate Node canônico por uma release."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
import argparse
|
|
7
|
-
import json
|
|
8
6
|
import pathlib
|
|
7
|
+
import subprocess
|
|
9
8
|
import sys
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
SEVERITY_ORDER = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def load_findings(path: pathlib.Path) -> list[dict[str, Any]]:
|
|
16
|
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
17
|
-
if not isinstance(payload, list):
|
|
18
|
-
raise ValueError("Findings input must be a JSON array")
|
|
19
|
-
return payload
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def normalize_finding(finding: dict[str, Any]) -> dict[str, Any]:
|
|
23
|
-
severity = finding.get("severity", "P2")
|
|
24
|
-
if severity not in SEVERITY_ORDER:
|
|
25
|
-
severity = "P2"
|
|
26
|
-
return {
|
|
27
|
-
"severity": severity,
|
|
28
|
-
"task_id": finding.get("task_id", ""),
|
|
29
|
-
"title": finding.get("title", ""),
|
|
30
|
-
"file": finding.get("file", ""),
|
|
31
|
-
"line": finding.get("line"),
|
|
32
|
-
"summary": finding.get("summary", ""),
|
|
33
|
-
"evidence": finding.get("evidence", ""),
|
|
34
|
-
"diff_attributed": bool(finding.get("diff_attributed", True)),
|
|
35
|
-
}
|
|
36
9
|
|
|
37
10
|
|
|
38
11
|
def main() -> int:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
12
|
+
if len(sys.argv) != 2:
|
|
13
|
+
sys.stderr.write("Usage: python classify_findings.py <findings.json>\n")
|
|
14
|
+
return 1
|
|
15
|
+
script = pathlib.Path(__file__).with_name("classify_findings.mjs")
|
|
43
16
|
try:
|
|
44
|
-
|
|
45
|
-
except
|
|
46
|
-
sys.stderr.write(
|
|
17
|
+
return subprocess.run(["node", str(script), sys.argv[1]], check=False).returncode
|
|
18
|
+
except FileNotFoundError:
|
|
19
|
+
sys.stderr.write("Node.js ausente: requisito runtime do Atlas\n")
|
|
47
20
|
return 1
|
|
48
21
|
|
|
49
|
-
normalized.sort(key=lambda item: (SEVERITY_ORDER[item["severity"]], item["task_id"], item["file"], item["line"] or 0))
|
|
50
|
-
json.dump(normalized, sys.stdout, indent=2)
|
|
51
|
-
sys.stdout.write("\n")
|
|
52
|
-
return 0
|
|
53
|
-
|
|
54
22
|
|
|
55
23
|
if __name__ == "__main__":
|
|
56
24
|
raise SystemExit(main())
|
package/plugins/atlas-workflow-orchestrator/packages/skills/atlas-sprint-prd-generator/SKILL.md
CHANGED
|
@@ -16,6 +16,7 @@ Todo PRD gerado por esta skill deve declarar explicitamente a cadeia de execuç
|
|
|
16
16
|
* Sprint ID: `S<NN>` (`S01`, `S02`, etc.).
|
|
17
17
|
* Opcional: app/projeto alvo quando houver mais de uma fonte de backlog/roadmap.
|
|
18
18
|
* Opcional: path de saída.
|
|
19
|
+
* Opcional: path explícito do backlog autoritativo. Quando fornecido, vence qualquer descoberta.
|
|
19
20
|
|
|
20
21
|
*Se faltar o Sprint ID, peça antes de gerar.*
|
|
21
22
|
|
|
@@ -23,10 +24,11 @@ Todo PRD gerado por esta skill deve declarar explicitamente a cadeia de execuç
|
|
|
23
24
|
|
|
24
25
|
## Workflow Obrigatório
|
|
25
26
|
|
|
26
|
-
1. **Localizar Insumos:** Descubra a raiz do repo com `git rev-parse --show-toplevel`. Localize o template canônico em `<raiz-do-plugin>/packages/templates/PRD_TEMPLATE.md`. Localize
|
|
27
|
-
2. **
|
|
28
|
-
3. **
|
|
29
|
-
4. **
|
|
27
|
+
1. **Localizar Insumos:** Descubra a raiz do repo com `git rev-parse --show-toplevel`. Localize o template canônico em `<raiz-do-plugin>/packages/templates/PRD_TEMPLATE.md`. Localize backlogs candidatos (`**/BACKLOG_MESTRE*.md`) sem escolher por heurística silenciosa.
|
|
28
|
+
2. **Fechar autoridade:** use `../_shared/scripts/document_quality.mjs#resolveSprintAuthority` com precedência fixa: path explícito → backlog canônico referenciado pelo artefato/input → único candidato contendo o Sprint ID. Zero match bloqueia. Múltiplos matches sem autoridade, mesmo com conteúdo parecido, bloqueiam com paths conflitantes e `next_action` para informar o path.
|
|
29
|
+
3. **Extração da Sprint:** leia somente a fonte autoritativa. Extraia fase-fonte, objetivo, dependências e filename do PRD; registre no PRD o path + anchor exato da linha/seção do backlog.
|
|
30
|
+
4. **Inspecionar Código:** Busque no codebase por contratos reais que influenciam a feature e registre anchors estáveis (`path:símbolo` ou `path:linha`) nas referências; não copie implementação para o PRD.
|
|
31
|
+
5. **Redação/atualização:** siga `PRD_TEMPLATE.md`. Ao atualizar, preserve IDs `D*`, decisões fechadas, anchors e histórico; novos IDs são append-only. Mudança deliberada em D* exige decisão explícita e registro histórico.
|
|
30
32
|
|
|
31
33
|
### Resolução Canônica de Templates
|
|
32
34
|
|
|
@@ -57,6 +59,7 @@ Todo PRD criado ou atualizado por esta skill deve incluir, perto do topo e sem s
|
|
|
57
59
|
|
|
58
60
|
* **Status final:** `Aprovado para implementação`. Setar **automaticamente** ao finalizar a geração — é o status que o gate TC do orquestrador exige (`required_status=Aprovado para implementação`) para o PRD avançar no pipeline. Não deixar `Draft` (trava o gate e força correção manual). O sinal de determinismo que sustenta o avanço é o `atlas_scan_prd` (varredura de ambiguidade) + entrevista quando houver padrões bloqueantes — não o campo Status, que é marcador documental.
|
|
59
61
|
* **Data:** ISO `YYYY-MM-DD` (hoje).
|
|
62
|
+
* **Autoridade:** `Relacionado`/`Referências` inclui backlog autoritativo + anchor da sprint e anchors de código/contrato usados.
|
|
60
63
|
* **Escopo:** Lista fechada de capacidades funcionais.
|
|
61
64
|
* **UX:** Cobrir caminhos de `loading`, `empty`, `error`, `success` e `permission` sob a perspectiva do usuário.
|
|
62
65
|
* **Critérios de Aceite:** Binários e observáveis, divididos conforme `PRD_TEMPLATE.md` em: **Produto**, **UX**, **Dados** e **Regressão de produto**.
|
|
@@ -34,9 +34,16 @@ Read the JSON file at `.atlas/state/<run_id>/<slice>.json` using the schema in `
|
|
|
34
34
|
3. **Executed task ids** — `tasks`.
|
|
35
35
|
4. **Boundary refs** — `boundary_refs`.
|
|
36
36
|
5. **Explicit cold-review note** — you did not observe implementation; read current code only.
|
|
37
|
+
6. **Deterministic boundary** — `base_sha`, `head_sha`, `contract_kind`, and all evidence/probe arrays.
|
|
38
|
+
7. **Working-tree delta** — compare `worktree_baseline`/`worktree_final` and current tree; unchanged preexisting dirt stays outside, later mutations must be evidenced.
|
|
39
|
+
8. **Repair correlation** — on attempt 2, correlate every target finding id with `repair_evidence` in the same state path.
|
|
37
40
|
|
|
38
41
|
Do not accept inline contract, copied diff, or pasted task lists as the validation boundary. If `state_path` is missing, unreadable, or lacks any required field, return JSON with `verdict: "fail"` and one P1 finding for `Input insuficiente: <missing item>`.
|
|
39
42
|
|
|
43
|
+
Compatibilidade: state legado mínimo sem `contract_kind` só é aceito quando `executor_skill=atlas-plan-execute`; nesse caso o plano continua autoritativo. State de `atlas-direct-execute` exige extensão completa e `obligations` não vazio.
|
|
44
|
+
|
|
45
|
+
Antes de validar código, compare `base_sha...head_sha`, `HEAD`, snapshot final atual e delta `worktree_baseline→worktree_final` com `files_changed`/evidências. Não infira base pelo nome da branch. Divergência gera `boundary_violations` e finding P1 estruturado.
|
|
46
|
+
|
|
40
47
|
---
|
|
41
48
|
|
|
42
49
|
## Resolução Canônica de Templates
|
|
@@ -79,23 +86,24 @@ Do not accept inline contract, copied diff, or pasted task lists as the validati
|
|
|
79
86
|
3. **For each relevant Section 6 Contract:** verify signature, behavior, and returned shape where applicable.
|
|
80
87
|
4. **For each relevant Section 8 checklist item:** mark it pass or fail with evidence.
|
|
81
88
|
5. **Perform cross-task checks** for shared state, missing required args, route order, partial failure handling, and UI/backend permission mismatch.
|
|
82
|
-
6. **
|
|
89
|
+
6. **Aplique baseline + perfis ativos** abaixo. Resolva os perfis por manifests/comandos reais conforme `../_shared/references/stack-profiles.md`; não invente critérios fora do plano, baseline e perfis ativos.
|
|
83
90
|
7. **Do not patch files or propose diffs.** Suggested fix must fit in 1-2 lines of text.
|
|
84
91
|
|
|
85
92
|
---
|
|
86
93
|
|
|
87
|
-
##
|
|
94
|
+
## Baseline universal + perfis
|
|
95
|
+
|
|
96
|
+
Fonte compartilhada: `../_shared/references/stack-profiles.md`. Execute `detectStackProfiles(project_root, declared_commands, boundary_paths)` de `../_shared/scripts/document_quality.mjs`; aplique cada entrada de `boundaries` somente aos arquivos daquele package.
|
|
97
|
+
|
|
98
|
+
Sempre aplique baseline universal: segurança/permissões, boundary/contratos, erros/falhas parciais, concorrência/reentrada, cleanup/estado stale, integridade de dados/input e checks realmente declarados.
|
|
99
|
+
|
|
100
|
+
Ative regras específicas somente quando o perfil retornar `true`:
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* **Backend and UI gate match:** Sensitive mutations require server-side enforcement. UI gating alone is insufficient (Page reads `canManage` from Store).
|
|
95
|
-
* **Route registration:** Literal routes are registered before parameterized routes (`/:id`, `/:id/edit`) under the same prefix.
|
|
96
|
-
* **Localization:** New localization keys must exist in every required locale file; generated l10n is clean.
|
|
97
|
-
* **Analyzer:** `flutter analyze` (or stack equivalent) returns zero issues for touched files in boundary.
|
|
98
|
-
* **Casts and nullability:** Remote payload casts use safe defensive patterns; nulos in collections treated with `?? []`.
|
|
102
|
+
- `flutter_dart`: lifecycle Flutter, rotas/args, null-safety/casts, l10n, analyze/test; GetX somente se dependência/import/regra real confirmar GetX.
|
|
103
|
+
- `node_typescript`: handles/promises, validação runtime, ESM/CJS/exports/tipos e scripts Node reais.
|
|
104
|
+
- `python`: context managers/cleanup, exceções/async, typing/parsing e ferramentas Python declaradas.
|
|
105
|
+
|
|
106
|
+
Monorepo pode ativar múltiplos perfis, sempre restritos ao boundary correspondente. Fixture Node sem sinal Flutter não recebe regra Flutter/GetX.
|
|
99
107
|
|
|
100
108
|
---
|
|
101
109
|
|
|
@@ -110,10 +118,15 @@ Return strict JSON as the final output. Do not wrap it in Markdown and do not pr
|
|
|
110
118
|
"verdict": "pass | fail | pass_with_observations",
|
|
111
119
|
"findings": [
|
|
112
120
|
{
|
|
121
|
+
"id": "F-001",
|
|
113
122
|
"severity": "P0|P1|P2|P3",
|
|
114
123
|
"file": "string",
|
|
115
|
-
"line":
|
|
116
|
-
"
|
|
124
|
+
"line": 1,
|
|
125
|
+
"failure_mode": "string",
|
|
126
|
+
"evidence": "string",
|
|
127
|
+
"recommendation": "string",
|
|
128
|
+
"fix_validation": "string",
|
|
129
|
+
"msg": "string (deprecated; derivado por uma release)"
|
|
117
130
|
}
|
|
118
131
|
],
|
|
119
132
|
"observations": [
|
|
@@ -134,6 +147,8 @@ Return strict JSON as the final output. Do not wrap it in Markdown and do not pr
|
|
|
134
147
|
|
|
135
148
|
`dispatch_token` must equal `validator_recovery.expected_dispatch_token`. `findings`, `observations`, and `boundary_violations` must always be arrays. Use empty arrays when there are no items.
|
|
136
149
|
|
|
150
|
+
IDs são únicos, obrigatórios no formato `F-NNN` e estáveis nos dois ciclos. Severity é estritamente `P0|P1|P2|P3`. No segundo ciclo, confirme por ID que `repair_evidence` registra arquivos, checks e `status: resolved`; finding não correlacionado permanece P1. O MCP rejeita shape incompleto e `pass`/`pass_with_observations` quando há P0/P1.
|
|
151
|
+
|
|
137
152
|
**Proof-of-work (`challenge_response`).** If `validator_recovery.challenge` is not `null`, it carries `{ file, algo: "sha256" }` — a boundary file you must have read access to. Compute the sha256 of that file's raw bytes (`shasum -a 256 "<challenge.file>"`) and return the hex (first token) in `challenge_response`. If `challenge` is `null`, return `null`. Never fabricate the hash: the orchestrator recomputes it from disk and blocks the slice (`challenge_failed`) on mismatch. This is a *mechanical* attestation that the verdict touched real boundary bytes — it closes the laziest bypass (claiming `pass` with no read at all); it does **not** by itself prove you read and understood the code (hashing a file does not require loading its content). Reading the boundary remains your obligation. It is not a non-forgeable isolation proof either (the MCP shares one stdio caller). Challenge failures are bounded per attempt: past the cap the slot closes terminally (`challenge_exhausted`), which usually signals path resolution diverging from the consumer root.
|
|
138
153
|
|
|
139
154
|
---
|
|
@@ -811,7 +811,17 @@ Ações imediatas:
|
|
|
811
811
|
|
|
812
812
|
---
|
|
813
813
|
|
|
814
|
-
## 21.
|
|
814
|
+
## 21. Registro de alterações
|
|
815
|
+
|
|
816
|
+
Registro append-only de atualizações deste backlog. Não substitui decisões da seção 18.
|
|
817
|
+
|
|
818
|
+
| Data | IDs afetados | Alteração | Motivo / fonte |
|
|
819
|
+
|---|---|---|---|
|
|
820
|
+
| [AAAA-MM-DD] | [SNN/DEC-N/RN] | [resumo objetivo] | [pedido, PRD, contrato ou código] |
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
|
|
824
|
+
## 22. Como usar este template
|
|
815
825
|
|
|
816
826
|
### Setup inicial
|
|
817
827
|
|
|
@@ -840,5 +850,6 @@ Ações imediatas:
|
|
|
840
850
|
### Manter vivo
|
|
841
851
|
|
|
842
852
|
16. Toda decisão que muda escopo, contrato ou sequência atualiza: fonte canônica → PRD afetado → este backlog.
|
|
843
|
-
17.
|
|
844
|
-
18.
|
|
853
|
+
17. Em updates, preservar IDs, sprints `done`, decisões fechadas e itens não relacionados; bloquear ciclos, enums inválidos e placeholders acidentais.
|
|
854
|
+
18. Registrar decisões na seção 18, riscos na seção 19 e cada update no registro append-only da seção 21.
|
|
855
|
+
19. Manter o registro de sprints (seção 7) e o progresso (seção 8.2) como fonte de estado do projeto.
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| **Data** | <YYYY-MM-DD> |
|
|
17
17
|
| **Dependências de negócio** | <Entregas anteriores necessárias — ex.: “dashboard com lista”> |
|
|
18
18
|
| **Relacionado** | <Regras de negócio, MVP, backlog §X, DEC-*, Q-* — links> |
|
|
19
|
+
| **Fonte da sprint** | <path explícito do backlog autoritativo + anchor único SNN> |
|
|
19
20
|
|
|
20
21
|
### Metadados de execução
|
|
21
22
|
|
|
@@ -134,7 +135,7 @@
|
|
|
134
135
|
|
|
135
136
|
**Dependências:** <ID entrega — por que bloqueia ou alimenta> · <decisão externa, se houver>
|
|
136
137
|
|
|
137
|
-
**Referências:** <PRD pai, regras de negócio, backlog
|
|
138
|
+
**Referências:** <PRD pai, regras de negócio, backlog autoritativo + anchor; anchors de contrato/código usados na validação, sem copiar implementação>
|
|
138
139
|
|
|
139
140
|
**Histórico:** <YYYY-MM-DD — evento>
|
|
140
141
|
|
|
@@ -8,7 +8,7 @@ Path:
|
|
|
8
8
|
.atlas/state/<run_id>/<slice>.json
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Schema mínimo:
|
|
11
|
+
Schema legado mínimo (reader compatível):
|
|
12
12
|
|
|
13
13
|
```json
|
|
14
14
|
{
|
|
@@ -24,9 +24,33 @@ Schema mínimo:
|
|
|
24
24
|
}
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
Extensão determinística (writer atual):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"base_sha": "40-char git commit SHA",
|
|
32
|
+
"head_sha": "40-char git commit SHA",
|
|
33
|
+
"contract_kind": "plan | direct",
|
|
34
|
+
"obligations": [{"id": "O1", "requirement": "...", "expected_evidence": ["path/test/check"]}],
|
|
35
|
+
"invariants": [{"id": "I1", "requirement": "...", "expected_evidence": ["path/check"]}],
|
|
36
|
+
"scenario_probes": [{"id": "S1", "scenario": "...", "expected": "..."}],
|
|
37
|
+
"risk_probes": [{"id": "R1", "risk": "...", "probe": "..."}],
|
|
38
|
+
"validation_map": [{"obligation_ids": ["O1"], "checks": ["node --test ..."], "status": "passed"}],
|
|
39
|
+
"task_evidence": [{"task": "T01", "files": ["packages/foo.js"], "checks": ["node --test ..."], "result": "passed"}],
|
|
40
|
+
"repair_evidence": [{"finding_id": "F-001", "files_touched": ["packages/foo.js"], "checks_run": ["node --test ..."], "status": "resolved"}],
|
|
41
|
+
"worktree_baseline": [{"path": "preexisting.txt", "status": "M", "sha256": "<64 hex>"}],
|
|
42
|
+
"worktree_final": [{"path": "preexisting.txt", "status": "M", "sha256": "<64 hex>"}]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
27
46
|
Regras:
|
|
28
47
|
|
|
29
48
|
- `run_id`, `slice`, `tasks`, `files_changed`, `diff_stat`, `plan_path`, `boundary_refs`, `executed_at` e `executor_skill` são obrigatórios.
|
|
30
49
|
- `files_changed` contém paths relativos ao repositório consumidor.
|
|
31
50
|
- `boundary_refs` aponta para invariantes, contratos ou tasks do plano que delimitam a validação.
|
|
32
51
|
- O arquivo é uma projeção de boundary para o validator; estado de run continua tendo `atlas_run_state` como fonte primária quando MCP estiver disponível.
|
|
52
|
+
- Writers atuais sempre preenchem a extensão. `contract_kind=direct` exige `obligations` não vazio; `plan` mantém o contrato autoritativo em `plan_path`.
|
|
53
|
+
- Writers capturam `worktree_baseline` antes da primeira mutação e `worktree_final` imediatamente antes do handoff. Ambos usam entradas únicas/ordenadas `{path,status,sha256}`; `status` é `A|M|D|R|C|T|U`, delete usa `sha256:null`, symlink usa SHA-256 do target textual.
|
|
54
|
+
- Readers aceitam temporariamente o schema legado mínimo somente para `atlas-plan-execute` sem `contract_kind`. `atlas-direct-execute` nunca degrada para legado.
|
|
55
|
+
- `base_sha` e `head_sha` são commits explícitos; não inferir base pelo nome da branch. `files_changed` e os arquivos de `task_evidence`/`repair_evidence` devem ser exatamente o diff `base_sha...head_sha` somado ao delta `worktree_baseline→worktree_final`. Dirty preexistente byte/status-idêntico fica fora.
|
|
56
|
+
- `repair_evidence` é aditivo e obrigatório por finding P0/P1 estruturado após repair; o segundo validator correlaciona pelo mesmo `finding_id`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Baseline universal e perfis de stack
|
|
2
|
+
|
|
3
|
+
Ativação é determinística: inspecione manifests no boundary e comandos realmente declarados no repo/plano. Use `detectStackProfiles(project_root, declared_commands, boundary_paths)`; consuma o array `boundaries` e não deduza stack por extensão isolada.
|
|
4
|
+
|
|
5
|
+
## Baseline universal — sempre ativo
|
|
6
|
+
|
|
7
|
+
- segurança, autenticação/autorização e dados sensíveis;
|
|
8
|
+
- boundary real, contratos, schemas e consumidores afetados;
|
|
9
|
+
- erros, falhas parciais, concorrência, idempotência e reentrada;
|
|
10
|
+
- setup/cleanup, recursos, estado stale e persistência;
|
|
11
|
+
- integridade de dados, nulos, enums, validação de input;
|
|
12
|
+
- checks declarados pelo repo/plano, sem inventar ferramenta.
|
|
13
|
+
|
|
14
|
+
## Flutter/Dart — `pubspec.yaml` ou comando `flutter`/`dart`
|
|
15
|
+
|
|
16
|
+
- lifecycle de Widget/Controller/Store, dispose/reset e async stale;
|
|
17
|
+
- rotas literais antes de parametrizadas quando o roteador exigir;
|
|
18
|
+
- args obrigatórios, null-safety, casts defensivos, coleções `?? []`;
|
|
19
|
+
- l10n em todos os locales e geração limpa;
|
|
20
|
+
- `flutter analyze`/`flutter test` somente quando declarados/aplicáveis;
|
|
21
|
+
- GetX apenas quando dependência/import/regras reais do repo confirmarem GetX.
|
|
22
|
+
|
|
23
|
+
## Node/TypeScript — `package.json`, `tsconfig.json` ou comando Node real
|
|
24
|
+
|
|
25
|
+
- lifecycle de processos/handles, abort/cleanup e promises rejeitadas;
|
|
26
|
+
- validação runtime nas fronteiras JSON/HTTP/MCP;
|
|
27
|
+
- ESM/CJS, exports, tipos e scripts realmente declarados;
|
|
28
|
+
- `node --test`, test runner, lint/typecheck apenas se presentes no repo/plano.
|
|
29
|
+
|
|
30
|
+
## Python — `pyproject.toml`, `requirements.txt`, `setup.py` ou comando Python real
|
|
31
|
+
|
|
32
|
+
- context managers, cleanup, exceções e async/cancelamento;
|
|
33
|
+
- parsing/typing nas fronteiras e mutabilidade de defaults;
|
|
34
|
+
- `pytest`, `ruff`, `mypy` ou equivalente apenas se declarados.
|
|
35
|
+
|
|
36
|
+
Perfis podem coexistir em monorepo. Regra de perfil nunca vira finding fora do boundary onde seu sinal foi ativado.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const VALID = Object.freeze({
|
|
6
|
+
moscow: new Set(['Must', 'Should', 'Could', "Won't now"]),
|
|
7
|
+
gain: new Set(['alto', 'médio', 'baixo']),
|
|
8
|
+
effort: new Set(['alto', 'médio', 'baixo']),
|
|
9
|
+
priority: new Set(['P0', 'P1', 'P2', 'P3']),
|
|
10
|
+
state: new Set(['backlog', 'ready', 'doing', 'review', 'done', 'blocked']),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const STACK_MANIFESTS = ['package.json', 'tsconfig.json', 'pubspec.yaml', 'pyproject.toml', 'requirements.txt', 'setup.py'];
|
|
14
|
+
|
|
15
|
+
function boundaryRoot(projectRoot, boundary) {
|
|
16
|
+
const project = path.resolve(projectRoot);
|
|
17
|
+
let current = path.resolve(project, boundary ?? '.');
|
|
18
|
+
const relative = path.relative(project, current);
|
|
19
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
20
|
+
throw new Error(`BOUNDARY_OUTSIDE_PROJECT:${boundary}`);
|
|
21
|
+
}
|
|
22
|
+
if (fs.existsSync(current) && fs.statSync(current).isFile()) current = path.dirname(current);
|
|
23
|
+
while (current === project || current.startsWith(`${project}${path.sep}`)) {
|
|
24
|
+
if (STACK_MANIFESTS.some((name) => fs.existsSync(path.join(current, name)))) return current;
|
|
25
|
+
if (current === project) break;
|
|
26
|
+
current = path.dirname(current);
|
|
27
|
+
}
|
|
28
|
+
return path.resolve(project, boundary ?? '.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function containsGetxImport(root) {
|
|
32
|
+
const queue = ['lib', 'test'].map((name) => path.join(root, name)).filter((dir) => fs.existsSync(dir));
|
|
33
|
+
let inspected = 0;
|
|
34
|
+
while (queue.length > 0 && inspected < 5000) {
|
|
35
|
+
const current = queue.pop();
|
|
36
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
37
|
+
const target = path.join(current, entry.name);
|
|
38
|
+
if (entry.isDirectory()) queue.push(target);
|
|
39
|
+
else if (entry.isFile() && entry.name.endsWith('.dart')) {
|
|
40
|
+
inspected += 1;
|
|
41
|
+
if (/package:get\/get(?:_core)?\.dart/.test(fs.readFileSync(target, 'utf8'))) return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectBoundaryProfile(root, declaredCommands) {
|
|
49
|
+
const exists = (name) => fs.existsSync(path.join(root, name));
|
|
50
|
+
const commands = declaredCommands.filter((v) => typeof v === 'string');
|
|
51
|
+
let packageJson = null;
|
|
52
|
+
if (exists('package.json')) {
|
|
53
|
+
try { packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); } catch {}
|
|
54
|
+
}
|
|
55
|
+
let pubspec = '';
|
|
56
|
+
if (exists('pubspec.yaml')) {
|
|
57
|
+
try { pubspec = fs.readFileSync(path.join(root, 'pubspec.yaml'), 'utf8'); } catch {}
|
|
58
|
+
}
|
|
59
|
+
const packageCommands = Object.values(packageJson?.scripts ?? {}).filter((v) => typeof v === 'string');
|
|
60
|
+
const allCommands = [...commands, ...packageCommands];
|
|
61
|
+
const hasCommand = (re) => allCommands.some((command) => re.test(command));
|
|
62
|
+
return {
|
|
63
|
+
universal: true,
|
|
64
|
+
flutter_dart: exists('pubspec.yaml') || hasCommand(/\b(flutter|dart)\b/),
|
|
65
|
+
node_typescript: exists('package.json') || exists('tsconfig.json') || hasCommand(/\b(node|npm|pnpm|yarn|bun|tsc)\b/),
|
|
66
|
+
python: exists('pyproject.toml') || exists('requirements.txt') || exists('setup.py') || hasCommand(/\b(python3?|pytest|ruff|mypy)\b/),
|
|
67
|
+
getx: /^\s{0,4}get\s*:/m.test(pubspec) || containsGetxImport(root),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function detectStackProfiles(root, declaredCommands = [], boundaryPaths = ['.']) {
|
|
72
|
+
const project = path.resolve(root);
|
|
73
|
+
const requested = boundaryPaths.length > 0 ? boundaryPaths : ['.'];
|
|
74
|
+
const boundaries = [...new Set(requested.map((boundary) => boundaryRoot(project, boundary)))].map((dir) => ({
|
|
75
|
+
boundary: path.relative(project, dir).replaceAll('\\', '/') || '.',
|
|
76
|
+
...detectBoundaryProfile(dir, declaredCommands),
|
|
77
|
+
}));
|
|
78
|
+
return {
|
|
79
|
+
universal: true,
|
|
80
|
+
flutter_dart: boundaries.some((profile) => profile.flutter_dart),
|
|
81
|
+
node_typescript: boundaries.some((profile) => profile.node_typescript),
|
|
82
|
+
python: boundaries.some((profile) => profile.python),
|
|
83
|
+
getx: boundaries.some((profile) => profile.getx),
|
|
84
|
+
boundaries,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseTable(markdown, heading) {
|
|
89
|
+
const start = markdown.indexOf(heading);
|
|
90
|
+
if (start < 0) return [];
|
|
91
|
+
const tail = markdown.slice(start + heading.length);
|
|
92
|
+
const end = tail.search(/\n##?\s/);
|
|
93
|
+
return (end < 0 ? tail : tail.slice(0, end)).split('\n')
|
|
94
|
+
.filter((line) => /^\|.*\|\s*$/.test(line))
|
|
95
|
+
.map((line) => line.split('|').slice(1, -1).map((cell) => cell.trim()))
|
|
96
|
+
.filter((cells) => cells.length && !cells.every((cell) => /^-+$/.test(cell)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseSprintRows(markdown) {
|
|
100
|
+
const rows = parseTable(markdown, '## 7. Registro de sprints');
|
|
101
|
+
const header = rows.findIndex((row) => row[0] === 'ID');
|
|
102
|
+
return rows.slice(header + 1).filter((row) => /^S\d{2}[a-z]?$/.test(row[0])).map((row) => ({
|
|
103
|
+
id: row[0], name: row[1], phase: row[2], objective: row[3], moscow: row[4], gain: row[5].toLowerCase(),
|
|
104
|
+
effort: row[6].toLowerCase(), priority: row[7], prd: row[8], dependencies: row[9], state: row[10], gate: row[11], raw: row,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function parseDecisionRows(markdown) {
|
|
109
|
+
const rows = parseTable(markdown, '### Decisões bloqueantes');
|
|
110
|
+
const header = rows.findIndex((row) => row[0] === 'ID');
|
|
111
|
+
return rows.slice(header + 1).filter((row) => /^D\d+$/.test(row[0])).map((row) => ({
|
|
112
|
+
id: row[0], decision: row[1], blocks: row[2], owner: row[3], status: row[4], raw: row,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function dependencyIds(value) {
|
|
117
|
+
if (!value || value === '—') return [];
|
|
118
|
+
return [...value.matchAll(/S\d{2}[a-z]?/g)].map((match) => match[0]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function findCycle(rows) {
|
|
122
|
+
const graph = new Map(rows.map((row) => [row.id, dependencyIds(row.dependencies)]));
|
|
123
|
+
const visiting = new Set(); const visited = new Set();
|
|
124
|
+
const walk = (id, chain = []) => {
|
|
125
|
+
if (visiting.has(id)) return [...chain.slice(chain.indexOf(id)), id];
|
|
126
|
+
if (visited.has(id)) return null;
|
|
127
|
+
visiting.add(id);
|
|
128
|
+
for (const dep of graph.get(id) ?? []) {
|
|
129
|
+
const cycle = walk(dep, [...chain, id]);
|
|
130
|
+
if (cycle) return cycle;
|
|
131
|
+
}
|
|
132
|
+
visiting.delete(id); visited.add(id); return null;
|
|
133
|
+
};
|
|
134
|
+
for (const id of graph.keys()) { const cycle = walk(id); if (cycle) return cycle; }
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function changeLogBody(markdown) {
|
|
139
|
+
const match = /^##\s+(?:Registro de alterações|Histórico de alterações)\s*$/im.exec(markdown);
|
|
140
|
+
if (!match) return null;
|
|
141
|
+
const tail = markdown.slice(match.index + match[0].length);
|
|
142
|
+
const end = tail.search(/\n##\s+/);
|
|
143
|
+
return (end < 0 ? tail : tail.slice(0, end)).trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function validateBacklogUpdate(before, after, { authorizedIds = [] } = {}) {
|
|
147
|
+
const errors = [];
|
|
148
|
+
const authorized = new Set(authorizedIds);
|
|
149
|
+
const oldRows = parseSprintRows(before); const newRows = parseSprintRows(after);
|
|
150
|
+
const oldById = new Map(oldRows.map((row) => [row.id, row]));
|
|
151
|
+
const newById = new Map(newRows.map((row) => [row.id, row]));
|
|
152
|
+
if (oldById.size !== oldRows.length) errors.push('DUPLICATE_SPRINT_ID_BEFORE');
|
|
153
|
+
if (newById.size !== newRows.length) errors.push('DUPLICATE_SPRINT_ID_AFTER');
|
|
154
|
+
for (const [id, oldRow] of oldById) {
|
|
155
|
+
const next = newById.get(id);
|
|
156
|
+
if (!next) errors.push(`SPRINT_REMOVED:${id}`);
|
|
157
|
+
else if (JSON.stringify(oldRow.raw) !== JSON.stringify(next.raw) && !authorized.has(id)) {
|
|
158
|
+
errors.push(oldRow.state === 'done' ? `DONE_SPRINT_CHANGED:${id}` : `UNAUTHORIZED_SPRINT_CHANGED:${id}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const oldDecisions = new Map(parseDecisionRows(before).map((row) => [row.id, row]));
|
|
162
|
+
const newDecisions = new Map(parseDecisionRows(after).map((row) => [row.id, row]));
|
|
163
|
+
for (const [id, row] of oldDecisions) {
|
|
164
|
+
const next = newDecisions.get(id);
|
|
165
|
+
if (!next) errors.push(`DECISION_REMOVED:${id}`);
|
|
166
|
+
else if (/^(decidido|fechado|aprovado)$/i.test(row.status)
|
|
167
|
+
&& JSON.stringify(row.raw) !== JSON.stringify(next.raw) && !authorized.has(id)) errors.push(`CLOSED_DECISION_CHANGED:${id}`);
|
|
168
|
+
}
|
|
169
|
+
for (const row of newRows) {
|
|
170
|
+
for (const [field, values] of Object.entries(VALID)) if (!values.has(row[field])) errors.push(`INVALID_ENUM:${row.id}:${field}:${row[field]}`);
|
|
171
|
+
for (const dependency of dependencyIds(row.dependencies)) {
|
|
172
|
+
if (!newById.has(dependency)) errors.push(`DEPENDENCY_NOT_FOUND:${row.id}:${dependency}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const cycle = findCycle(newRows); if (cycle) errors.push(`DEPENDENCY_CYCLE:${cycle.join('>')}`);
|
|
176
|
+
if (/\[(?:NOME_|RESULTADO_|observação|decisão|slug|pendente\])/i.test(after)) errors.push('UNRESOLVED_PLACEHOLDER');
|
|
177
|
+
if (before !== after) {
|
|
178
|
+
const oldLog = changeLogBody(before);
|
|
179
|
+
const newLog = changeLogBody(after);
|
|
180
|
+
if (newLog === null) errors.push('CHANGELOG_REQUIRED');
|
|
181
|
+
else if (oldLog !== null && !newLog.startsWith(oldLog)) errors.push('CHANGELOG_REWRITTEN');
|
|
182
|
+
else if (newLog === oldLog) errors.push('CHANGELOG_ENTRY_REQUIRED');
|
|
183
|
+
}
|
|
184
|
+
return { valid: errors.length === 0, errors };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function resolveSprintAuthority({ sprintId, explicitPath, canonicalPath, candidates }) {
|
|
188
|
+
const normalized = candidates.map((candidate) => ({ ...candidate, path: path.resolve(candidate.path) }));
|
|
189
|
+
const byPath = (target) => normalized.find((candidate) => candidate.path === path.resolve(target));
|
|
190
|
+
if (explicitPath) {
|
|
191
|
+
const match = byPath(explicitPath); if (!match?.sprints.includes(sprintId)) throw new Error(`SPRINT_NOT_FOUND_IN_EXPLICIT_PATH:${sprintId}`); return match;
|
|
192
|
+
}
|
|
193
|
+
if (canonicalPath) {
|
|
194
|
+
const match = byPath(canonicalPath); if (!match?.sprints.includes(sprintId)) throw new Error(`SPRINT_NOT_FOUND_IN_CANONICAL_BACKLOG:${sprintId}`); return match;
|
|
195
|
+
}
|
|
196
|
+
const matches = normalized.filter((candidate) => candidate.sprints.includes(sprintId));
|
|
197
|
+
if (matches.length !== 1) throw new Error(matches.length ? `AMBIGUOUS_BACKLOG_AUTHORITY:${matches.map((m) => m.path).join(',')}` : `SPRINT_NOT_FOUND:${sprintId}`);
|
|
198
|
+
return matches[0];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function closedDecisionIds(prd) {
|
|
202
|
+
return new Set([...prd.matchAll(/^\|\s*(D\d+)\s*\|\s*(?!<|\[)(.+?)\s*\|\s*$/gm)].map((match) => match[1]));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function pendingInterviewQuestions(prd, questions) {
|
|
206
|
+
const closed = closedDecisionIds(prd);
|
|
207
|
+
return questions.filter((question) => !closed.has(question.decision_id));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function applyInterviewRound(prd, answers, date = new Date().toISOString().slice(0, 10)) {
|
|
211
|
+
const ids = new Set();
|
|
212
|
+
for (const answer of answers) {
|
|
213
|
+
if (!answer || typeof answer.decision_id !== 'string' || !/^D\d+$/.test(answer.decision_id)) throw new Error('INVALID_DECISION_ID');
|
|
214
|
+
if (ids.has(answer.decision_id)) throw new Error(`DUPLICATE_DECISION_ID:${answer.decision_id}`);
|
|
215
|
+
if (typeof answer.value !== 'string' || !answer.value.trim()) throw new Error(`EMPTY_DECISION_VALUE:${answer.decision_id}`);
|
|
216
|
+
ids.add(answer.decision_id);
|
|
217
|
+
}
|
|
218
|
+
let updated = prd;
|
|
219
|
+
for (const answer of answers) {
|
|
220
|
+
const row = new RegExp(`^\\|\\s*${answer.decision_id}\\s*\\|.*$`, 'm');
|
|
221
|
+
const replacement = `| ${answer.decision_id} | ${answer.value} |`;
|
|
222
|
+
updated = row.test(updated) ? updated.replace(row, replacement) : updated.replace(/(\| ID \| Decisão \|\n\|[-| ]+\|)/, `$1\n${replacement}`);
|
|
223
|
+
}
|
|
224
|
+
const log = `${date} — entrevista: ${answers.map((answer) => answer.decision_id).join(', ')} persistida(s)`;
|
|
225
|
+
updated = /\*\*Histórico:\*\*/.test(updated)
|
|
226
|
+
? updated.replace(/(\*\*Histórico:\*\*[^\n]*)/, `$1 · ${log}`)
|
|
227
|
+
: `${updated.trimEnd()}\n\n**Histórico:** ${log}\n`;
|
|
228
|
+
return updated;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function persistInterviewRound(prdPath, answers, date = new Date().toISOString().slice(0, 10)) {
|
|
232
|
+
const absolute = path.resolve(prdPath);
|
|
233
|
+
const temporary = path.join(path.dirname(absolute), `.${path.basename(absolute)}.${process.pid}.${Date.now()}.tmp`);
|
|
234
|
+
try {
|
|
235
|
+
const current = fs.readFileSync(absolute, 'utf8');
|
|
236
|
+
const updated = applyInterviewRound(current, answers, date);
|
|
237
|
+
const materialized = closedDecisionIds(updated);
|
|
238
|
+
const missingBeforeWrite = answers.filter((answer) => !materialized.has(answer.decision_id));
|
|
239
|
+
if (missingBeforeWrite.length > 0) {
|
|
240
|
+
throw new Error(`DECISION_NOT_MATERIALIZED:${missingBeforeWrite.map((answer) => answer.decision_id).join(',')}`);
|
|
241
|
+
}
|
|
242
|
+
const mode = fs.statSync(absolute).mode;
|
|
243
|
+
fs.writeFileSync(temporary, updated, { encoding: 'utf8', mode });
|
|
244
|
+
fs.renameSync(temporary, absolute);
|
|
245
|
+
const readback = fs.readFileSync(absolute, 'utf8');
|
|
246
|
+
if (readback !== updated) throw new Error('READBACK_DIVERGENT');
|
|
247
|
+
return readback;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
try { fs.rmSync(temporary, { force: true }); } catch {}
|
|
250
|
+
throw new Error(`INTERVIEW_PERSISTENCE_FAILED:${error.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|