@jaimevalasek/aioson 1.30.2 → 1.33.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +19 -6
  3. package/docs/en/5-reference/cli-reference.md +1 -1
  4. package/docs/pt/5-referencia/comandos-cli.md +1 -1
  5. package/docs/pt/5-referencia/harness-retro.md +2 -1
  6. package/package.json +1 -1
  7. package/src/cli.js +38 -21
  8. package/src/commands/classify.js +389 -327
  9. package/src/commands/harness-retro-promote.js +387 -0
  10. package/src/commands/prototype-check.js +163 -0
  11. package/src/commands/verify-implementation.js +428 -0
  12. package/src/commands/workflow-next.js +222 -9
  13. package/src/constants.js +2 -0
  14. package/src/lib/retro/retro-render.js +10 -1
  15. package/src/lib/retro/retro-sources.js +45 -27
  16. package/src/lib/retro/verification-reports.js +230 -0
  17. package/src/runtime-store.js +13 -9
  18. package/src/verification/evidence-bundle.js +251 -0
  19. package/src/verification/ledger-store.js +221 -0
  20. package/src/verification/path-policy.js +74 -0
  21. package/src/verification/policy-engine.js +95 -0
  22. package/src/verification/prompt-package.js +314 -0
  23. package/src/verification/redaction.js +77 -0
  24. package/src/verification/report-parser.js +132 -0
  25. package/src/verification/report-store.js +97 -0
  26. package/src/verification/result.js +16 -0
  27. package/src/verification/runners/index.js +319 -0
  28. package/src/verification/runtime-telemetry.js +144 -0
  29. package/src/verification/schema.js +276 -0
  30. package/src/verification/source-discovery.js +153 -0
  31. package/template/.aioson/agents/analyst.md +9 -6
  32. package/template/.aioson/agents/briefing-refiner.md +22 -0
  33. package/template/.aioson/agents/briefing.md +69 -12
  34. package/template/.aioson/agents/design-hybrid-forge.md +18 -14
  35. package/template/.aioson/agents/dev.md +23 -10
  36. package/template/.aioson/agents/deyvin.md +3 -2
  37. package/template/.aioson/agents/product.md +15 -11
  38. package/template/.aioson/agents/qa.md +13 -4
  39. package/template/.aioson/agents/scope-check.md +19 -5
  40. package/template/.aioson/agents/sheldon.md +4 -3
  41. package/template/.aioson/agents/ux-ui.md +3 -2
  42. package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
  43. package/template/.aioson/docs/prototype-contract.md +81 -0
  44. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
  45. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
  46. package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
  47. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
  48. package/template/.aioson/skills/process/prototype-forge/SKILL.md +92 -0
  49. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+
7
+ const { validateVerificationReport } = require('../../verification/schema');
8
+
9
+ function isFileSafe(filePath) {
10
+ try {
11
+ return fs.lstatSync(filePath).isFile();
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ function listFilesSafe(dir) {
18
+ try {
19
+ return fs.readdirSync(dir, { withFileTypes: true });
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ function readTextSafe(filePath) {
26
+ try {
27
+ return fs.readFileSync(filePath, 'utf8');
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function collectReportFiles(featureDirs) {
34
+ const seen = new Set();
35
+ const files = [];
36
+
37
+ for (const featureDir of featureDirs || []) {
38
+ const latest = path.join(featureDir, 'verification-report.md');
39
+ if (isFileSafe(latest)) {
40
+ seen.add(latest);
41
+ files.push(latest);
42
+ }
43
+
44
+ const runsDir = path.join(featureDir, 'verification-runs');
45
+ for (const entry of listFilesSafe(runsDir)) {
46
+ if (!entry.isFile()) continue;
47
+ if (!/-report\.md$/i.test(entry.name)) continue;
48
+ const full = path.join(runsDir, entry.name);
49
+ if (seen.has(full)) continue;
50
+ seen.add(full);
51
+ files.push(full);
52
+ }
53
+ }
54
+
55
+ files.sort();
56
+ return files;
57
+ }
58
+
59
+ function extractMachineReport(content) {
60
+ const text = String(content || '');
61
+ const heading = /^##\s+Machine Report\s*$/im.exec(text);
62
+ if (!heading) return { ok: false, reason: 'missing_machine_report' };
63
+ const afterHeading = text.slice(heading.index + heading[0].length);
64
+ const block = afterHeading.match(/```(?:json)?\s*([\s\S]*?)```/i);
65
+ if (!block) return { ok: false, reason: 'missing_machine_report_json' };
66
+ try {
67
+ return { ok: true, report: JSON.parse(block[1]) };
68
+ } catch {
69
+ return { ok: false, reason: 'invalid_machine_report_json' };
70
+ }
71
+ }
72
+
73
+ function stableStringify(value) {
74
+ if (Array.isArray(value)) {
75
+ return `[${value.map(stableStringify).join(',')}]`;
76
+ }
77
+ if (value && typeof value === 'object') {
78
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
79
+ }
80
+ return JSON.stringify(value);
81
+ }
82
+
83
+ function reportIdentity(report) {
84
+ return crypto.createHash('sha1').update(stableStringify(report)).digest('hex');
85
+ }
86
+
87
+ function isLatestReportPath(filePath) {
88
+ return path.basename(filePath) === 'verification-report.md';
89
+ }
90
+
91
+ function shouldSkipPromotedDuplicate(seenReports, identity, filePath) {
92
+ const seen = seenReports.get(identity);
93
+ if (!seen) {
94
+ seenReports.set(identity, {
95
+ paths: [filePath],
96
+ skippedPromotedDuplicate: false
97
+ });
98
+ return false;
99
+ }
100
+
101
+ const involvesLatest = isLatestReportPath(filePath) || seen.paths.some(isLatestReportPath);
102
+ seen.paths.push(filePath);
103
+ if (involvesLatest && !seen.skippedPromotedDuplicate) {
104
+ seen.skippedPromotedDuplicate = true;
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+
110
+ function timestampFromPath(filePath) {
111
+ const name = path.basename(filePath);
112
+ const match = name.match(/(\d{8}T\d{6}Z)/);
113
+ if (!match) return null;
114
+ return match[1].replace(
115
+ /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/,
116
+ '$1-$2-$3T$4:$5:$6Z'
117
+ );
118
+ }
119
+
120
+ function severityFromVerification(severity) {
121
+ const value = String(severity || '').toLowerCase();
122
+ if (value === 'blocking') return 'high';
123
+ if (value === 'warning') return 'medium';
124
+ if (value === 'info') return 'info';
125
+ return 'unknown';
126
+ }
127
+
128
+ function statusFromVerification(status) {
129
+ const value = String(status || '').toUpperCase();
130
+ if (value === 'CONFIRMS' || value === 'NOT_APPLICABLE') return 'fixed';
131
+ if (value === 'DOES_NOT_CONFIRM' || value === 'PARTIAL' || value === 'NOT_VERIFIED') return 'open';
132
+ return 'unknown';
133
+ }
134
+
135
+ function fileRefFromFinding(finding) {
136
+ if (!finding || !finding.file) return null;
137
+ return finding.line ? `${finding.file}:${finding.line}` : String(finding.file);
138
+ }
139
+
140
+ function titleFromFinding(report, finding) {
141
+ const parts = [
142
+ finding.kind || 'verification',
143
+ finding.status || report.verdict || 'UNKNOWN'
144
+ ];
145
+ if (finding.claim_id) parts.push(`claim ${finding.claim_id}`);
146
+ return parts.join(' ');
147
+ }
148
+
149
+ function isRetroRelevantFinding(finding) {
150
+ const status = String((finding && finding.status) || '').toUpperCase();
151
+ return status === 'DOES_NOT_CONFIRM' || status === 'PARTIAL' || status === 'NOT_VERIFIED';
152
+ }
153
+
154
+ function reportFindings({ report, slug, sourcePath, sourceDate, makeFinding }) {
155
+ const findings = Array.isArray(report.findings) ? report.findings : [];
156
+ return findings.filter(isRetroRelevantFinding).map((finding, index) => makeFinding({
157
+ source_type: 'verification_report',
158
+ feature_slug: slug,
159
+ finding_id: finding.id ? String(finding.id).slice(0, 80) : `VR-${index + 1}`,
160
+ severity: severityFromVerification(finding.severity),
161
+ title: titleFromFinding(report, finding),
162
+ file_ref: fileRefFromFinding(finding),
163
+ date: sourceDate,
164
+ status: statusFromVerification(finding.status),
165
+ source_path: sourcePath,
166
+ signature: null
167
+ }));
168
+ }
169
+
170
+ function readVerificationReports({ rootDir, slug, locations, makeFinding, relPath }) {
171
+ const warnings = [];
172
+ const findings = [];
173
+ const files = collectReportFiles(locations.featureDirs);
174
+ const countedFiles = [];
175
+ const seenReports = new Map();
176
+ let count = 0;
177
+
178
+ for (const full of files) {
179
+ const sourcePath = relPath(rootDir, full);
180
+ const text = readTextSafe(full);
181
+ if (text === null) {
182
+ warnings.push(`verification_report ilegível: ${sourcePath}`);
183
+ continue;
184
+ }
185
+
186
+ const parsed = extractMachineReport(text);
187
+ if (!parsed.ok) {
188
+ warnings.push(`verification_report ignorado (${parsed.reason}): ${sourcePath}`);
189
+ continue;
190
+ }
191
+
192
+ const errors = validateVerificationReport(parsed.report, {
193
+ slug,
194
+ requestedPolicy: parsed.report.policy || 'standard'
195
+ });
196
+ if (errors.length > 0) {
197
+ const compact = errors.map((error) => `${error.field}:${error.reason}`).join(', ');
198
+ warnings.push(`verification_report inválido (${compact}): ${sourcePath}`);
199
+ continue;
200
+ }
201
+
202
+ const identity = reportIdentity(parsed.report);
203
+ if (shouldSkipPromotedDuplicate(seenReports, identity, full)) continue;
204
+ countedFiles.push(full);
205
+ count += 1;
206
+ findings.push(...reportFindings({
207
+ report: parsed.report,
208
+ slug,
209
+ sourcePath,
210
+ sourceDate: timestampFromPath(full),
211
+ makeFinding
212
+ }));
213
+ }
214
+
215
+ return { findings, warnings, count, files: countedFiles };
216
+ }
217
+
218
+ module.exports = {
219
+ readVerificationReports,
220
+ _internal: {
221
+ collectReportFiles,
222
+ extractMachineReport,
223
+ reportIdentity,
224
+ shouldSkipPromotedDuplicate,
225
+ severityFromVerification,
226
+ statusFromVerification,
227
+ titleFromFinding,
228
+ isRetroRelevantFinding
229
+ }
230
+ };
@@ -1567,15 +1567,19 @@ function startRun(db, options) {
1567
1567
  finished_at: status === 'completed' || status === 'failed' ? now : null
1568
1568
  });
1569
1569
 
1570
- appendRunEvent(db, {
1571
- runKey,
1572
- eventType: String(options.eventType || 'start'),
1573
- phase: options.phase || 'run',
1574
- status,
1575
- message: String(options.message || options.title || 'Agent started'),
1576
- payload: options.payload,
1577
- createdAt: now
1578
- });
1570
+ appendRunEvent(db, {
1571
+ runKey,
1572
+ eventType: String(options.eventType || 'start'),
1573
+ phase: options.phase || 'run',
1574
+ status,
1575
+ toolName: options.toolName,
1576
+ message: String(options.message || options.title || 'Agent started'),
1577
+ payload: options.payload,
1578
+ verdict: options.verdict,
1579
+ tokenCount: options.tokenCount,
1580
+ progressPct: options.progressPct,
1581
+ createdAt: now
1582
+ });
1579
1583
 
1580
1584
  return runKey;
1581
1585
  }
@@ -0,0 +1,251 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { execFileSync } = require('node:child_process');
6
+
7
+ const { resolveInsideRoot } = require('./path-policy');
8
+ const { createCounter, redactText, redactJson, totalRedactions } = require('./redaction');
9
+
10
+ const ARTIFACT_PREVIEW_CHARS = 700;
11
+ const ARTIFACT_PREVIEW_TOTAL_CHARS = 8000;
12
+ const ARTIFACT_SUMMARY_LIMIT = 60;
13
+
14
+ function runGit(rootDir, args) {
15
+ try {
16
+ return {
17
+ ok: true,
18
+ output: execFileSync('git', args, {
19
+ cwd: rootDir,
20
+ encoding: 'utf8',
21
+ stdio: ['ignore', 'pipe', 'pipe']
22
+ }).trim()
23
+ };
24
+ } catch (error) {
25
+ return {
26
+ ok: false,
27
+ output: '',
28
+ error: error.message
29
+ };
30
+ }
31
+ }
32
+
33
+ async function fileExists(rootDir, relPath) {
34
+ const safe = resolveInsideRoot(rootDir, relPath);
35
+ if (!safe.ok) return false;
36
+ try {
37
+ const stat = await fs.stat(safe.path);
38
+ return stat.isFile() || stat.isDirectory();
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ async function fileStat(rootDir, relPath) {
45
+ const safe = resolveInsideRoot(rootDir, relPath);
46
+ if (!safe.ok) return { ok: false, reason: safe.reason };
47
+ try {
48
+ const stat = await fs.stat(safe.path);
49
+ return { ok: true, path: safe.path, stat };
50
+ } catch {
51
+ return { ok: false, reason: 'not_found' };
52
+ }
53
+ }
54
+
55
+ function evidencePathsFromLedger(ledger) {
56
+ if (!ledger || !Array.isArray(ledger.claims)) return [];
57
+ const paths = [];
58
+ for (const claim of ledger.claims) {
59
+ if (!claim || !Array.isArray(claim.evidence)) continue;
60
+ for (const evidence of claim.evidence) {
61
+ if (evidence && evidence.path) paths.push(evidence.path);
62
+ }
63
+ }
64
+ return [...new Set(paths)];
65
+ }
66
+
67
+ async function knownChecks(rootDir, slug, ledger, policy = 'standard') {
68
+ const checks = [];
69
+ const addCheck = (candidate) => {
70
+ if (!candidate || !candidate.command) return;
71
+ const existing = checks.find((check) => check.command === candidate.command);
72
+ if (!existing) {
73
+ checks.push(candidate);
74
+ return;
75
+ }
76
+ existing.required = Boolean(existing.required || candidate.required);
77
+ existing.source = existing.source === candidate.source
78
+ ? existing.source
79
+ : `${existing.source},${candidate.source}`;
80
+ existing.last_status = candidate.last_status || existing.last_status || null;
81
+ };
82
+
83
+ if (await fileExists(rootDir, 'package.json')) {
84
+ addCheck({ command: 'npm test', source: 'package.json', required: false });
85
+ }
86
+ if (await fileExists(rootDir, 'scripts/check-js.js')) {
87
+ addCheck({ command: 'node scripts/check-js.js', source: 'scripts/check-js.js', required: false });
88
+ }
89
+ if (await fileExists(rootDir, `.aioson/context/prd-${slug}.md`)) {
90
+ const strictFlag = policy === 'strict' ? ' --strict' : '';
91
+ addCheck({ command: `aioson prototype:check . --feature=${slug}${strictFlag}`, source: 'prototype_contract', required: false });
92
+ }
93
+ if (await fileExists(rootDir, `.aioson/plans/${slug}/harness-contract.json`)) {
94
+ addCheck({ command: `aioson harness:check . --slug=${slug}`, source: 'harness_contract', required: false });
95
+ }
96
+ if (ledger && Array.isArray(ledger.verification_commands)) {
97
+ for (const item of ledger.verification_commands) {
98
+ if (item && item.command) {
99
+ addCheck({
100
+ command: item.command,
101
+ source: 'ledger',
102
+ required: Boolean(item.required),
103
+ last_status: item.last_status || null
104
+ });
105
+ }
106
+ }
107
+ }
108
+ return checks;
109
+ }
110
+
111
+ function commandPlan(commands) {
112
+ return (commands || []).map((command, index) => ({
113
+ order: index + 1,
114
+ command: command.command,
115
+ required: Boolean(command.required),
116
+ source: command.source || 'unknown',
117
+ last_status: command.last_status || null,
118
+ run_by: 'developer_or_clean_auditor',
119
+ expectation: command.required ? 'must be run or explicitly justified' : 'run when relevant to touched surface'
120
+ }));
121
+ }
122
+
123
+ function dirtyWorktree(statusOutput) {
124
+ return Boolean(String(statusOutput || '').trim());
125
+ }
126
+
127
+ function artifactCanPreview(relPath) {
128
+ return /\.(md|txt|json|js|ts|tsx|jsx|html|css|yaml|yml)$/i.test(String(relPath || ''));
129
+ }
130
+
131
+ async function artifactSummaries(rootDir, artifacts, redactionCounter) {
132
+ const summaries = [];
133
+ let usedPreviewChars = 0;
134
+
135
+ for (const artifact of (artifacts || []).slice(0, ARTIFACT_SUMMARY_LIMIT)) {
136
+ const relPath = artifact.path;
137
+ const stat = await fileStat(rootDir, relPath);
138
+ const summary = {
139
+ type: artifact.type,
140
+ role: artifact.role,
141
+ path: relPath,
142
+ exists: stat.ok,
143
+ size_bytes: stat.ok ? stat.stat.size : null
144
+ };
145
+
146
+ if (!stat.ok) {
147
+ summary.omitted_reason = stat.reason;
148
+ summaries.push(summary);
149
+ continue;
150
+ }
151
+ if (!stat.stat.isFile()) {
152
+ summary.omitted_reason = 'not_file';
153
+ summaries.push(summary);
154
+ continue;
155
+ }
156
+ if (!artifactCanPreview(relPath)) {
157
+ summary.omitted_reason = 'unsupported_preview_type';
158
+ summaries.push(summary);
159
+ continue;
160
+ }
161
+ if (usedPreviewChars >= ARTIFACT_PREVIEW_TOTAL_CHARS) {
162
+ summary.omitted_reason = 'preview_budget_exhausted';
163
+ summaries.push(summary);
164
+ continue;
165
+ }
166
+
167
+ const remaining = ARTIFACT_PREVIEW_TOTAL_CHARS - usedPreviewChars;
168
+ const limit = Math.min(ARTIFACT_PREVIEW_CHARS, remaining);
169
+ const handle = await fs.open(stat.path, 'r');
170
+ try {
171
+ const buffer = Buffer.alloc(limit);
172
+ const { bytesRead } = await handle.read(buffer, 0, limit, 0);
173
+ const preview = buffer.subarray(0, bytesRead).toString('utf8');
174
+ usedPreviewChars += preview.length;
175
+ summary.preview = redactText(preview, redactionCounter);
176
+ summary.preview_truncated = stat.stat.size > bytesRead;
177
+ } finally {
178
+ await handle.close();
179
+ }
180
+ summaries.push(summary);
181
+ }
182
+
183
+ if ((artifacts || []).length > ARTIFACT_SUMMARY_LIMIT) {
184
+ summaries.push({
185
+ type: 'budget_notice',
186
+ role: 'artifact_summary',
187
+ path: null,
188
+ exists: false,
189
+ omitted_reason: `artifact_summary_limit_${ARTIFACT_SUMMARY_LIMIT}`,
190
+ omitted_count: artifacts.length - ARTIFACT_SUMMARY_LIMIT
191
+ });
192
+ }
193
+
194
+ return summaries;
195
+ }
196
+
197
+ async function buildEvidenceBundle(rootDir, slug, ledger, sourceArtifacts, policy) {
198
+ const redactionCounter = createCounter();
199
+ const branch = runGit(rootDir, ['rev-parse', '--abbrev-ref', 'HEAD']);
200
+ const status = runGit(rootDir, ['status', '--short']);
201
+ const diffStat = runGit(rootDir, ['diff', '--stat']);
202
+ const artifactPaths = (sourceArtifacts || []).map((artifact) => artifact.path);
203
+ const evidencePaths = evidencePathsFromLedger(ledger);
204
+ const touchedPaths = [...new Set([...artifactPaths, ...evidencePaths])].filter(Boolean).slice(0, 25);
205
+ const recentCommits = touchedPaths.length > 0
206
+ ? runGit(rootDir, ['log', '--oneline', '-n', '5', '--', ...touchedPaths])
207
+ : { ok: true, output: '' };
208
+
209
+ const verificationCommands = redactJson(await knownChecks(rootDir, slug, ledger, policy), redactionCounter);
210
+ const sanitizedLedgerClaims = redactJson(ledger && Array.isArray(ledger.claims) ? ledger.claims : [], redactionCounter);
211
+ const sanitizedKnownGaps = redactJson(ledger && Array.isArray(ledger.known_gaps) ? ledger.known_gaps : [], redactionCounter);
212
+ const summaries = await artifactSummaries(rootDir, sourceArtifacts || [], redactionCounter);
213
+ const git = {
214
+ branch: branch.ok ? redactText(branch.output, redactionCounter) : null,
215
+ branch_error: branch.ok ? null : branch.error,
216
+ status: status.ok ? redactText(status.output, redactionCounter) : null,
217
+ dirty_worktree: status.ok ? dirtyWorktree(status.output) : null,
218
+ status_error: status.ok ? null : status.error,
219
+ diff_stat: diffStat.ok ? redactText(diffStat.output, redactionCounter) : null,
220
+ diff_stat_error: diffStat.ok ? null : diffStat.error,
221
+ recent_commits: recentCommits.ok ? redactText(recentCommits.output, redactionCounter) : null,
222
+ recent_commits_error: recentCommits.ok ? null : recentCommits.error
223
+ };
224
+
225
+ return {
226
+ feature_slug: slug,
227
+ policy,
228
+ git,
229
+ source_artifacts: sourceArtifacts || [],
230
+ artifact_summaries: summaries,
231
+ ledger_claims: sanitizedLedgerClaims,
232
+ known_gaps: sanitizedKnownGaps,
233
+ verification_commands: verificationCommands,
234
+ command_plan: commandPlan(verificationCommands),
235
+ redactions: {
236
+ ...redactionCounter,
237
+ total: totalRedactions(redactionCounter)
238
+ },
239
+ preview_budget: {
240
+ artifact_preview_chars: ARTIFACT_PREVIEW_CHARS,
241
+ artifact_preview_total_chars: ARTIFACT_PREVIEW_TOTAL_CHARS,
242
+ artifact_summary_limit: ARTIFACT_SUMMARY_LIMIT
243
+ },
244
+ generated_at: new Date().toISOString(),
245
+ project_root: path.resolve(rootDir)
246
+ };
247
+ }
248
+
249
+ module.exports = {
250
+ buildEvidenceBundle
251
+ };