@dogfood-lab/findings 1.2.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/LICENSE +21 -0
- package/README.md +96 -0
- package/advise/advice-bundle.js +155 -0
- package/advise/index.js +5 -0
- package/advise/query.js +182 -0
- package/cli.js +936 -0
- package/derive/dedupe.js +107 -0
- package/derive/derive-findings.js +187 -0
- package/derive/ids.js +48 -0
- package/derive/index.js +9 -0
- package/derive/load-records.js +153 -0
- package/derive/rules.js +415 -0
- package/derive/write-findings.js +63 -0
- package/index.js +11 -0
- package/lib/atomic-write.js +47 -0
- package/lib/file-lock.js +359 -0
- package/lib/rename-with-retry.js +43 -0
- package/package.json +70 -0
- package/reader.js +156 -0
- package/review/event-log.js +177 -0
- package/review/index.js +6 -0
- package/review/review-engine.js +288 -0
- package/review/transitions.js +79 -0
- package/synthesis/doctrine-derivation.js +128 -0
- package/synthesis/index.js +8 -0
- package/synthesis/pattern-derivation.js +184 -0
- package/synthesis/recommendation-derivation.js +156 -0
- package/synthesis/validate-artifacts.js +46 -0
- package/synthesis/write-artifacts.js +75 -0
- package/validate.js +87 -0
package/cli.js
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* dogfood findings CLI
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* list List all findings (supports --repo, --status, --surface, --issue-kind, --transfer-scope)
|
|
8
|
+
* show <id> Show a single finding by finding_id
|
|
9
|
+
* validate Validate all findings (or a specific file with --file)
|
|
10
|
+
* derive Derive candidate findings from records (--dry-run or --write)
|
|
11
|
+
* explain Explain how a derived finding was produced
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { resolve, dirname, relative } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import {
|
|
18
|
+
validateFindingFile,
|
|
19
|
+
validateFinding,
|
|
20
|
+
discoverFindings,
|
|
21
|
+
discoverFixtures,
|
|
22
|
+
loadFindings,
|
|
23
|
+
findById,
|
|
24
|
+
filterFindings,
|
|
25
|
+
findDuplicates
|
|
26
|
+
} from './index.js';
|
|
27
|
+
import {
|
|
28
|
+
deriveFromRecord,
|
|
29
|
+
deriveFromRecords,
|
|
30
|
+
getRuleInventory,
|
|
31
|
+
getRuleById,
|
|
32
|
+
dedupeAgainstExisting,
|
|
33
|
+
loadRecordById,
|
|
34
|
+
loadRecordsForRepo,
|
|
35
|
+
loadAllRecords,
|
|
36
|
+
writeFindings
|
|
37
|
+
} from './derive/index.js';
|
|
38
|
+
import {
|
|
39
|
+
performAction,
|
|
40
|
+
performMerge,
|
|
41
|
+
getReviewQueue,
|
|
42
|
+
getEventsForFinding
|
|
43
|
+
} from './review/index.js';
|
|
44
|
+
import {
|
|
45
|
+
derivePatterns,
|
|
46
|
+
deriveRecommendations,
|
|
47
|
+
deriveDoctrine,
|
|
48
|
+
validatePattern,
|
|
49
|
+
validateRecommendation,
|
|
50
|
+
validateDoctrine,
|
|
51
|
+
writePattern,
|
|
52
|
+
writeRecommendation,
|
|
53
|
+
writeDoctrine,
|
|
54
|
+
loadPatterns,
|
|
55
|
+
loadRecommendations,
|
|
56
|
+
loadDoctrines
|
|
57
|
+
} from './synthesis/index.js';
|
|
58
|
+
import {
|
|
59
|
+
generateAdviceBundle,
|
|
60
|
+
generateSyncExport
|
|
61
|
+
} from './advise/index.js';
|
|
62
|
+
|
|
63
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
const ROOT = resolve(__dirname, '../..');
|
|
65
|
+
|
|
66
|
+
function parseArgs(argv) {
|
|
67
|
+
const args = argv.slice(2);
|
|
68
|
+
const command = args[0];
|
|
69
|
+
const positional = [];
|
|
70
|
+
const flags = {};
|
|
71
|
+
|
|
72
|
+
for (let i = 1; i < args.length; i++) {
|
|
73
|
+
const arg = args[i];
|
|
74
|
+
if (arg.startsWith('--')) {
|
|
75
|
+
const eqIdx = arg.indexOf('=');
|
|
76
|
+
if (eqIdx !== -1) {
|
|
77
|
+
flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
78
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
79
|
+
flags[arg.slice(2)] = args[++i];
|
|
80
|
+
} else {
|
|
81
|
+
flags[arg.slice(2)] = true;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
positional.push(arg);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { command, positional, flags };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatFindingSummary(f, rootDir) {
|
|
92
|
+
const rel = relative(rootDir, f.path);
|
|
93
|
+
const d = f.data;
|
|
94
|
+
const valid = f.valid ? 'valid' : 'INVALID';
|
|
95
|
+
return `[${d.status}] ${d.finding_id} (${d.product_surface}, ${d.issue_kind}, ${d.transfer_scope}) [${valid}]\n ${d.title}\n ${rel}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatFindingDetail(f, rootDir) {
|
|
99
|
+
const d = f.data;
|
|
100
|
+
const lines = [
|
|
101
|
+
`Finding: ${d.finding_id}`,
|
|
102
|
+
`Title: ${d.title}`,
|
|
103
|
+
`Status: ${d.status}`,
|
|
104
|
+
`Repo: ${d.repo}`,
|
|
105
|
+
`Surface: ${d.product_surface}`,
|
|
106
|
+
d.execution_mode ? `Mode: ${d.execution_mode}` : null,
|
|
107
|
+
`Stage: ${d.journey_stage}`,
|
|
108
|
+
``,
|
|
109
|
+
`Issue: ${d.issue_kind}`,
|
|
110
|
+
`Root cause: ${d.root_cause_kind}`,
|
|
111
|
+
`Remediation: ${d.remediation_kind}`,
|
|
112
|
+
`Scope: ${d.transfer_scope}`,
|
|
113
|
+
``,
|
|
114
|
+
`Summary:`,
|
|
115
|
+
` ${d.summary.trim()}`,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
if (d.doctrine_statement) {
|
|
119
|
+
lines.push('', 'Doctrine:', ` ${d.doctrine_statement.trim()}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (d.notes) {
|
|
123
|
+
lines.push('', 'Notes:', ` ${d.notes.trim()}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
lines.push('', `Source records: ${d.source_record_ids.join(', ')}`);
|
|
127
|
+
|
|
128
|
+
if (d.scenario_ids && d.scenario_ids.length) {
|
|
129
|
+
lines.push(`Scenarios: ${d.scenario_ids.join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.push('', `Evidence (${d.evidence.length}):`);
|
|
133
|
+
for (const e of d.evidence) {
|
|
134
|
+
const parts = [` - ${e.evidence_kind}`];
|
|
135
|
+
if (e.record_id) parts.push(`record=${e.record_id}`);
|
|
136
|
+
if (e.scenario_id) parts.push(`scenario=${e.scenario_id}`);
|
|
137
|
+
if (e.doc_ref) parts.push(`doc=${e.doc_ref}`);
|
|
138
|
+
if (e.policy_ref) parts.push(`policy=${e.policy_ref}`);
|
|
139
|
+
if (e.artifact_ref) parts.push(`artifact=${e.artifact_ref}`);
|
|
140
|
+
if (e.note) parts.push(`| ${e.note}`);
|
|
141
|
+
lines.push(parts.join(' '));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (d.fix_refs && d.fix_refs.length) {
|
|
145
|
+
lines.push('', `Fix refs (${d.fix_refs.length}):`);
|
|
146
|
+
for (const r of d.fix_refs) {
|
|
147
|
+
const parts = [` - ${r.ref_kind}: ${r.ref}`];
|
|
148
|
+
if (r.note) parts.push(`| ${r.note}`);
|
|
149
|
+
lines.push(parts.join(' '));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
lines.push('', `Valid: ${f.valid ? 'yes' : 'NO'}`);
|
|
154
|
+
if (!f.valid && f.errors.length) {
|
|
155
|
+
for (const err of f.errors) {
|
|
156
|
+
lines.push(` ERROR: ${err.path} — ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
lines.push(`File: ${relative(rootDir, f.path)}`);
|
|
161
|
+
|
|
162
|
+
return lines.filter(l => l !== null).join('\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function main() {
|
|
166
|
+
const { command, positional, flags } = parseArgs(process.argv);
|
|
167
|
+
|
|
168
|
+
if (!command || command === 'help' || command === '--help') {
|
|
169
|
+
console.log(`dogfood findings — finding contract spine + derivation engine
|
|
170
|
+
|
|
171
|
+
Commands:
|
|
172
|
+
list List all findings
|
|
173
|
+
show <finding_id> Show a single finding in detail
|
|
174
|
+
validate Validate all findings (or --file <path> for one)
|
|
175
|
+
validate --all Validate all findings + all fixtures
|
|
176
|
+
derive Derive candidate findings from records
|
|
177
|
+
explain <finding_id> Show derivation provenance for a finding
|
|
178
|
+
rules List all derivation rules
|
|
179
|
+
accept <id> Accept a finding (--actor, --reason)
|
|
180
|
+
reject <id> Reject a finding (--actor, --reason, --reject-reason)
|
|
181
|
+
review <id> Move finding to reviewed (--actor)
|
|
182
|
+
edit <id> Edit finding fields (--actor, --set field=value)
|
|
183
|
+
merge <ids...> Merge findings (--into <id>, --actor, --reason)
|
|
184
|
+
reopen <id> Reopen a rejected/accepted finding (--actor, --reason)
|
|
185
|
+
invalidate <id> Invalidate an accepted finding (--actor, --reason)
|
|
186
|
+
history <id> Show review history for a finding
|
|
187
|
+
queue Show review queue
|
|
188
|
+
|
|
189
|
+
Derive options:
|
|
190
|
+
--record <run_id> Derive from a specific record
|
|
191
|
+
--repo <org/repo> Derive from all records for a repo
|
|
192
|
+
--all Derive from all records
|
|
193
|
+
--dry-run Show what would be emitted (default)
|
|
194
|
+
--write Write candidates to disk
|
|
195
|
+
|
|
196
|
+
Filters (for list):
|
|
197
|
+
--repo <org/repo>
|
|
198
|
+
--status <candidate|reviewed|accepted|rejected>
|
|
199
|
+
--surface <cli|desktop|web|api|mcp-server|npm-package|plugin|library>
|
|
200
|
+
--issue-kind <kind>
|
|
201
|
+
--transfer-scope <scope>
|
|
202
|
+
--include-fixtures Also list fixture findings`);
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (command === 'list') {
|
|
207
|
+
const includeFixtures = flags['include-fixtures'];
|
|
208
|
+
const allFindings = loadFindings(ROOT);
|
|
209
|
+
|
|
210
|
+
if (includeFixtures) {
|
|
211
|
+
allFindings.push(...loadFindings(ROOT, { fixtures: true, fixtureKind: 'valid' }));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const filters = {};
|
|
215
|
+
if (flags.repo) filters.repo = flags.repo;
|
|
216
|
+
if (flags.status) filters.status = flags.status;
|
|
217
|
+
if (flags.surface) filters.surface = flags.surface;
|
|
218
|
+
if (flags['issue-kind']) filters.issueKind = flags['issue-kind'];
|
|
219
|
+
if (flags['transfer-scope']) filters.transferScope = flags['transfer-scope'];
|
|
220
|
+
|
|
221
|
+
const filtered = filterFindings(allFindings, filters);
|
|
222
|
+
|
|
223
|
+
if (filtered.length === 0) {
|
|
224
|
+
console.log('No findings found.');
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const f of filtered) {
|
|
229
|
+
console.log(formatFindingSummary(f, ROOT));
|
|
230
|
+
console.log();
|
|
231
|
+
}
|
|
232
|
+
console.log(`${filtered.length} finding(s)`);
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (command === 'show') {
|
|
237
|
+
const findingId = positional[0];
|
|
238
|
+
if (!findingId) {
|
|
239
|
+
console.error('Usage: dogfood findings show <finding_id>');
|
|
240
|
+
process.exit(2);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const result = findById(ROOT, findingId);
|
|
244
|
+
if (!result) {
|
|
245
|
+
console.error(`Finding not found: ${findingId}`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(formatFindingDetail(result, ROOT));
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (command === 'validate') {
|
|
254
|
+
const singleFile = flags.file;
|
|
255
|
+
const all = flags.all;
|
|
256
|
+
|
|
257
|
+
if (singleFile) {
|
|
258
|
+
const filePath = resolve(singleFile);
|
|
259
|
+
if (!existsSync(filePath)) {
|
|
260
|
+
console.error(`File not found: ${filePath}`);
|
|
261
|
+
process.exit(2);
|
|
262
|
+
}
|
|
263
|
+
const result = validateFindingFile(filePath);
|
|
264
|
+
if (result.valid) {
|
|
265
|
+
console.log(`PASS: ${relative(ROOT, filePath)}`);
|
|
266
|
+
process.exit(0);
|
|
267
|
+
} else {
|
|
268
|
+
console.error(`FAIL: ${relative(ROOT, filePath)}`);
|
|
269
|
+
for (const err of result.errors) {
|
|
270
|
+
console.error(` ${err.path} — ${err.message}`);
|
|
271
|
+
}
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Validate all findings + optionally fixtures
|
|
277
|
+
let failed = 0;
|
|
278
|
+
let passed = 0;
|
|
279
|
+
|
|
280
|
+
const realFindings = loadFindings(ROOT);
|
|
281
|
+
for (const f of realFindings) {
|
|
282
|
+
if (f.valid) {
|
|
283
|
+
console.log(`PASS: ${relative(ROOT, f.path)}`);
|
|
284
|
+
passed++;
|
|
285
|
+
} else {
|
|
286
|
+
console.error(`FAIL: ${relative(ROOT, f.path)}`);
|
|
287
|
+
for (const err of f.errors) {
|
|
288
|
+
console.error(` ${err.path} — ${err.message}`);
|
|
289
|
+
}
|
|
290
|
+
failed++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (all) {
|
|
295
|
+
// Valid fixtures should all pass
|
|
296
|
+
const validFixtures = loadFindings(ROOT, { fixtures: true, fixtureKind: 'valid' });
|
|
297
|
+
for (const f of validFixtures) {
|
|
298
|
+
if (f.valid) {
|
|
299
|
+
console.log(`PASS (fixture): ${relative(ROOT, f.path)}`);
|
|
300
|
+
passed++;
|
|
301
|
+
} else {
|
|
302
|
+
console.error(`FAIL (fixture): ${relative(ROOT, f.path)}`);
|
|
303
|
+
for (const err of f.errors) {
|
|
304
|
+
console.error(` ${err.path} — ${err.message}`);
|
|
305
|
+
}
|
|
306
|
+
failed++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Invalid fixtures should all fail
|
|
311
|
+
const invalidFixtures = loadFindings(ROOT, { fixtures: true, fixtureKind: 'invalid' });
|
|
312
|
+
for (const f of invalidFixtures) {
|
|
313
|
+
if (!f.valid) {
|
|
314
|
+
console.log(`PASS (expected invalid): ${relative(ROOT, f.path)}`);
|
|
315
|
+
passed++;
|
|
316
|
+
} else {
|
|
317
|
+
console.error(`FAIL (expected invalid but passed): ${relative(ROOT, f.path)}`);
|
|
318
|
+
failed++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Duplicate check
|
|
324
|
+
const allForDupes = [...loadFindings(ROOT)];
|
|
325
|
+
const dupes = findDuplicates(allForDupes);
|
|
326
|
+
if (dupes.length > 0) {
|
|
327
|
+
for (const d of dupes) {
|
|
328
|
+
console.error(`DUPLICATE: ${d.findingId} found in ${d.paths.map(p => relative(ROOT, p)).join(', ')}`);
|
|
329
|
+
failed++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
334
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (command === 'derive') {
|
|
338
|
+
const recordId = flags.record;
|
|
339
|
+
const repoKey = flags.repo;
|
|
340
|
+
const all = flags.all;
|
|
341
|
+
const write = flags.write;
|
|
342
|
+
|
|
343
|
+
// Load records based on scope
|
|
344
|
+
let entries = [];
|
|
345
|
+
if (recordId) {
|
|
346
|
+
const entry = loadRecordById(ROOT, recordId);
|
|
347
|
+
if (!entry) {
|
|
348
|
+
console.error(`Record not found: ${recordId}`);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
entries = [entry];
|
|
352
|
+
} else if (repoKey) {
|
|
353
|
+
entries = loadRecordsForRepo(ROOT, repoKey);
|
|
354
|
+
if (entries.length === 0) {
|
|
355
|
+
console.error(`No records found for repo: ${repoKey}`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
} else if (all) {
|
|
359
|
+
entries = loadAllRecords(ROOT);
|
|
360
|
+
if (entries.length === 0) {
|
|
361
|
+
console.error('No records found.');
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
console.error('Specify --record <run_id>, --repo <org/repo>, or --all');
|
|
366
|
+
process.exit(2);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Derive
|
|
370
|
+
const { candidates, ruleErrors, stats } = deriveFromRecords(entries);
|
|
371
|
+
|
|
372
|
+
// Surface per-rule throws — derivation only "succeeded" when ruleErrors is empty.
|
|
373
|
+
// Exit non-zero so CI never silently treats a partially-degraded run as green.
|
|
374
|
+
if (ruleErrors.length > 0) {
|
|
375
|
+
console.error(`${ruleErrors.length} rule error(s) during derivation:`);
|
|
376
|
+
for (const e of ruleErrors) {
|
|
377
|
+
console.error(` rule=${e.ruleId} run_id=${e.runId} msg=${e.message}`);
|
|
378
|
+
}
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Validate all candidates against schema
|
|
383
|
+
const invalid = candidates.filter(c => !validateFinding(c).valid);
|
|
384
|
+
if (invalid.length > 0) {
|
|
385
|
+
console.error(`${invalid.length} candidate(s) failed schema validation:`);
|
|
386
|
+
for (const c of invalid) {
|
|
387
|
+
const result = validateFinding(c);
|
|
388
|
+
console.error(` ${c.finding_id}: ${result.errors.map(e => `${e.path} ${e.message}`).join('; ')}`);
|
|
389
|
+
}
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Dedupe against existing findings
|
|
394
|
+
const existingFindings = loadFindings(ROOT);
|
|
395
|
+
const { toWrite, skippedUnchanged, collisions } = dedupeAgainstExisting(candidates, existingFindings);
|
|
396
|
+
|
|
397
|
+
// Report
|
|
398
|
+
console.log(`Processed ${stats.recordsProcessed} record(s)`);
|
|
399
|
+
console.log(`Rules evaluated: ${stats.rulesEvaluated}`);
|
|
400
|
+
console.log(`Candidates emitted: ${candidates.length}`);
|
|
401
|
+
if (stats.deduped > 0) console.log(`Deduped within batch: ${stats.deduped}`);
|
|
402
|
+
if (skippedUnchanged > 0) console.log(`Skipped unchanged: ${skippedUnchanged}`);
|
|
403
|
+
if (collisions.length > 0) {
|
|
404
|
+
console.log(`Collisions: ${collisions.length}`);
|
|
405
|
+
for (const c of collisions) {
|
|
406
|
+
console.log(` ${c.findingId} (existing status: ${c.existingStatus})`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
console.log();
|
|
410
|
+
|
|
411
|
+
for (const c of toWrite) {
|
|
412
|
+
console.log(` - ${c.finding_id}`);
|
|
413
|
+
console.log(` rule: ${c.derived.rule_id}`);
|
|
414
|
+
const evSummary = c.evidence.map(e => `${e.evidence_kind}:${e.record_id || e.scenario_id || ''}`).join(', ');
|
|
415
|
+
console.log(` evidence: ${evSummary}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!write) {
|
|
419
|
+
console.log(`\n(dry-run) ${toWrite.length} candidate(s) would be written. Use --write to materialize.`);
|
|
420
|
+
process.exit(0);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Write mode
|
|
424
|
+
const { written, errors } = writeFindings(ROOT, toWrite);
|
|
425
|
+
console.log(`\nWritten: ${written.length}`);
|
|
426
|
+
for (const p of written) {
|
|
427
|
+
console.log(` ${relative(ROOT, p)}`);
|
|
428
|
+
}
|
|
429
|
+
if (errors.length > 0) {
|
|
430
|
+
console.error(`Errors: ${errors.length}`);
|
|
431
|
+
for (const e of errors) {
|
|
432
|
+
console.error(` ${e.findingId}: ${e.error}`);
|
|
433
|
+
}
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
process.exit(0);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (command === 'explain') {
|
|
440
|
+
const findingId = positional[0];
|
|
441
|
+
if (!findingId) {
|
|
442
|
+
console.error('Usage: dogfood findings explain <finding_id>');
|
|
443
|
+
process.exit(2);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = findById(ROOT, findingId);
|
|
447
|
+
if (!result) {
|
|
448
|
+
console.error(`Finding not found: ${findingId}`);
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const d = result.data;
|
|
453
|
+
const lines = [
|
|
454
|
+
`Finding: ${d.finding_id}`,
|
|
455
|
+
`Title: ${d.title}`,
|
|
456
|
+
`Status: ${d.status}`,
|
|
457
|
+
''
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
if (d.derived) {
|
|
461
|
+
lines.push(
|
|
462
|
+
'Derivation:',
|
|
463
|
+
` Method: ${d.derived.method}`,
|
|
464
|
+
` Rule: ${d.derived.rule_id}`,
|
|
465
|
+
` Derived: ${d.derived.derived_at}`,
|
|
466
|
+
'',
|
|
467
|
+
'Rationale:',
|
|
468
|
+
` ${d.derived.rationale.trim()}`,
|
|
469
|
+
''
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const rule = getRuleById(d.derived.rule_id);
|
|
473
|
+
if (rule) {
|
|
474
|
+
lines.push(`Rule description:`, ` ${rule.description}`, '');
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
lines.push('(Hand-authored finding — no derivation metadata)', '');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
lines.push(`Source records: ${d.source_record_ids.join(', ')}`);
|
|
481
|
+
if (d.scenario_ids?.length) {
|
|
482
|
+
lines.push(`Scenarios: ${d.scenario_ids.join(', ')}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
lines.push('', `Evidence (${d.evidence.length}):`);
|
|
486
|
+
for (const e of d.evidence) {
|
|
487
|
+
const parts = [` - ${e.evidence_kind}`];
|
|
488
|
+
if (e.record_id) parts.push(`record=${e.record_id}`);
|
|
489
|
+
if (e.scenario_id) parts.push(`scenario=${e.scenario_id}`);
|
|
490
|
+
if (e.note) parts.push(`| ${e.note}`);
|
|
491
|
+
lines.push(parts.join(' '));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
lines.push('', `Classification:`);
|
|
495
|
+
lines.push(` Issue: ${d.issue_kind}`);
|
|
496
|
+
lines.push(` Root cause: ${d.root_cause_kind}`);
|
|
497
|
+
lines.push(` Remediation: ${d.remediation_kind}`);
|
|
498
|
+
lines.push(` Scope: ${d.transfer_scope}`);
|
|
499
|
+
|
|
500
|
+
console.log(lines.join('\n'));
|
|
501
|
+
process.exit(0);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── Review commands ──────────────────────────────
|
|
505
|
+
|
|
506
|
+
if (['accept', 'reject', 'review', 'reopen', 'invalidate'].includes(command)) {
|
|
507
|
+
const findingId = positional[0];
|
|
508
|
+
if (!findingId) {
|
|
509
|
+
console.error(`Usage: dogfood findings ${command} <finding_id> --actor <name> [--reason "..."]`);
|
|
510
|
+
process.exit(2);
|
|
511
|
+
}
|
|
512
|
+
const actor = flags.actor || 'operator';
|
|
513
|
+
const result = performAction(ROOT, {
|
|
514
|
+
findingId,
|
|
515
|
+
action: command,
|
|
516
|
+
actor,
|
|
517
|
+
reason: flags.reason,
|
|
518
|
+
rejectReason: flags['reject-reason'],
|
|
519
|
+
notes: flags.notes
|
|
520
|
+
});
|
|
521
|
+
if (!result.success) {
|
|
522
|
+
console.error(`FAILED: ${result.error}`);
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
console.log(`${command}: ${findingId} → ${result.finding.status}`);
|
|
526
|
+
if (result.event) {
|
|
527
|
+
console.log(`Event: ${result.event.review_event_id} (${result.event.from_status} → ${result.event.to_status})`);
|
|
528
|
+
}
|
|
529
|
+
process.exit(0);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (command === 'edit') {
|
|
533
|
+
const findingId = positional[0];
|
|
534
|
+
if (!findingId) {
|
|
535
|
+
console.error('Usage: dogfood findings edit <finding_id> --actor <name> --set field=value [--set field=value]');
|
|
536
|
+
process.exit(2);
|
|
537
|
+
}
|
|
538
|
+
// Parse --set flags
|
|
539
|
+
const fieldChanges = {};
|
|
540
|
+
const args = process.argv.slice(2);
|
|
541
|
+
for (let i = 0; i < args.length; i++) {
|
|
542
|
+
if (args[i] === '--set' && i + 1 < args.length) {
|
|
543
|
+
const [field, ...rest] = args[++i].split('=');
|
|
544
|
+
fieldChanges[field] = rest.join('=');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (Object.keys(fieldChanges).length === 0) {
|
|
548
|
+
console.error('No field changes specified. Use --set field=value');
|
|
549
|
+
process.exit(2);
|
|
550
|
+
}
|
|
551
|
+
const actor = flags.actor || 'operator';
|
|
552
|
+
const result = performAction(ROOT, {
|
|
553
|
+
findingId,
|
|
554
|
+
action: 'edit',
|
|
555
|
+
actor,
|
|
556
|
+
fieldChanges,
|
|
557
|
+
notes: flags.notes
|
|
558
|
+
});
|
|
559
|
+
if (!result.success) {
|
|
560
|
+
console.error(`FAILED: ${result.error}`);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
console.log(`Edited: ${findingId}`);
|
|
564
|
+
if (result.event?.field_changes) {
|
|
565
|
+
for (const [field, change] of Object.entries(result.event.field_changes)) {
|
|
566
|
+
console.log(` ${field}: "${change.from}" → "${change.to}"`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
process.exit(0);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (command === 'merge') {
|
|
573
|
+
const sourceIds = positional;
|
|
574
|
+
const canonicalId = flags.into;
|
|
575
|
+
const actor = flags.actor || 'operator';
|
|
576
|
+
const reason = flags.reason;
|
|
577
|
+
if (sourceIds.length < 2 || !canonicalId) {
|
|
578
|
+
console.error('Usage: dogfood findings merge <id1> <id2> [<id3>...] --into <canonical_id> --actor <name> --reason "..."');
|
|
579
|
+
process.exit(2);
|
|
580
|
+
}
|
|
581
|
+
const result = performMerge(ROOT, { sourceIds, canonicalId, actor, reason });
|
|
582
|
+
if (!result.success) {
|
|
583
|
+
console.error(`FAILED: ${result.error}`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
console.log(`Merged into: ${canonicalId}`);
|
|
587
|
+
console.log(`Sources superseded: ${sourceIds.filter(id => id !== canonicalId).join(', ')}`);
|
|
588
|
+
console.log(`Evidence count: ${result.canonical.evidence.length}`);
|
|
589
|
+
console.log(`Source records: ${result.canonical.source_record_ids.length}`);
|
|
590
|
+
process.exit(0);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (command === 'history') {
|
|
594
|
+
const findingId = positional[0];
|
|
595
|
+
if (!findingId) {
|
|
596
|
+
console.error('Usage: dogfood findings history <finding_id>');
|
|
597
|
+
process.exit(2);
|
|
598
|
+
}
|
|
599
|
+
const events = getEventsForFinding(ROOT, findingId);
|
|
600
|
+
if (events.length === 0) {
|
|
601
|
+
console.log(`No review history for: ${findingId}`);
|
|
602
|
+
process.exit(0);
|
|
603
|
+
}
|
|
604
|
+
console.log(`Review history for ${findingId} (${events.length} event(s)):\n`);
|
|
605
|
+
for (const e of events) {
|
|
606
|
+
console.log(` ${e.timestamp} ${e.action} ${e.from_status} → ${e.to_status} by ${e.actor}`);
|
|
607
|
+
if (e.reason) console.log(` Reason: ${e.reason}`);
|
|
608
|
+
if (e.field_changes) {
|
|
609
|
+
for (const [field, change] of Object.entries(e.field_changes)) {
|
|
610
|
+
console.log(` ${field}: "${change.from}" → "${change.to}"`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (e.merged_from_ids) console.log(` Merged from: ${e.merged_from_ids.join(', ')}`);
|
|
614
|
+
}
|
|
615
|
+
process.exit(0);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (command === 'queue') {
|
|
619
|
+
const queue = getReviewQueue(ROOT);
|
|
620
|
+
if (queue.length === 0) {
|
|
621
|
+
console.log('Review queue is empty.');
|
|
622
|
+
process.exit(0);
|
|
623
|
+
}
|
|
624
|
+
console.log(`Review queue (${queue.length} item(s)):\n`);
|
|
625
|
+
for (const item of queue) {
|
|
626
|
+
console.log(` [${item.data.status}] ${item.data.finding_id}`);
|
|
627
|
+
console.log(` ${item.queueReason}`);
|
|
628
|
+
console.log(` ${item.data.title}`);
|
|
629
|
+
console.log();
|
|
630
|
+
}
|
|
631
|
+
process.exit(0);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ── Synthesis commands ──────────────────────────────
|
|
635
|
+
|
|
636
|
+
if (command === 'patterns') {
|
|
637
|
+
const sub = positional[0];
|
|
638
|
+
if (sub === 'derive') {
|
|
639
|
+
const write = flags.write;
|
|
640
|
+
const { patterns, stats } = derivePatterns(ROOT, { includeFixtures: flags['include-fixtures'] });
|
|
641
|
+
|
|
642
|
+
// Validate all
|
|
643
|
+
const invalid = patterns.filter(p => !validatePattern(p).valid);
|
|
644
|
+
if (invalid.length > 0) {
|
|
645
|
+
console.error(`${invalid.length} pattern(s) failed schema validation`);
|
|
646
|
+
for (const p of invalid) {
|
|
647
|
+
const r = validatePattern(p);
|
|
648
|
+
console.error(` ${p.pattern_id}: ${r.errors.map(e => `${e.path} ${e.message}`).join('; ')}`);
|
|
649
|
+
}
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
console.log(`Findings considered: ${stats.findingsConsidered}`);
|
|
654
|
+
console.log(`Clusters found: ${stats.clustersFound}`);
|
|
655
|
+
console.log(`Below threshold: ${stats.belowThreshold}`);
|
|
656
|
+
console.log(`Patterns emitted: ${patterns.length}\n`);
|
|
657
|
+
|
|
658
|
+
for (const p of patterns) {
|
|
659
|
+
console.log(` ${p.pattern_id} [${p.pattern_strength}]`);
|
|
660
|
+
console.log(` ${p.title}`);
|
|
661
|
+
console.log(` findings: ${p.source_finding_ids.join(', ')}`);
|
|
662
|
+
console.log();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (write && patterns.length > 0) {
|
|
666
|
+
for (const p of patterns) {
|
|
667
|
+
const path = writePattern(ROOT, p);
|
|
668
|
+
console.log(`Written: ${relative(ROOT, path)}`);
|
|
669
|
+
}
|
|
670
|
+
} else if (!write && patterns.length > 0) {
|
|
671
|
+
console.log(`(dry-run) ${patterns.length} pattern(s) would be written. Use --write to materialize.`);
|
|
672
|
+
}
|
|
673
|
+
process.exit(0);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (sub === 'list') {
|
|
677
|
+
const patterns = loadPatterns(ROOT);
|
|
678
|
+
if (patterns.length === 0) { console.log('No patterns found.'); process.exit(0); }
|
|
679
|
+
for (const p of patterns) {
|
|
680
|
+
console.log(`[${p.status}] ${p.pattern_id} (${p.pattern_strength || 'unknown'})`);
|
|
681
|
+
console.log(` ${p.title}`);
|
|
682
|
+
console.log();
|
|
683
|
+
}
|
|
684
|
+
console.log(`${patterns.length} pattern(s)`);
|
|
685
|
+
process.exit(0);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (sub === 'show' || sub === 'explain') {
|
|
689
|
+
const id = positional[1];
|
|
690
|
+
if (!id) { console.error(`Usage: dogfood findings patterns ${sub} <pattern_id>`); process.exit(2); }
|
|
691
|
+
const all = loadPatterns(ROOT);
|
|
692
|
+
const p = all.find(x => x.pattern_id === id);
|
|
693
|
+
if (!p) { console.error(`Pattern not found: ${id}`); process.exit(1); }
|
|
694
|
+
|
|
695
|
+
console.log(`Pattern: ${p.pattern_id}`);
|
|
696
|
+
console.log(`Title: ${p.title}`);
|
|
697
|
+
console.log(`Status: ${p.status}`);
|
|
698
|
+
console.log(`Kind: ${p.pattern_kind}`);
|
|
699
|
+
console.log(`Strength: ${p.pattern_strength || 'unknown'}`);
|
|
700
|
+
console.log(`Scope: ${p.transfer_scope}`);
|
|
701
|
+
console.log(`\nSummary:\n ${p.summary}`);
|
|
702
|
+
console.log(`\nSupport: ${p.support.finding_count} findings, ${p.support.repo_count} repos, ${p.support.surface_count} surfaces`);
|
|
703
|
+
console.log(`\nSource findings: ${p.source_finding_ids.join(', ')}`);
|
|
704
|
+
console.log(`Dimensions: surfaces=${(p.dimensions.product_surfaces||[]).join(',')}, issues=${(p.dimensions.issue_kinds||[]).join(',')}`);
|
|
705
|
+
if (p.lineage_note) console.log(`\nLineage: ${p.lineage_note}`);
|
|
706
|
+
process.exit(0);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
console.error('Usage: dogfood findings patterns <derive|list|show|explain> [options]');
|
|
710
|
+
process.exit(2);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (command === 'recommendations') {
|
|
714
|
+
const sub = positional[0];
|
|
715
|
+
if (sub === 'derive') {
|
|
716
|
+
const write = flags.write;
|
|
717
|
+
const { recommendations, stats } = deriveRecommendations(ROOT);
|
|
718
|
+
|
|
719
|
+
console.log(`Patterns considered: ${stats.patternsConsidered}`);
|
|
720
|
+
console.log(`Recommendations emitted: ${stats.recommendationsEmitted}\n`);
|
|
721
|
+
|
|
722
|
+
for (const r of recommendations) {
|
|
723
|
+
console.log(` ${r.recommendation_id} [${r.confidence}]`);
|
|
724
|
+
console.log(` ${r.title}`);
|
|
725
|
+
console.log();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (write && recommendations.length > 0) {
|
|
729
|
+
for (const r of recommendations) {
|
|
730
|
+
const path = writeRecommendation(ROOT, r);
|
|
731
|
+
console.log(`Written: ${relative(ROOT, path)}`);
|
|
732
|
+
}
|
|
733
|
+
} else if (!write && recommendations.length > 0) {
|
|
734
|
+
console.log(`(dry-run) ${recommendations.length} recommendation(s) would be written. Use --write to materialize.`);
|
|
735
|
+
}
|
|
736
|
+
process.exit(0);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (sub === 'list') {
|
|
740
|
+
const recs = loadRecommendations(ROOT);
|
|
741
|
+
if (recs.length === 0) { console.log('No recommendations found.'); process.exit(0); }
|
|
742
|
+
for (const r of recs) {
|
|
743
|
+
console.log(`[${r.status}] ${r.recommendation_id}`);
|
|
744
|
+
console.log(` ${r.title}`);
|
|
745
|
+
console.log();
|
|
746
|
+
}
|
|
747
|
+
process.exit(0);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (sub === 'show') {
|
|
751
|
+
const id = positional[1];
|
|
752
|
+
if (!id) { console.error('Usage: dogfood findings recommendations show <id>'); process.exit(2); }
|
|
753
|
+
const all = loadRecommendations(ROOT);
|
|
754
|
+
const r = all.find(x => x.recommendation_id === id);
|
|
755
|
+
if (!r) { console.error(`Recommendation not found: ${id}`); process.exit(1); }
|
|
756
|
+
console.log(`Recommendation: ${r.recommendation_id}`);
|
|
757
|
+
console.log(`Title: ${r.title}`);
|
|
758
|
+
console.log(`Status: ${r.status}`);
|
|
759
|
+
console.log(`Kind: ${r.recommendation_kind}`);
|
|
760
|
+
console.log(`Confidence: ${r.confidence}`);
|
|
761
|
+
console.log(`\nSummary:\n ${r.summary}`);
|
|
762
|
+
console.log(`\nAction: ${r.action.type} → ${r.action.target}`);
|
|
763
|
+
console.log(` ${r.action.details}`);
|
|
764
|
+
console.log(`\nBased on patterns: ${r.based_on_pattern_ids.join(', ')}`);
|
|
765
|
+
console.log(`Applies to: ${(r.applies_to?.product_surfaces || []).join(', ')}`);
|
|
766
|
+
process.exit(0);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
console.error('Usage: dogfood findings recommendations <derive|list|show> [options]');
|
|
770
|
+
process.exit(2);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (command === 'doctrine') {
|
|
774
|
+
const sub = positional[0];
|
|
775
|
+
if (sub === 'derive') {
|
|
776
|
+
const write = flags.write;
|
|
777
|
+
const { doctrines, stats } = deriveDoctrine(ROOT);
|
|
778
|
+
|
|
779
|
+
console.log(`Patterns considered: ${stats.patternsConsidered}`);
|
|
780
|
+
console.log(`Doctrines emitted: ${stats.doctrinesEmitted}`);
|
|
781
|
+
console.log(`Below threshold: ${stats.belowThreshold}\n`);
|
|
782
|
+
|
|
783
|
+
for (const d of doctrines) {
|
|
784
|
+
console.log(` ${d.doctrine_id} [${d.strength}]`);
|
|
785
|
+
console.log(` ${d.title}`);
|
|
786
|
+
console.log();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (write && doctrines.length > 0) {
|
|
790
|
+
for (const d of doctrines) {
|
|
791
|
+
const path = writeDoctrine(ROOT, d);
|
|
792
|
+
console.log(`Written: ${relative(ROOT, path)}`);
|
|
793
|
+
}
|
|
794
|
+
} else if (!write && doctrines.length > 0) {
|
|
795
|
+
console.log(`(dry-run) ${doctrines.length} doctrine(s) would be written. Use --write to materialize.`);
|
|
796
|
+
}
|
|
797
|
+
process.exit(0);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (sub === 'list') {
|
|
801
|
+
const docs = loadDoctrines(ROOT);
|
|
802
|
+
if (docs.length === 0) { console.log('No doctrine found.'); process.exit(0); }
|
|
803
|
+
for (const d of docs) {
|
|
804
|
+
console.log(`[${d.status}] ${d.doctrine_id} [${d.strength}]`);
|
|
805
|
+
console.log(` ${d.statement}`);
|
|
806
|
+
console.log();
|
|
807
|
+
}
|
|
808
|
+
process.exit(0);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (sub === 'show') {
|
|
812
|
+
const id = positional[1];
|
|
813
|
+
if (!id) { console.error('Usage: dogfood findings doctrine show <id>'); process.exit(2); }
|
|
814
|
+
const all = loadDoctrines(ROOT);
|
|
815
|
+
const d = all.find(x => x.doctrine_id === id);
|
|
816
|
+
if (!d) { console.error(`Doctrine not found: ${id}`); process.exit(1); }
|
|
817
|
+
console.log(`Doctrine: ${d.doctrine_id}`);
|
|
818
|
+
console.log(`Title: ${d.title}`);
|
|
819
|
+
console.log(`Status: ${d.status}`);
|
|
820
|
+
console.log(`Kind: ${d.doctrine_kind}`);
|
|
821
|
+
console.log(`Strength: ${d.strength}`);
|
|
822
|
+
console.log(`Scope: ${d.transfer_scope}`);
|
|
823
|
+
console.log(`\nStatement:\n ${d.statement}`);
|
|
824
|
+
console.log(`\nRationale:\n ${d.rationale}`);
|
|
825
|
+
console.log(`\nBased on patterns: ${d.based_on_pattern_ids.join(', ')}`);
|
|
826
|
+
process.exit(0);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
console.error('Usage: dogfood findings doctrine <derive|list|show> [options]');
|
|
830
|
+
process.exit(2);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ── Advice commands ──────────────────────────────
|
|
834
|
+
|
|
835
|
+
if (command === 'advise') {
|
|
836
|
+
const surface = flags.surface;
|
|
837
|
+
const executionMode = flags['execution-mode'] || flags.mode;
|
|
838
|
+
const repo = flags.repo;
|
|
839
|
+
|
|
840
|
+
if (!surface && !repo) {
|
|
841
|
+
console.error('Usage: dogfood findings advise --surface <surface> [--execution-mode <mode>] [--repo <org/repo>]');
|
|
842
|
+
process.exit(2);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const bundle = generateAdviceBundle(ROOT, { surface, executionMode, repo });
|
|
846
|
+
const a = bundle.advice;
|
|
847
|
+
|
|
848
|
+
console.log(`Advice for: ${[surface, executionMode, repo].filter(Boolean).join(', ') || 'general'}\n`);
|
|
849
|
+
|
|
850
|
+
if (a.starter_checks.length > 0) {
|
|
851
|
+
console.log(`Starter checks (${a.starter_checks.length}):`);
|
|
852
|
+
for (const c of a.starter_checks) {
|
|
853
|
+
console.log(` [${c.confidence}] ${c.id}`);
|
|
854
|
+
console.log(` ${c.title}`);
|
|
855
|
+
}
|
|
856
|
+
console.log();
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (a.evidence_expectations.length > 0) {
|
|
860
|
+
console.log(`Evidence expectations (${a.evidence_expectations.length}):`);
|
|
861
|
+
for (const e of a.evidence_expectations) {
|
|
862
|
+
console.log(` [${e.confidence}] ${e.id}`);
|
|
863
|
+
console.log(` ${e.title}`);
|
|
864
|
+
}
|
|
865
|
+
console.log();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (a.verification_rules.length > 0) {
|
|
869
|
+
console.log(`Verification rules (${a.verification_rules.length}):`);
|
|
870
|
+
for (const v of a.verification_rules) {
|
|
871
|
+
console.log(` [${v.confidence}] ${v.id}`);
|
|
872
|
+
console.log(` ${v.title}`);
|
|
873
|
+
}
|
|
874
|
+
console.log();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (a.likely_failure_classes.length > 0) {
|
|
878
|
+
console.log(`Likely failure classes:`);
|
|
879
|
+
for (const fc of a.likely_failure_classes) {
|
|
880
|
+
console.log(` ${fc.issueKind} (${fc.count} finding(s))`);
|
|
881
|
+
}
|
|
882
|
+
console.log();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (a.relevant_doctrine.length > 0) {
|
|
886
|
+
console.log(`Relevant doctrine (${a.relevant_doctrine.length}):`);
|
|
887
|
+
for (const d of a.relevant_doctrine) {
|
|
888
|
+
console.log(` [${d.strength}] ${d.id}`);
|
|
889
|
+
console.log(` ${d.statement}`);
|
|
890
|
+
}
|
|
891
|
+
console.log();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
console.log(`Support: ${bundle.support.pattern_count} patterns, ${bundle.support.finding_count} findings, ${bundle.support.recommendation_count} recommendations, ${bundle.support.doctrine_count} doctrine`);
|
|
895
|
+
if (bundle.support.pattern_ids.length > 0) {
|
|
896
|
+
console.log(`Pattern IDs: ${bundle.support.pattern_ids.join(', ')}`);
|
|
897
|
+
}
|
|
898
|
+
process.exit(0);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (command === 'sync-export') {
|
|
902
|
+
const bundle = generateSyncExport(ROOT);
|
|
903
|
+
const json = flags.json;
|
|
904
|
+
|
|
905
|
+
if (json) {
|
|
906
|
+
console.log(JSON.stringify(bundle, null, 2));
|
|
907
|
+
} else {
|
|
908
|
+
console.log(`Dogfood sync export (${bundle.exported_at})`);
|
|
909
|
+
console.log(` Findings: ${bundle.counts.findings}`);
|
|
910
|
+
console.log(` Patterns: ${bundle.counts.patterns}`);
|
|
911
|
+
console.log(` Recommendations: ${bundle.counts.recommendations}`);
|
|
912
|
+
console.log(` Doctrine: ${bundle.counts.doctrine}`);
|
|
913
|
+
console.log(`\nUse --json for machine-readable output.`);
|
|
914
|
+
}
|
|
915
|
+
process.exit(0);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (command === 'rules') {
|
|
919
|
+
const inventory = getRuleInventory();
|
|
920
|
+
console.log(`Derivation rules (${inventory.length}):\n`);
|
|
921
|
+
for (const r of inventory) {
|
|
922
|
+
console.log(` ${r.ruleId}`);
|
|
923
|
+
console.log(` ${r.description}`);
|
|
924
|
+
console.log();
|
|
925
|
+
}
|
|
926
|
+
process.exit(0);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
console.error(`Unknown command: ${command}. Run with --help for usage.`);
|
|
930
|
+
process.exit(2);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
main().catch(err => {
|
|
934
|
+
console.error(err);
|
|
935
|
+
process.exit(2);
|
|
936
|
+
});
|