create-claude-cabinet 0.14.2 → 0.16.0
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/lib/cli.js +27 -2
- package/lib/settings-merge.js +7 -6
- package/package.json +1 -1
- package/templates/cabinet/pib-db-access.md +56 -0
- package/templates/mcp/pib-db.json +11 -0
- package/templates/scripts/pib-db-lib.mjs +281 -0
- package/templates/scripts/pib-db-mcp-server.mjs +292 -0
- package/templates/scripts/pib-db.mjs +38 -249
- package/templates/skills/audit/SKILL.md +29 -2
- package/templates/skills/cabinet/SKILL.md +10 -0
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +360 -0
- package/templates/skills/cabinet-cc-health/SKILL.md +4 -3
- package/templates/skills/cabinet-organized-mind/SKILL.md +24 -4
- package/templates/skills/cc-feedback/SKILL.md +9 -1
- package/templates/skills/cc-publish/SKILL.md +8 -1
- package/templates/skills/debrief/SKILL.md +9 -0
- package/templates/skills/debrief/phases/audit-pattern-capture.md +2 -4
- package/templates/skills/debrief/phases/close-work.md +18 -14
- package/templates/skills/debrief/phases/upstream-feedback.md +10 -2
- package/templates/skills/execute/SKILL.md +11 -0
- package/templates/skills/execute-plans/SKILL.md +15 -11
- package/templates/skills/investigate/SKILL.md +7 -0
- package/templates/skills/orient/SKILL.md +45 -36
- package/templates/skills/plan/SKILL.md +31 -2
- package/templates/skills/plan/phases/overlap-check.md +8 -4
- package/templates/skills/plan/phases/work-tracker.md +6 -2
- package/templates/skills/triage-audit/SKILL.md +17 -7
- package/templates/skills/triage-audit/phases/apply-verdicts.md +14 -5
- package/templates/skills/triage-audit/phases/load-findings.md +8 -5
package/lib/cli.js
CHANGED
|
@@ -252,6 +252,9 @@ function generateSkillIndex(projectDir) {
|
|
|
252
252
|
type: isCabinet ? 'cabinet' : 'workflow',
|
|
253
253
|
};
|
|
254
254
|
|
|
255
|
+
// Argument hint (for skill index consumers)
|
|
256
|
+
if (fm['argument-hint']) entry.argumentHint = fm['argument-hint'];
|
|
257
|
+
|
|
255
258
|
// Invocability flags
|
|
256
259
|
if (fm['disable-model-invocation'] === 'true') entry.manual = true;
|
|
257
260
|
if (fm['user-invocable'] === 'false') entry.userInvocable = false;
|
|
@@ -361,7 +364,7 @@ const MODULES = {
|
|
|
361
364
|
mandatory: false,
|
|
362
365
|
default: true,
|
|
363
366
|
lean: false,
|
|
364
|
-
templates: ['scripts/pib-db.mjs', 'scripts/pib-db-schema.sql', 'scripts/work-tracker-server.mjs', 'scripts/work-tracker-ui.html', 'skills/work-tracker'],
|
|
367
|
+
templates: ['scripts/pib-db.mjs', 'scripts/pib-db-lib.mjs', 'scripts/pib-db-mcp-server.mjs', 'scripts/pib-db-schema.sql', 'scripts/work-tracker-server.mjs', 'scripts/work-tracker-ui.html', 'skills/work-tracker'],
|
|
365
368
|
needsDb: true,
|
|
366
369
|
},
|
|
367
370
|
'planning': {
|
|
@@ -391,7 +394,8 @@ const MODULES = {
|
|
|
391
394
|
'cabinet', 'briefing',
|
|
392
395
|
'skills/cabinet-accessibility', 'skills/cabinet-anti-confirmation',
|
|
393
396
|
'skills/cabinet-architecture', 'skills/cabinet-boundary-man',
|
|
394
|
-
'skills/cabinet-
|
|
397
|
+
'skills/cabinet-anthropic-insider', 'skills/cabinet-cc-health',
|
|
398
|
+
'skills/cabinet-data-integrity',
|
|
395
399
|
'skills/cabinet-debugger', 'skills/cabinet-historian',
|
|
396
400
|
'skills/cabinet-organized-mind', 'skills/cabinet-process-therapist',
|
|
397
401
|
'skills/cabinet-qa', 'skills/cabinet-record-keeper',
|
|
@@ -891,6 +895,27 @@ async function run() {
|
|
|
891
895
|
console.log(` ⚙️ Merged hooks into ${path.relative(projectDir, settingsPath)}`);
|
|
892
896
|
}
|
|
893
897
|
|
|
898
|
+
// --- Merge pib-db MCP server into .mcp.json ---
|
|
899
|
+
if (selectedModules.includes('work-tracking') && !flags.dryRun) {
|
|
900
|
+
try {
|
|
901
|
+
const mcpTemplatePath = path.join(templateRoot, 'mcp', 'pib-db.json');
|
|
902
|
+
if (fs.existsSync(mcpTemplatePath)) {
|
|
903
|
+
const mcpConfig = JSON.parse(fs.readFileSync(mcpTemplatePath, 'utf8'));
|
|
904
|
+
const mcpJsonPath = path.join(projectDir, '.mcp.json');
|
|
905
|
+
let existing = {};
|
|
906
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
907
|
+
existing = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
908
|
+
}
|
|
909
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
910
|
+
Object.assign(existing.mcpServers, mcpConfig.mcpServers);
|
|
911
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
|
|
912
|
+
console.log(` 🔌 Merged pib-db MCP server into .mcp.json`);
|
|
913
|
+
}
|
|
914
|
+
} catch (err) {
|
|
915
|
+
console.log(` ⚠ MCP config merge failed: ${err.message}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
894
919
|
// --- Set up database ---
|
|
895
920
|
if (includeDb && selectedModules.includes('work-tracking') && !flags.dryRun) {
|
|
896
921
|
try {
|
package/lib/settings-merge.js
CHANGED
|
@@ -112,14 +112,15 @@ function mergeSettings(projectDir, { includeDb = true, includeMemory = false } =
|
|
|
112
112
|
if (!settings.hooks[event]) {
|
|
113
113
|
settings.hooks[event] = newHooks;
|
|
114
114
|
} else {
|
|
115
|
-
// Add hooks that don't already exist (check by command path)
|
|
115
|
+
// Add hooks that don't already exist (check by command path or prompt text)
|
|
116
116
|
for (const newHook of newHooks) {
|
|
117
|
-
const
|
|
118
|
-
|
|
117
|
+
const hookKey = h => h.command || h.prompt || '';
|
|
118
|
+
const existingKeys = settings.hooks[event].flatMap(h =>
|
|
119
|
+
h.hooks.map(hh => hookKey(hh))
|
|
119
120
|
);
|
|
120
|
-
const
|
|
121
|
-
const alreadyExists =
|
|
122
|
-
|
|
121
|
+
const newKeys = newHook.hooks.map(h => hookKey(h));
|
|
122
|
+
const alreadyExists = newKeys.every(k =>
|
|
123
|
+
existingKeys.includes(k)
|
|
123
124
|
);
|
|
124
125
|
if (!alreadyExists) {
|
|
125
126
|
settings.hooks[event].push(newHook);
|
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# pib-db Access Protocol
|
|
2
|
+
|
|
3
|
+
How to interact with the process infrastructure database (pib-db).
|
|
4
|
+
|
|
5
|
+
## Preference Order
|
|
6
|
+
|
|
7
|
+
1. **MCP tools (preferred):** If `pib_*` MCP tools are available (check
|
|
8
|
+
by attempting to use them), use them directly. They return structured
|
|
9
|
+
JSON — no parsing needed.
|
|
10
|
+
|
|
11
|
+
2. **CLI fallback:** If MCP tools are not available, use the CLI:
|
|
12
|
+
```bash
|
|
13
|
+
node scripts/pib-db.mjs <command> [args]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Skills should reference this document instead of embedding their own
|
|
17
|
+
fallback logic. The access method is determined once at the start of the
|
|
18
|
+
skill execution:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Check: are pib_* MCP tools available?
|
|
22
|
+
YES → use pib_list_projects, pib_create_action, etc.
|
|
23
|
+
NO → use node scripts/pib-db.mjs list-projects, etc.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Available Operations
|
|
27
|
+
|
|
28
|
+
| MCP Tool | CLI Equivalent | Description |
|
|
29
|
+
| ---------------------- | --------------------------------------- | ------------------------------ |
|
|
30
|
+
| pib_create_project | create-project "name" --area X | Create a project |
|
|
31
|
+
| pib_list_projects | list-projects | List active projects |
|
|
32
|
+
| pib_create_action | create-action "text" --notes X | Create an action (work item) |
|
|
33
|
+
| pib_list_actions | list-actions [--status X] | List actions |
|
|
34
|
+
| pib_update_action | update-action fid --status X | Update action fields |
|
|
35
|
+
| pib_complete_action | complete-action fid | Mark action done |
|
|
36
|
+
| pib_ingest_findings | ingest-findings run-dir | Ingest audit findings |
|
|
37
|
+
| pib_triage | triage finding-id status [notes] | Triage a finding |
|
|
38
|
+
| pib_triage_history | triage-history | Get suppression list |
|
|
39
|
+
| pib_query | query "SQL" | Run arbitrary SQL |
|
|
40
|
+
|
|
41
|
+
## Surface Area Validation
|
|
42
|
+
|
|
43
|
+
`pib_create_action` (and the CLI `create-action`) require that notes
|
|
44
|
+
contain a `## Surface Area` section with at least one `- files:` or
|
|
45
|
+
`- dirs:` line. This ensures every action clearly defines what it
|
|
46
|
+
touches.
|
|
47
|
+
|
|
48
|
+
Example notes format:
|
|
49
|
+
```
|
|
50
|
+
Implement the new feature.
|
|
51
|
+
|
|
52
|
+
## Surface Area
|
|
53
|
+
- files: src/components/Widget.js
|
|
54
|
+
- files: src/utils/helpers.js
|
|
55
|
+
- dirs: tests/components/
|
|
56
|
+
```
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Process-in-a-Box shared library
|
|
2
|
+
//
|
|
3
|
+
// All database operations as importable functions.
|
|
4
|
+
// Both the CLI (pib-db.mjs) and MCP server (pib-db-mcp-server.mjs)
|
|
5
|
+
// import from here. Schema changes update one place.
|
|
6
|
+
//
|
|
7
|
+
// Every function takes (db, params) and returns a result object.
|
|
8
|
+
// None of them do console.log — callers decide how to present output.
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function generateFid(prefix) {
|
|
18
|
+
return `${prefix}:${randomUUID().replace(/-/g, '').slice(0, 8)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function today() {
|
|
22
|
+
return new Date().toISOString().slice(0, 10);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Init — create tables from schema
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
export function init(db, { schemaPath }) {
|
|
29
|
+
const schema = readFileSync(schemaPath, 'utf-8');
|
|
30
|
+
db.exec(schema);
|
|
31
|
+
|
|
32
|
+
// Migrate existing DBs — add columns that may not exist yet
|
|
33
|
+
const migrations = [
|
|
34
|
+
{ table: 'actions', column: 'status', sql: "ALTER TABLE actions ADD COLUMN status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','in-progress','blocked','deferred','done'))" },
|
|
35
|
+
{ table: 'actions', column: 'tags', sql: "ALTER TABLE actions ADD COLUMN tags TEXT NOT NULL DEFAULT ''" },
|
|
36
|
+
];
|
|
37
|
+
for (const m of migrations) {
|
|
38
|
+
const cols = db.prepare(`PRAGMA table_info(${m.table})`).all();
|
|
39
|
+
if (!cols.some(c => c.name === m.column)) {
|
|
40
|
+
try { db.exec(m.sql); } catch { /* column may already exist */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { message: `Database initialized` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Query — run arbitrary SQL
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
export function query(db, { sql }) {
|
|
51
|
+
if (sql.trim().toUpperCase().startsWith('SELECT')) {
|
|
52
|
+
const rows = db.prepare(sql).all();
|
|
53
|
+
return { rows };
|
|
54
|
+
} else {
|
|
55
|
+
db.exec(sql);
|
|
56
|
+
return { message: 'Done.' };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Actions
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate that notes contain a ## Surface Area section with at least one
|
|
66
|
+
* - files: or - dirs: line. Returns null if valid, or an error object if not.
|
|
67
|
+
*/
|
|
68
|
+
function validateSurfaceArea(notes) {
|
|
69
|
+
if (!notes) {
|
|
70
|
+
return {
|
|
71
|
+
error: 'missing_surface_area',
|
|
72
|
+
message: 'Action notes must contain a ## Surface Area section.',
|
|
73
|
+
suggestedFormat: [
|
|
74
|
+
'## Surface Area',
|
|
75
|
+
'- files: path/to/file.js',
|
|
76
|
+
'- files: path/to/other.js',
|
|
77
|
+
'- dirs: src/components/',
|
|
78
|
+
].join('\n'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hasSection = /^## Surface Area/m.test(notes);
|
|
83
|
+
if (!hasSection) {
|
|
84
|
+
return {
|
|
85
|
+
error: 'missing_surface_area',
|
|
86
|
+
message: 'Action notes must contain a ## Surface Area section.',
|
|
87
|
+
suggestedFormat: [
|
|
88
|
+
'## Surface Area',
|
|
89
|
+
'- files: path/to/file.js',
|
|
90
|
+
'- files: path/to/other.js',
|
|
91
|
+
'- dirs: src/components/',
|
|
92
|
+
].join('\n'),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Extract everything after ## Surface Area until the next ## or end
|
|
97
|
+
const sectionMatch = notes.match(/^## Surface Area\s*\n([\s\S]*?)(?=\n## |\n*$)/m);
|
|
98
|
+
const sectionBody = sectionMatch ? sectionMatch[1] : '';
|
|
99
|
+
const hasEntry = /^- (?:files|dirs):/m.test(sectionBody);
|
|
100
|
+
if (!hasEntry) {
|
|
101
|
+
return {
|
|
102
|
+
error: 'empty_surface_area',
|
|
103
|
+
message: '## Surface Area section must contain at least one "- files:" or "- dirs:" line.',
|
|
104
|
+
suggestedFormat: [
|
|
105
|
+
'## Surface Area',
|
|
106
|
+
'- files: path/to/file.js',
|
|
107
|
+
'- dirs: src/components/',
|
|
108
|
+
].join('\n'),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null; // valid
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createAction(db, { text, area, projectFid, due, notes }) {
|
|
116
|
+
const validationError = validateSurfaceArea(notes);
|
|
117
|
+
if (validationError) {
|
|
118
|
+
return { error: validationError };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fid = generateFid('act');
|
|
122
|
+
db.prepare(`
|
|
123
|
+
INSERT INTO actions (fid, text, area, project_fid, due, notes, created)
|
|
124
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
125
|
+
`).run(fid, text, area || null, projectFid || null, due || null, notes || '', today());
|
|
126
|
+
return { fid, text, message: `Created action ${fid}: ${text}` };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function listActions(db, { status, project } = {}) {
|
|
130
|
+
const conditions = ['a.deleted_at IS NULL'];
|
|
131
|
+
const params = [];
|
|
132
|
+
|
|
133
|
+
if (status) {
|
|
134
|
+
conditions.push('a.status = ?');
|
|
135
|
+
params.push(status);
|
|
136
|
+
} else {
|
|
137
|
+
conditions.push('a.completed = 0');
|
|
138
|
+
}
|
|
139
|
+
if (project) {
|
|
140
|
+
conditions.push('a.project_fid = ?');
|
|
141
|
+
params.push(project);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rows = db.prepare(`
|
|
145
|
+
SELECT a.fid, a.text, a.area, a.due, a.flagged, a.status, a.tags, p.name as project
|
|
146
|
+
FROM actions a
|
|
147
|
+
LEFT JOIN projects p ON a.project_fid = p.fid
|
|
148
|
+
WHERE ${conditions.join(' AND ')}
|
|
149
|
+
ORDER BY
|
|
150
|
+
CASE WHEN a.due IS NOT NULL AND a.due <= date('now') THEN 0 ELSE 1 END,
|
|
151
|
+
a.due,
|
|
152
|
+
a.flagged DESC,
|
|
153
|
+
a.created DESC
|
|
154
|
+
`).all(...params);
|
|
155
|
+
return { rows };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function updateAction(db, { fid, status, text, tags, notes, due, flagged }) {
|
|
159
|
+
const sets = [];
|
|
160
|
+
const params = [];
|
|
161
|
+
|
|
162
|
+
if (status !== undefined) { sets.push('status = ?'); params.push(status); }
|
|
163
|
+
if (text !== undefined) { sets.push('text = ?'); params.push(text); }
|
|
164
|
+
if (tags !== undefined) { sets.push('tags = ?'); params.push(tags); }
|
|
165
|
+
if (notes !== undefined) { sets.push('notes = ?'); params.push(notes); }
|
|
166
|
+
if (due !== undefined) { sets.push('due = ?'); params.push(due); }
|
|
167
|
+
if (flagged !== undefined) { sets.push('flagged = ?'); params.push(flagged === 'true' || flagged === '1' || flagged === true ? 1 : 0); }
|
|
168
|
+
|
|
169
|
+
// If marking done, also set completed fields
|
|
170
|
+
if (status === 'done') {
|
|
171
|
+
sets.push('completed = 1', 'completed_at = ?');
|
|
172
|
+
params.push(new Date().toISOString());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (sets.length === 0) {
|
|
176
|
+
return { error: { message: 'No fields to update. Use status, text, tags, notes, due, or flagged.' } };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
params.push(fid);
|
|
180
|
+
db.prepare(`UPDATE actions SET ${sets.join(', ')} WHERE fid = ?`).run(...params);
|
|
181
|
+
return { fid, message: `Updated ${fid}` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function completeAction(db, { fid }) {
|
|
185
|
+
db.prepare(`
|
|
186
|
+
UPDATE actions SET completed = 1, completed_at = ?, status = 'done' WHERE fid = ?
|
|
187
|
+
`).run(new Date().toISOString(), fid);
|
|
188
|
+
return { fid, message: `Completed ${fid}` };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Projects
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
export function createProject(db, { name, area, notes, due }) {
|
|
195
|
+
const fid = generateFid('prj');
|
|
196
|
+
db.prepare(`
|
|
197
|
+
INSERT INTO projects (fid, name, area, notes, due, created)
|
|
198
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
199
|
+
`).run(fid, name, area || null, notes || '', due || null, today());
|
|
200
|
+
return { fid, name, message: `Created project ${fid}: ${name}` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function listProjects(db) {
|
|
204
|
+
const rows = db.prepare(`
|
|
205
|
+
SELECT p.fid, p.name, p.area, p.status, p.due,
|
|
206
|
+
(SELECT COUNT(*) FROM actions a WHERE a.project_fid = p.fid AND a.completed = 0 AND a.deleted_at IS NULL) as open_actions
|
|
207
|
+
FROM projects p
|
|
208
|
+
WHERE p.status = 'active' AND p.deleted_at IS NULL
|
|
209
|
+
ORDER BY p.created DESC
|
|
210
|
+
`).all();
|
|
211
|
+
return { rows };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Audit — ingest findings from a run directory
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
export function ingestFindings(db, { runDir }) {
|
|
218
|
+
const summaryPath = join(runDir, 'run-summary.json');
|
|
219
|
+
if (!existsSync(summaryPath)) {
|
|
220
|
+
return { error: { message: `No run-summary.json found in ${runDir}` } };
|
|
221
|
+
}
|
|
222
|
+
const data = JSON.parse(readFileSync(summaryPath, 'utf-8'));
|
|
223
|
+
const runId = data.meta?.runId || `run-${Date.now()}`;
|
|
224
|
+
const timestamp = data.meta?.timestamp || new Date().toISOString();
|
|
225
|
+
const dateStr = timestamp.slice(0, 10);
|
|
226
|
+
|
|
227
|
+
db.prepare(`
|
|
228
|
+
INSERT OR REPLACE INTO audit_runs (id, date, timestamp, trigger, finding_count)
|
|
229
|
+
VALUES (?, ?, ?, ?, ?)
|
|
230
|
+
`).run(runId, dateStr, timestamp, data.meta?.trigger || 'manual', data.findings?.length || 0);
|
|
231
|
+
|
|
232
|
+
const insert = db.prepare(`
|
|
233
|
+
INSERT OR REPLACE INTO audit_findings
|
|
234
|
+
(id, run_id, cabinet_member, severity, title, description, assumption,
|
|
235
|
+
evidence, question, file, line, suggested_fix, auto_fixable, type)
|
|
236
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
237
|
+
`);
|
|
238
|
+
|
|
239
|
+
let count = 0;
|
|
240
|
+
for (const f of (data.findings || [])) {
|
|
241
|
+
insert.run(
|
|
242
|
+
f.id, runId, f['cabinet-member'], f.severity, f.title,
|
|
243
|
+
f.description || null, f.assumption || null, f.evidence || null,
|
|
244
|
+
f.question || null, f.file || null, f.line || null,
|
|
245
|
+
f.suggestedFix || null, f.autoFixable ? 1 : 0, f.type || 'finding'
|
|
246
|
+
);
|
|
247
|
+
count++;
|
|
248
|
+
}
|
|
249
|
+
return { count, runId, message: `Ingested ${count} findings from ${runDir} (run: ${runId})` };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Triage
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
export function triage(db, { findingId, status, notes }) {
|
|
256
|
+
db.prepare(`
|
|
257
|
+
UPDATE audit_findings
|
|
258
|
+
SET triage_status = ?, triage_notes = ?, triaged_at = ?
|
|
259
|
+
WHERE id = ?
|
|
260
|
+
`).run(status, notes || null, new Date().toISOString(), findingId);
|
|
261
|
+
return { findingId, status, message: `Triaged ${findingId} → ${status}` };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function triageHistory(db) {
|
|
265
|
+
const rejected = db.prepare(`
|
|
266
|
+
SELECT id, cabinet_member, title FROM audit_findings
|
|
267
|
+
WHERE triage_status = 'rejected'
|
|
268
|
+
`).all();
|
|
269
|
+
|
|
270
|
+
const deferred = db.prepare(`
|
|
271
|
+
SELECT id, cabinet_member, title FROM audit_findings
|
|
272
|
+
WHERE triage_status = 'deferred'
|
|
273
|
+
`).all();
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
rejectedIds: rejected.map(r => r.id),
|
|
277
|
+
rejectedFingerprints: rejected.map(r => ({ 'cabinet-member': r.cabinet_member, title: r.title })),
|
|
278
|
+
deferredIds: deferred.map(r => r.id),
|
|
279
|
+
deferredFingerprints: deferred.map(r => ({ 'cabinet-member': r.cabinet_member, title: r.title })),
|
|
280
|
+
};
|
|
281
|
+
}
|