@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.
- package/CHANGELOG.md +27 -0
- package/README.md +19 -6
- package/docs/en/5-reference/cli-reference.md +1 -1
- package/docs/pt/5-referencia/comandos-cli.md +1 -1
- package/docs/pt/5-referencia/harness-retro.md +2 -1
- package/package.json +1 -1
- package/src/cli.js +38 -21
- package/src/commands/classify.js +389 -327
- package/src/commands/harness-retro-promote.js +387 -0
- package/src/commands/prototype-check.js +163 -0
- package/src/commands/verify-implementation.js +428 -0
- package/src/commands/workflow-next.js +222 -9
- package/src/constants.js +2 -0
- package/src/lib/retro/retro-render.js +10 -1
- package/src/lib/retro/retro-sources.js +45 -27
- package/src/lib/retro/verification-reports.js +230 -0
- package/src/runtime-store.js +13 -9
- package/src/verification/evidence-bundle.js +251 -0
- package/src/verification/ledger-store.js +221 -0
- package/src/verification/path-policy.js +74 -0
- package/src/verification/policy-engine.js +95 -0
- package/src/verification/prompt-package.js +314 -0
- package/src/verification/redaction.js +77 -0
- package/src/verification/report-parser.js +132 -0
- package/src/verification/report-store.js +97 -0
- package/src/verification/result.js +16 -0
- package/src/verification/runners/index.js +319 -0
- package/src/verification/runtime-telemetry.js +144 -0
- package/src/verification/schema.js +276 -0
- package/src/verification/source-discovery.js +153 -0
- package/template/.aioson/agents/analyst.md +9 -6
- package/template/.aioson/agents/briefing-refiner.md +22 -0
- package/template/.aioson/agents/briefing.md +69 -12
- package/template/.aioson/agents/design-hybrid-forge.md +18 -14
- package/template/.aioson/agents/dev.md +23 -10
- package/template/.aioson/agents/deyvin.md +3 -2
- package/template/.aioson/agents/product.md +15 -11
- package/template/.aioson/agents/qa.md +13 -4
- package/template/.aioson/agents/scope-check.md +19 -5
- package/template/.aioson/agents/sheldon.md +4 -3
- package/template/.aioson/agents/ux-ui.md +3 -2
- package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
- package/template/.aioson/docs/prototype-contract.md +81 -0
- package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
- package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
- package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
- package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
- package/template/.aioson/skills/process/prototype-forge/SKILL.md +92 -0
- 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
|
+
};
|
package/src/runtime-store.js
CHANGED
|
@@ -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
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
+
};
|