@jaimevalasek/aioson 1.22.0 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +932 -919
- package/docs/en/5-reference/cli-reference.md +85 -0
- package/docs/pt/4-agentes/pm.md +31 -4
- package/docs/pt/5-referencia/README.md +3 -0
- package/docs/pt/5-referencia/autopilot-handoff.md +131 -0
- package/docs/pt/5-referencia/comandos-cli.md +72 -6
- package/docs/pt/5-referencia/harness-retro.md +133 -0
- package/docs/pt/5-referencia/loop-guardrails.md +225 -0
- package/docs/pt/5-referencia/sdd-automation-scripts.md +25 -13
- package/package.json +1 -1
- package/src/agents.js +1 -1
- package/src/cli.js +70 -29
- package/src/commands/agent-epilogue.js +186 -0
- package/src/commands/context-select.js +33 -0
- package/src/commands/harness-preview.js +74 -0
- package/src/commands/harness-retro.js +221 -0
- package/src/commands/preflight-context.js +13 -9
- package/src/commands/review-cycle.js +328 -0
- package/src/commands/runtime.js +4 -4
- package/src/commands/self-implement-loop.js +12 -2
- package/src/commands/state-save.js +2 -0
- package/src/commands/workflow-execute.js +138 -28
- package/src/commands/workflow-next.js +11 -2
- package/src/commands/workflow-status.js +30 -10
- package/src/constants.js +15 -13
- package/src/context-memory.js +50 -25
- package/src/context-selector.js +394 -0
- package/src/harness/preview-artifact.js +85 -0
- package/src/i18n/messages/en.js +34 -7
- package/src/i18n/messages/es.js +34 -7
- package/src/i18n/messages/fr.js +34 -7
- package/src/i18n/messages/pt-BR.js +34 -7
- package/src/lib/retro/retro-aggregate.js +192 -0
- package/src/lib/retro/retro-render.js +185 -0
- package/src/lib/retro/retro-sources.js +624 -0
- package/src/parser.js +1 -1
- package/src/squad/preflight-context.js +26 -27
- package/template/.aioson/agents/analyst.md +41 -46
- package/template/.aioson/agents/architect.md +33 -46
- package/template/.aioson/agents/briefing.md +76 -67
- package/template/.aioson/agents/dev.md +73 -55
- package/template/.aioson/agents/deyvin.md +55 -50
- package/template/.aioson/agents/discovery-design-doc.md +35 -22
- package/template/.aioson/agents/manifests/architect.manifest.json +11 -1
- package/template/.aioson/agents/manifests/dev.manifest.json +15 -0
- package/template/.aioson/agents/manifests/pm.manifest.json +20 -0
- package/template/.aioson/agents/orchestrator.md +31 -18
- package/template/.aioson/agents/pentester.md +12 -4
- package/template/.aioson/agents/pm.md +41 -35
- package/template/.aioson/agents/product.md +116 -165
- package/template/.aioson/agents/qa.md +44 -13
- package/template/.aioson/agents/scope-check.md +46 -24
- package/template/.aioson/agents/sheldon.md +13 -0
- package/template/.aioson/agents/tester.md +28 -5
- package/template/.aioson/agents/ux-ui.md +36 -31
- package/template/.aioson/agents/validator.md +10 -2
- package/template/.aioson/config/autonomy-protocol.json +7 -0
- package/template/.aioson/design-docs/code-reuse.md +10 -5
- package/template/.aioson/design-docs/componentization.md +10 -5
- package/template/.aioson/design-docs/file-size.md +10 -5
- package/template/.aioson/design-docs/folder-structure.md +10 -5
- package/template/.aioson/design-docs/naming.md +10 -5
- package/template/.aioson/docs/autonomy-protocol.md +2 -2
- package/template/.aioson/docs/autopilot-handoff.md +82 -34
- package/template/.aioson/docs/briefing/briefing-craft.md +9 -3
- package/template/.aioson/docs/deyvin/continuity-recovery.md +18 -22
- package/template/.aioson/docs/product/conversation-playbook.md +8 -3
- package/template/.aioson/docs/product/prd-contract.md +8 -3
- package/template/.aioson/docs/product/quality-lens.md +8 -3
- package/template/.aioson/docs/product/research-loop.md +8 -3
- package/template/.aioson/docs/ux-ui/accessibility-audit.md +7 -2
- package/template/.aioson/docs/ux-ui/audit-mode.md +7 -2
- package/template/.aioson/docs/ux-ui/component-map.md +7 -2
- package/template/.aioson/docs/ux-ui/design-execution.md +7 -2
- package/template/.aioson/docs/ux-ui/design-gate.md +7 -2
- package/template/.aioson/docs/ux-ui/research-mode.md +7 -2
- package/template/.aioson/docs/ux-ui/site-delivery.md +7 -2
- package/template/.aioson/docs/ux-ui/token-contract.md +7 -2
- package/template/.aioson/rules/aioson-context-boundary.md +11 -9
- package/template/.aioson/rules/disk-first-artifacts.md +1 -1
- package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +1 -1
- package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +3 -2
- package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +21 -9
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -1
- package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +2 -1
- package/template/.aioson/skills/static/web-research-cache.md +29 -8
- package/template/AGENTS.md +1 -1
- package/template/CLAUDE.md +1 -1
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Leitores de fonte para `aioson harness:retro` (RHO-lite, requirements §3.2/§5.1).
|
|
5
|
+
*
|
|
6
|
+
* Um leitor por fonte; cada um é best-effort independente (padrão de
|
|
7
|
+
* `attempt-artifacts.js`): nunca propaga exceção, sempre devolve
|
|
8
|
+
* `{ findings, warnings, count, ... }`. Fonte ausente, vazia, ilegível ou DB
|
|
9
|
+
* lockado vira linha de aviso na "Trilha minerada", nunca erro fatal (REQ-3).
|
|
10
|
+
*
|
|
11
|
+
* Mineração 100% determinística (REQ-1): nenhuma chamada LLM, nenhuma
|
|
12
|
+
* classificação semântica — só regex e chaves exatas. Leitura-apenas: este
|
|
13
|
+
* módulo NUNCA escreve no filesystem.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const crypto = require('node:crypto');
|
|
19
|
+
|
|
20
|
+
const FINDING_ID_RE = /\b([A-Z]{1,2}-\d{1,2})\b/;
|
|
21
|
+
const TRAIL_ENTRY_RE = /^\*\*([^*]+)\*\*\s*\|\s*@?([\w.-]+)\s*\|\s*_([^_]+)_\s*$/;
|
|
22
|
+
const VERDICT_RE = /\b(?:verdict|veredicto)\b[^\n]*?\b(PASS|FAIL)\b/i;
|
|
23
|
+
const VERDICT_FALLBACK_RE = /\b(PASS|FAIL)\b/i;
|
|
24
|
+
const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
25
|
+
|
|
26
|
+
const SEVERITY_ALIASES = {
|
|
27
|
+
critical: 'critical',
|
|
28
|
+
crit: 'critical',
|
|
29
|
+
high: 'high',
|
|
30
|
+
med: 'medium',
|
|
31
|
+
medium: 'medium',
|
|
32
|
+
low: 'low',
|
|
33
|
+
info: 'info',
|
|
34
|
+
informational: 'info'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Normaliza severidade (case-insensitive); desconhecida → `unknown` (nunca promove). */
|
|
38
|
+
function normalizeSeverity(raw) {
|
|
39
|
+
if (raw === null || raw === undefined) return 'unknown';
|
|
40
|
+
const key = String(raw).trim().toLowerCase();
|
|
41
|
+
return SEVERITY_ALIASES[key] || 'unknown';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Path relativo com separador POSIX (determinismo cross-OS — AC-4, EC Windows). */
|
|
45
|
+
function relPath(rootDir, p) {
|
|
46
|
+
return path.relative(rootDir, p).replaceAll('\\', '/');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readTextSafe(p) {
|
|
50
|
+
try {
|
|
51
|
+
return fs.readFileSync(p, 'utf8');
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function listFilesSafe(dir) {
|
|
58
|
+
try {
|
|
59
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function statSizeSafe(p) {
|
|
66
|
+
try {
|
|
67
|
+
return fs.statSync(p).size;
|
|
68
|
+
} catch {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Parser de frontmatter YAML simples (`key: value`, valor escalar de 1 linha). */
|
|
74
|
+
function parseFrontmatter(text) {
|
|
75
|
+
if (typeof text !== 'string' || !text.startsWith('---')) return { data: {}, body: text || '' };
|
|
76
|
+
const end = text.indexOf('\n---', 3);
|
|
77
|
+
if (end === -1) return { data: {}, body: text };
|
|
78
|
+
const block = text.slice(3, end).replace(/^\r?\n/, '');
|
|
79
|
+
const body = text.slice(end + 4);
|
|
80
|
+
const data = {};
|
|
81
|
+
for (const line of block.split(/\r?\n/)) {
|
|
82
|
+
const m = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
83
|
+
if (!m) continue;
|
|
84
|
+
// Remove comentário inline (ex.: `status: resolved # open | ...`).
|
|
85
|
+
let value = m[2].replace(/\s+#.*$/, '').trim();
|
|
86
|
+
value = value.replace(/^["']|["']$/g, '');
|
|
87
|
+
data[m[1]] = value;
|
|
88
|
+
}
|
|
89
|
+
return { data, body };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isoDate(raw) {
|
|
93
|
+
if (!raw) return null;
|
|
94
|
+
const m = String(raw).match(/\d{4}-\d{2}-\d{2}(?:T[\d:.+Z-]+)?/);
|
|
95
|
+
return m ? m[0] : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function makeFinding(partial) {
|
|
99
|
+
return {
|
|
100
|
+
source_type: partial.source_type,
|
|
101
|
+
feature_slug: partial.feature_slug,
|
|
102
|
+
finding_id: partial.finding_id || null,
|
|
103
|
+
severity: normalizeSeverity(partial.severity),
|
|
104
|
+
title: (partial.title ? String(partial.title) : '').slice(0, 200),
|
|
105
|
+
file_ref: partial.file_ref || null,
|
|
106
|
+
date: partial.date || null,
|
|
107
|
+
status: partial.status || 'unknown',
|
|
108
|
+
source_path: partial.source_path,
|
|
109
|
+
signature: partial.signature || null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- 1. QA reports ----------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
const KNOWN_SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'info']);
|
|
116
|
+
|
|
117
|
+
/** Extrai findings estruturados de um QA report (tabelas + headers de finding). */
|
|
118
|
+
function extractQaFindings({ body, slug, sourcePath, date, status }) {
|
|
119
|
+
const found = new Map(); // finding_id → finding (primeiro vence)
|
|
120
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
121
|
+
const line = rawLine.trim();
|
|
122
|
+
// Linha de tabela: | ID | Sev | ... |
|
|
123
|
+
if (line.startsWith('|')) {
|
|
124
|
+
const cells = line.split('|').map((c) => c.trim()).filter((c, i, arr) => i > 0 && i < arr.length);
|
|
125
|
+
if (cells.length >= 2) {
|
|
126
|
+
const idMatch = cells[0].match(FINDING_ID_RE);
|
|
127
|
+
if (idMatch) {
|
|
128
|
+
const sevCell = cells.find((c) => KNOWN_SEVERITIES.has(c.toLowerCase()));
|
|
129
|
+
if (sevCell) {
|
|
130
|
+
const id = idMatch[1];
|
|
131
|
+
if (!found.has(id)) {
|
|
132
|
+
found.set(id, makeFinding({
|
|
133
|
+
source_type: 'qa_report', feature_slug: slug, finding_id: id,
|
|
134
|
+
severity: sevCell, title: cells[2] || cells[1] || id,
|
|
135
|
+
date, status, source_path: sourcePath
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// Header de finding: ### C-01 — Title (High)
|
|
144
|
+
const head = rawLine.match(/^#{2,5}\s+([A-Z]{1,2}-\d{1,2})\b\s*[—:-]?\s*(.*)$/);
|
|
145
|
+
if (head) {
|
|
146
|
+
const id = head[1];
|
|
147
|
+
if (!found.has(id)) {
|
|
148
|
+
const sevMatch = head[2].match(/\(([A-Za-z]+)/);
|
|
149
|
+
found.set(id, makeFinding({
|
|
150
|
+
source_type: 'qa_report', feature_slug: slug, finding_id: id,
|
|
151
|
+
severity: sevMatch ? sevMatch[1] : 'unknown',
|
|
152
|
+
title: head[2].replace(/\(.*$/, '').trim() || id,
|
|
153
|
+
date, status, source_path: sourcePath
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return [...found.values()];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readQaReports({ rootDir, ctxDir, slug, locations }) {
|
|
162
|
+
const findings = [];
|
|
163
|
+
const warnings = [];
|
|
164
|
+
let count = 0;
|
|
165
|
+
for (const dir of locations.qaDirs) {
|
|
166
|
+
for (const ent of listFilesSafe(dir)) {
|
|
167
|
+
if (!ent.isFile()) continue;
|
|
168
|
+
if (!/^qa-report-/.test(ent.name) || !ent.name.endsWith('.md')) continue;
|
|
169
|
+
if (!ent.name.includes(slug)) continue;
|
|
170
|
+
const full = path.join(dir, ent.name);
|
|
171
|
+
const text = readTextSafe(full);
|
|
172
|
+
if (text === null) {
|
|
173
|
+
warnings.push(`qa_report ilegível: ${relPath(rootDir, full)}`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
count += 1;
|
|
177
|
+
const { data, body } = parseFrontmatter(text);
|
|
178
|
+
const date = isoDate(data.created_at || data.updated_at || data.date);
|
|
179
|
+
const status = (data.verdict || '').toUpperCase() === 'FAIL' ? 'open' : 'fixed';
|
|
180
|
+
findings.push(...extractQaFindings({ body, slug, sourcePath: relPath(rootDir, full), date, status }));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { findings, warnings, count };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- 2. Corrections plans ---------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function mapCorrectionStatus(raw) {
|
|
189
|
+
const v = String(raw || '').toLowerCase();
|
|
190
|
+
if (v === 'resolved') return 'fixed';
|
|
191
|
+
if (v === 'open' || v === 'in_progress') return 'open';
|
|
192
|
+
return 'unknown';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readCorrections({ rootDir, slug, locations }) {
|
|
196
|
+
const findings = [];
|
|
197
|
+
const warnings = [];
|
|
198
|
+
let count = 0;
|
|
199
|
+
let bytes = 0;
|
|
200
|
+
let entries = 0;
|
|
201
|
+
for (const dir of locations.planDirs) {
|
|
202
|
+
for (const ent of listFilesSafe(dir)) {
|
|
203
|
+
if (!ent.isFile()) continue;
|
|
204
|
+
if (!/^corrections-.*\.md$/.test(ent.name)) continue;
|
|
205
|
+
const full = path.join(dir, ent.name);
|
|
206
|
+
const text = readTextSafe(full);
|
|
207
|
+
if (text === null) {
|
|
208
|
+
warnings.push(`corrections ilegível: ${relPath(rootDir, full)}`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
count += 1;
|
|
212
|
+
bytes += statSizeSafe(full);
|
|
213
|
+
const { data, body } = parseFrontmatter(text);
|
|
214
|
+
const status = mapCorrectionStatus(data.status);
|
|
215
|
+
const date = isoDate(data.created || data.date);
|
|
216
|
+
const sourcePath = relPath(rootDir, full);
|
|
217
|
+
const lines = body.split(/\r?\n/);
|
|
218
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
219
|
+
const head = lines[i].match(/^#{2,5}\s+([A-Z]{1,2}-\d{1,2})\s*[—:-]\s*(.+?)\s*$/);
|
|
220
|
+
if (!head) continue;
|
|
221
|
+
entries += 1;
|
|
222
|
+
const sevMatch = head[2].match(/\(([A-Za-z]+)/);
|
|
223
|
+
// Procura linha File:/Files: logo após o header.
|
|
224
|
+
let fileRef = null;
|
|
225
|
+
for (let j = i + 1; j < Math.min(i + 4, lines.length); j += 1) {
|
|
226
|
+
const fm = lines[j].match(/^Files?:\s*(.+)$/i);
|
|
227
|
+
if (fm) { fileRef = fm[1].trim(); break; }
|
|
228
|
+
}
|
|
229
|
+
findings.push(makeFinding({
|
|
230
|
+
source_type: 'corrections', feature_slug: slug, finding_id: head[1],
|
|
231
|
+
severity: sevMatch ? sevMatch[1] : 'unknown',
|
|
232
|
+
title: head[2].replace(/\s*\(.*$/, '').trim(),
|
|
233
|
+
file_ref: fileRef, date, status, source_path: sourcePath
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return { findings, warnings, count, bytes, entries };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --- 3. Dossier Agent Trail (verdicts + ciclos FAIL→PASS) -------------------
|
|
242
|
+
|
|
243
|
+
function readDossierTrail({ rootDir, slug, locations }) {
|
|
244
|
+
const findings = [];
|
|
245
|
+
const warnings = [];
|
|
246
|
+
const cycles = [];
|
|
247
|
+
let count = 0;
|
|
248
|
+
let illegible = 0;
|
|
249
|
+
|
|
250
|
+
for (const full of locations.dossierFiles) {
|
|
251
|
+
const text = readTextSafe(full);
|
|
252
|
+
if (text === null) continue;
|
|
253
|
+
const sourcePath = relPath(rootDir, full);
|
|
254
|
+
const trailIdx = text.indexOf('## Agent Trail');
|
|
255
|
+
const region = trailIdx === -1 ? text : text.slice(trailIdx);
|
|
256
|
+
const lines = region.split(/\r?\n/);
|
|
257
|
+
|
|
258
|
+
// Acha headers de entrada e fatia o corpo entre eles.
|
|
259
|
+
const entries = [];
|
|
260
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
261
|
+
const m = lines[i].match(TRAIL_ENTRY_RE);
|
|
262
|
+
if (!m) continue;
|
|
263
|
+
const ts = m[1].trim();
|
|
264
|
+
const agent = m[2].trim().toLowerCase();
|
|
265
|
+
const section = m[3].trim();
|
|
266
|
+
if (!ISO_RE.test(ts)) { illegible += 1; continue; }
|
|
267
|
+
// Corpo: até o próximo header de entrada.
|
|
268
|
+
const bodyLines = [];
|
|
269
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
270
|
+
if (TRAIL_ENTRY_RE.test(lines[j])) break;
|
|
271
|
+
if (/^<!--\s*sha256:/.test(lines[j])) continue;
|
|
272
|
+
bodyLines.push(lines[j]);
|
|
273
|
+
}
|
|
274
|
+
entries.push({ ts, agent, section, body: bodyLines.join('\n') });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (entries.length === 0) continue;
|
|
278
|
+
count += entries.length;
|
|
279
|
+
|
|
280
|
+
// Verdicts ordenados por timestamp → ciclos FAIL→PASS (D5). O trail é fonte
|
|
281
|
+
// de VERDICTS/ciclos, não de findings: extrair findings do resumo @qa do
|
|
282
|
+
// trail duplicaria o que já vem do corrections plan / QA report (fonte
|
|
283
|
+
// autoritativa). Mantemos a leitura determinística e sem dupla contagem.
|
|
284
|
+
const verdicts = [];
|
|
285
|
+
for (const e of entries) {
|
|
286
|
+
let vm = e.body.match(VERDICT_RE);
|
|
287
|
+
if (!vm && e.agent === 'qa') vm = e.body.match(VERDICT_FALLBACK_RE);
|
|
288
|
+
if (vm) verdicts.push({ ts: e.ts, verdict: vm[1].toUpperCase() });
|
|
289
|
+
}
|
|
290
|
+
verdicts.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
|
|
291
|
+
let openFail = null;
|
|
292
|
+
for (const v of verdicts) {
|
|
293
|
+
if (v.verdict === 'FAIL') {
|
|
294
|
+
openFail = v.ts;
|
|
295
|
+
} else if (v.verdict === 'PASS' && openFail) {
|
|
296
|
+
cycles.push({ feature_slug: slug, fail_at: openFail, pass_at: v.ts, source_path: sourcePath });
|
|
297
|
+
openFail = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (illegible > 0) warnings.push(`dossier_trail: ${illegible} entrada(s) ilegível(is) (sem timestamp ISO)`);
|
|
303
|
+
return { findings, warnings, count, cycles };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// --- 4. execution_events (aios.sqlite, readonly — D7) -----------------------
|
|
307
|
+
|
|
308
|
+
function readExecutionEvents({ rootDir, targetDir, slug }) {
|
|
309
|
+
const warnings = [];
|
|
310
|
+
let count = 0;
|
|
311
|
+
let tokenAvailable = false;
|
|
312
|
+
let tokenTotal = 0;
|
|
313
|
+
const dbPath = path.join(targetDir, '.aioson', 'runtime', 'aios.sqlite');
|
|
314
|
+
if (!fs.existsSync(dbPath)) {
|
|
315
|
+
return { findings: [], warnings: [`execution_events: ${relPath(rootDir, dbPath)} ausente`], count: 0, tokenAvailable, tokenTotal };
|
|
316
|
+
}
|
|
317
|
+
let Database;
|
|
318
|
+
try {
|
|
319
|
+
Database = require('better-sqlite3');
|
|
320
|
+
} catch {
|
|
321
|
+
return { findings: [], warnings: ['execution_events: better-sqlite3 indisponível'], count: 0, tokenAvailable, tokenTotal };
|
|
322
|
+
}
|
|
323
|
+
let db = null;
|
|
324
|
+
try {
|
|
325
|
+
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
326
|
+
const rows = db.prepare('SELECT payload_json, token_count FROM execution_events').all();
|
|
327
|
+
for (const row of rows) {
|
|
328
|
+
if (!row.payload_json) continue;
|
|
329
|
+
let payload;
|
|
330
|
+
try {
|
|
331
|
+
payload = JSON.parse(row.payload_json);
|
|
332
|
+
} catch {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (!payload || payload.slug !== slug) continue;
|
|
336
|
+
count += 1;
|
|
337
|
+
if (row.token_count !== null && row.token_count !== undefined) {
|
|
338
|
+
tokenAvailable = true;
|
|
339
|
+
tokenTotal += Number(row.token_count) || 0;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
warnings.push(`execution_events: DB ilegível/lockado (${err.code || 'erro'})`);
|
|
344
|
+
} finally {
|
|
345
|
+
try { if (db) db.close(); } catch { /* best-effort */ }
|
|
346
|
+
}
|
|
347
|
+
return { findings: [], warnings, count, tokenAvailable, tokenTotal };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// --- 5. attempts/{n}/ -------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
function readAttempts({ rootDir, slug, locations }) {
|
|
353
|
+
const warnings = [];
|
|
354
|
+
let count = 0;
|
|
355
|
+
for (const planDir of locations.planDirs) {
|
|
356
|
+
const attemptsDir = path.join(planDir, 'attempts');
|
|
357
|
+
for (const ent of listFilesSafe(attemptsDir)) {
|
|
358
|
+
if (ent.isDirectory() && /^\d+$/.test(ent.name)) count += 1;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { findings: [], warnings, count };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// --- 6. progress.json failure_signatures ------------------------------------
|
|
365
|
+
|
|
366
|
+
function readFailureSignatures({ rootDir, slug, locations }) {
|
|
367
|
+
const findings = [];
|
|
368
|
+
const warnings = [];
|
|
369
|
+
let count = 0;
|
|
370
|
+
for (const planDir of locations.planDirs) {
|
|
371
|
+
const full = path.join(planDir, 'progress.json');
|
|
372
|
+
const text = readTextSafe(full);
|
|
373
|
+
if (text === null) continue;
|
|
374
|
+
let data;
|
|
375
|
+
try {
|
|
376
|
+
data = JSON.parse(text);
|
|
377
|
+
} catch {
|
|
378
|
+
warnings.push(`progress.json ilegível: ${relPath(rootDir, full)}`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const sigs = Array.isArray(data.failure_signatures) ? data.failure_signatures : [];
|
|
382
|
+
const sourcePath = relPath(rootDir, full);
|
|
383
|
+
for (const entry of sigs) {
|
|
384
|
+
const signature = typeof entry === 'string' ? entry : (entry && (entry.signature || entry.sha1)) || null;
|
|
385
|
+
if (!signature) continue;
|
|
386
|
+
const occurrences = typeof entry === 'object' && Number.isInteger(entry.occurrences)
|
|
387
|
+
? Math.max(1, entry.occurrences)
|
|
388
|
+
: (typeof entry === 'object' && Number.isInteger(entry.count) ? Math.max(1, entry.count) : 1);
|
|
389
|
+
const title = (typeof entry === 'object' && entry.title) ? entry.title : `failure signature ${String(signature).slice(0, 12)}`;
|
|
390
|
+
const severity = (typeof entry === 'object' && entry.severity) ? entry.severity : 'unknown';
|
|
391
|
+
for (let k = 0; k < occurrences; k += 1) {
|
|
392
|
+
count += 1;
|
|
393
|
+
findings.push(makeFinding({
|
|
394
|
+
source_type: 'progress', feature_slug: slug, finding_id: null,
|
|
395
|
+
severity, title, date: null, status: 'open',
|
|
396
|
+
source_path: sourcePath, signature: String(signature)
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return { findings, warnings, count };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// --- 7. Devlogs (aioson-logs/) ----------------------------------------------
|
|
405
|
+
|
|
406
|
+
function readDevlogs({ rootDir, targetDir, slug }) {
|
|
407
|
+
const findings = [];
|
|
408
|
+
const warnings = [];
|
|
409
|
+
let count = 0;
|
|
410
|
+
const logsDir = path.join(targetDir, 'aioson-logs');
|
|
411
|
+
for (const ent of listFilesSafe(logsDir)) {
|
|
412
|
+
if (!ent.isFile() || !ent.name.endsWith('.md')) continue;
|
|
413
|
+
const full = path.join(logsDir, ent.name);
|
|
414
|
+
const text = readTextSafe(full);
|
|
415
|
+
if (text === null) continue;
|
|
416
|
+
const { data } = parseFrontmatter(text);
|
|
417
|
+
if (data.feature !== slug) continue;
|
|
418
|
+
count += 1;
|
|
419
|
+
}
|
|
420
|
+
return { findings, warnings, count };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- Localização de artefatos por feature (ativo + arquivado) ---------------
|
|
424
|
+
|
|
425
|
+
function dirExists(p) {
|
|
426
|
+
try {
|
|
427
|
+
return fs.statSync(p).isDirectory();
|
|
428
|
+
} catch {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Resolve os diretórios/arquivos de uma feature em locais ativos E arquivados
|
|
435
|
+
* (`done/{slug}/`). Não inventa um segundo enumerador — espelha o layout que
|
|
436
|
+
* `feature-archive.js` produz.
|
|
437
|
+
*/
|
|
438
|
+
function resolveLocations(targetDir, ctxDir, slug) {
|
|
439
|
+
const doneRoot = path.join(ctxDir, 'done', slug);
|
|
440
|
+
|
|
441
|
+
const qaDirs = [ctxDir, doneRoot].filter(dirExists);
|
|
442
|
+
|
|
443
|
+
const planDirs = [
|
|
444
|
+
path.join(targetDir, '.aioson', 'plans', slug),
|
|
445
|
+
path.join(doneRoot, 'plans')
|
|
446
|
+
].filter(dirExists);
|
|
447
|
+
|
|
448
|
+
const dossierCandidates = [
|
|
449
|
+
path.join(ctxDir, 'features', slug, 'dossier.md'),
|
|
450
|
+
path.join(doneRoot, 'features', slug, 'dossier.md'),
|
|
451
|
+
path.join(doneRoot, 'dossier.md')
|
|
452
|
+
];
|
|
453
|
+
// SF-02: lstat (não statSync) para NÃO seguir symlink — consistente com os
|
|
454
|
+
// demais readers, que pulam symlinks via Dirent.isFile(). Um dossier.md que
|
|
455
|
+
// seja um symlink apontando para fora do workspace é ignorado, não seguido.
|
|
456
|
+
const dossierFiles = dossierCandidates.filter((p) => {
|
|
457
|
+
try { return fs.lstatSync(p).isFile(); } catch { return false; }
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
return { qaDirs, planDirs, dossierFiles, doneRoot };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Minera todas as fontes de UMA feature. Best-effort: nunca lança.
|
|
465
|
+
*
|
|
466
|
+
* @returns {{ slug, findings, cycles, counts, cost, minedPaths, warnings }}
|
|
467
|
+
*/
|
|
468
|
+
function collectFeatureSources(targetDir, slug) {
|
|
469
|
+
const ctxDir = path.join(targetDir, '.aioson', 'context');
|
|
470
|
+
const rootDir = targetDir;
|
|
471
|
+
const locations = resolveLocations(targetDir, ctxDir, slug);
|
|
472
|
+
|
|
473
|
+
const warnings = [];
|
|
474
|
+
const findings = [];
|
|
475
|
+
const cycles = [];
|
|
476
|
+
|
|
477
|
+
const qa = readQaReports({ rootDir, ctxDir, slug, locations });
|
|
478
|
+
const corr = readCorrections({ rootDir, slug, locations });
|
|
479
|
+
const trail = readDossierTrail({ rootDir, slug, locations });
|
|
480
|
+
const events = readExecutionEvents({ rootDir, targetDir, slug });
|
|
481
|
+
const attempts = readAttempts({ rootDir, slug, locations });
|
|
482
|
+
const sigs = readFailureSignatures({ rootDir, slug, locations });
|
|
483
|
+
const devlogs = readDevlogs({ rootDir, targetDir, slug });
|
|
484
|
+
|
|
485
|
+
for (const r of [qa, corr, trail, events, attempts, sigs, devlogs]) {
|
|
486
|
+
findings.push(...r.findings);
|
|
487
|
+
warnings.push(...r.warnings);
|
|
488
|
+
}
|
|
489
|
+
cycles.push(...trail.cycles);
|
|
490
|
+
|
|
491
|
+
const counts = {
|
|
492
|
+
qa_reports: qa.count,
|
|
493
|
+
corrections: corr.count,
|
|
494
|
+
dossier_trail: trail.count,
|
|
495
|
+
execution_events: events.count,
|
|
496
|
+
attempts: attempts.count,
|
|
497
|
+
failure_signatures: sigs.count,
|
|
498
|
+
devlogs: devlogs.count
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const cost = {
|
|
502
|
+
execution_events: events.count,
|
|
503
|
+
corrections: corr.entries,
|
|
504
|
+
fail_pass_cycles: cycles.length,
|
|
505
|
+
corrections_bytes: corr.bytes,
|
|
506
|
+
token_count_available: events.tokenAvailable,
|
|
507
|
+
token_total: events.tokenAvailable ? events.tokenTotal : null
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const minedPaths = [];
|
|
511
|
+
for (const d of locations.qaDirs) minedPaths.push(relPath(rootDir, d));
|
|
512
|
+
for (const d of locations.planDirs) minedPaths.push(relPath(rootDir, d));
|
|
513
|
+
for (const f of locations.dossierFiles) minedPaths.push(relPath(rootDir, f));
|
|
514
|
+
minedPaths.sort();
|
|
515
|
+
|
|
516
|
+
return { slug, findings, cycles, counts, cost, minedPaths, warnings };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Minera uma janela de features (1+). Soma contagens/custo e concatena
|
|
521
|
+
* findings/cycles (a chave de agrupamento inclui o slug, então misturar é seguro).
|
|
522
|
+
*/
|
|
523
|
+
function collectSources(targetDir, slugs) {
|
|
524
|
+
const list = Array.isArray(slugs) ? slugs : [slugs];
|
|
525
|
+
const findings = [];
|
|
526
|
+
const cycles = [];
|
|
527
|
+
const warnings = [];
|
|
528
|
+
const minedPaths = [];
|
|
529
|
+
const counts = { qa_reports: 0, corrections: 0, dossier_trail: 0, execution_events: 0, attempts: 0, failure_signatures: 0, devlogs: 0 };
|
|
530
|
+
const cost = { execution_events: 0, corrections: 0, fail_pass_cycles: 0, corrections_bytes: 0, token_count_available: false, token_total: null };
|
|
531
|
+
const costByFeature = {};
|
|
532
|
+
|
|
533
|
+
for (const slug of list) {
|
|
534
|
+
const f = collectFeatureSources(targetDir, slug);
|
|
535
|
+
findings.push(...f.findings);
|
|
536
|
+
cycles.push(...f.cycles);
|
|
537
|
+
warnings.push(...f.warnings);
|
|
538
|
+
minedPaths.push(...f.minedPaths);
|
|
539
|
+
costByFeature[slug] = f.cost;
|
|
540
|
+
for (const k of Object.keys(counts)) counts[k] += f.counts[k];
|
|
541
|
+
cost.execution_events += f.cost.execution_events;
|
|
542
|
+
cost.corrections += f.cost.corrections;
|
|
543
|
+
cost.fail_pass_cycles += f.cost.fail_pass_cycles;
|
|
544
|
+
cost.corrections_bytes += f.cost.corrections_bytes;
|
|
545
|
+
if (f.cost.token_count_available) {
|
|
546
|
+
cost.token_count_available = true;
|
|
547
|
+
cost.token_total = (cost.token_total || 0) + (f.cost.token_total || 0);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return { features_mined: list.slice(), findings, cycles, counts, cost, costByFeature, minedPaths, warnings };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Subdiretórios de `.aioson/context/done/` = features fechadas (D6). */
|
|
555
|
+
function enumerateClosedFeatures(targetDir) {
|
|
556
|
+
const doneDir = path.join(targetDir, '.aioson', 'context', 'done');
|
|
557
|
+
const slugs = [];
|
|
558
|
+
for (const ent of listFilesSafe(doneDir)) {
|
|
559
|
+
if (ent.isDirectory()) slugs.push(ent.name);
|
|
560
|
+
}
|
|
561
|
+
slugs.sort();
|
|
562
|
+
return slugs;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Data de PASS de uma feature para ordenar a janela `--last=N` (D2: trail vence).
|
|
567
|
+
* Prioridade: último PASS do Agent Trail → frontmatter de QA report → null.
|
|
568
|
+
* (O caller pode cair em MANIFEST `completed` quando isto retornar null.)
|
|
569
|
+
*/
|
|
570
|
+
function resolveFeatureExists(targetDir, slug) {
|
|
571
|
+
const ctxDir = path.join(targetDir, '.aioson', 'context');
|
|
572
|
+
const loc = resolveLocations(targetDir, ctxDir, slug);
|
|
573
|
+
if (loc.planDirs.length > 0 || loc.dossierFiles.length > 0) return true;
|
|
574
|
+
if (dirExists(loc.doneRoot)) return true;
|
|
575
|
+
if (dirExists(path.join(ctxDir, 'features', slug))) return true;
|
|
576
|
+
// qa-report-{slug}*.md no contexto ativo
|
|
577
|
+
for (const ent of listFilesSafe(ctxDir)) {
|
|
578
|
+
if (ent.isFile() && /^qa-report-/.test(ent.name) && ent.name.includes(slug) && ent.name.endsWith('.md')) return true;
|
|
579
|
+
}
|
|
580
|
+
// listado em features.md
|
|
581
|
+
const featuresText = readTextSafe(path.join(ctxDir, 'features.md'));
|
|
582
|
+
if (featuresText && new RegExp(`\\b${slug.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}\\b`).test(featuresText)) return true;
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function resolvePassDate(targetDir, slug) {
|
|
587
|
+
const ctxDir = path.join(targetDir, '.aioson', 'context');
|
|
588
|
+
const locations = resolveLocations(targetDir, ctxDir, slug);
|
|
589
|
+
|
|
590
|
+
// 1. Trail: maior pass_at dos ciclos FAIL→PASS.
|
|
591
|
+
const trail = readDossierTrail({ rootDir: targetDir, slug, locations });
|
|
592
|
+
if (trail.cycles.length > 0) {
|
|
593
|
+
return trail.cycles.map((c) => c.pass_at).sort().slice(-1)[0];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// 2. QA report com verdict PASS → created_at/updated_at.
|
|
597
|
+
let best = null;
|
|
598
|
+
for (const dir of locations.qaDirs) {
|
|
599
|
+
for (const ent of listFilesSafe(dir)) {
|
|
600
|
+
if (!ent.isFile() || !/^qa-report-/.test(ent.name) || !ent.name.includes(slug) || !ent.name.endsWith('.md')) continue;
|
|
601
|
+
const text = readTextSafe(path.join(dir, ent.name));
|
|
602
|
+
if (text === null) continue;
|
|
603
|
+
const { data } = parseFrontmatter(text);
|
|
604
|
+
if ((data.verdict || '').toUpperCase() === 'FAIL') continue;
|
|
605
|
+
const d = isoDate(data.updated_at || data.created_at || data.date);
|
|
606
|
+
if (d && (!best || d > best)) best = d;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return best;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
module.exports = {
|
|
613
|
+
collectSources,
|
|
614
|
+
collectFeatureSources,
|
|
615
|
+
resolveLocations,
|
|
616
|
+
resolvePassDate,
|
|
617
|
+
resolveFeatureExists,
|
|
618
|
+
enumerateClosedFeatures,
|
|
619
|
+
normalizeSeverity,
|
|
620
|
+
parseFrontmatter,
|
|
621
|
+
relPath,
|
|
622
|
+
// exportados para teste unitário dos parsers
|
|
623
|
+
_internal: { readQaReports, readCorrections, readDossierTrail, readFailureSignatures, makeFinding }
|
|
624
|
+
};
|
package/src/parser.js
CHANGED
|
@@ -29,7 +29,7 @@ function parseArgv(argv) {
|
|
|
29
29
|
'all', 'force', 'dry-run', 'no-interactive', 'fix', 'json',
|
|
30
30
|
'help', 'version', 'no-launch', 'attach', 'tmux',
|
|
31
31
|
'allow-warnings', 'install-hook', 'uninstall-hook', 'remove-hook',
|
|
32
|
-
'agent-safe',
|
|
32
|
+
'agent-safe', 'agentic',
|
|
33
33
|
'selective',
|
|
34
34
|
'status', 'suggest', 'apply',
|
|
35
35
|
'runtime-only', 'template-only', 'inception', 'locales',
|