@contentful/experience-design-system-cli 2.12.0 → 2.12.1-dev-build-bae15c7.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/README.md +214 -357
- package/dist/package.json +1 -1
- package/dist/src/analyze/extract/astro.js +9 -0
- package/dist/src/analyze/extract/react.js +23 -0
- package/dist/src/analyze/extract/stencil.js +3 -0
- package/dist/src/analyze/extract/vue.js +6 -0
- package/dist/src/analyze/extract/web-components.js +10 -0
- package/dist/src/analyze/pre-classify.d.ts +1 -1
- package/dist/src/analyze/pre-classify.js +3 -3
- package/dist/src/analyze/select/persistence.d.ts +13 -0
- package/dist/src/analyze/select/persistence.js +38 -0
- package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
- package/dist/src/analyze/select/preview-annotations.js +62 -0
- package/dist/src/analyze/select/tui/App.js +5 -2
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
- package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
- package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
- package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
- package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
- package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
- package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
- package/dist/src/analyze/select-agent/command.d.ts +2 -0
- package/dist/src/analyze/select-agent/command.js +260 -45
- package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
- package/dist/src/analyze/select-agent/show-rationale.js +92 -0
- package/dist/src/apply/command.js +7 -3
- package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
- package/dist/src/apply/tui/ServerApplyView.js +3 -2
- package/dist/src/credentials-store.d.ts +5 -0
- package/dist/src/credentials-store.js +7 -1
- package/dist/src/generate/agent-runner.d.ts +14 -0
- package/dist/src/generate/agent-runner.js +16 -0
- package/dist/src/generate/command.d.ts +6 -0
- package/dist/src/generate/command.js +45 -10
- package/dist/src/generate/progress.d.ts +9 -0
- package/dist/src/generate/progress.js +11 -0
- package/dist/src/generate/prompt-builder.d.ts +8 -0
- package/dist/src/generate/prompt-builder.js +22 -11
- package/dist/src/import/agent-model-resolve.d.ts +23 -0
- package/dist/src/import/agent-model-resolve.js +35 -0
- package/dist/src/import/auto-filter-resolve.d.ts +12 -0
- package/dist/src/import/auto-filter-resolve.js +16 -0
- package/dist/src/import/command.js +248 -8
- package/dist/src/import/orchestrator.d.ts +2 -0
- package/dist/src/import/orchestrator.js +11 -3
- package/dist/src/import/print-prompt.d.ts +35 -0
- package/dist/src/import/print-prompt.js +31 -0
- package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
- package/dist/src/import/tui/CustomPromptBanner.js +7 -0
- package/dist/src/import/tui/WizardApp.d.ts +112 -8
- package/dist/src/import/tui/WizardApp.js +820 -333
- package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
- package/dist/src/import/tui/auto-filter-error.js +29 -0
- package/dist/src/import/tui/final-review-host.d.ts +15 -0
- package/dist/src/import/tui/final-review-host.js +20 -0
- package/dist/src/import/tui/merge-ai-decisions.d.ts +5 -0
- package/dist/src/import/tui/merge-ai-decisions.js +22 -0
- package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
- package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
- package/dist/src/import/tui/push-progress.d.ts +23 -0
- package/dist/src/import/tui/push-progress.js +14 -0
- package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
- package/dist/src/import/tui/run-print-files-helpers.js +8 -0
- package/dist/src/import/tui/run-teaser.d.ts +5 -0
- package/dist/src/import/tui/run-teaser.js +9 -0
- package/dist/src/import/tui/runLivePreview.d.ts +35 -0
- package/dist/src/import/tui/runLivePreview.js +73 -0
- package/dist/src/import/tui/runScopeGate.d.ts +22 -0
- package/dist/src/import/tui/runScopeGate.js +28 -0
- package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
- package/dist/src/import/tui/scope-gate-host.js +20 -0
- package/dist/src/import/tui/spawn-generate.d.ts +33 -0
- package/dist/src/import/tui/spawn-generate.js +53 -0
- package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
- package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
- package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
- package/dist/src/import/tui/steps/DoneStep.js +10 -4
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
- package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
- package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
- package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
- package/dist/src/import/tui/steps/PushingStep.js +40 -0
- package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
- package/dist/src/import/tui/steps/ScopeGateStep.js +250 -0
- package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
- package/dist/src/import/tui/useLivePreview.d.ts +35 -0
- package/dist/src/import/tui/useLivePreview.js +120 -0
- package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
- package/dist/src/import/tui/wizard-generate-progress.js +25 -0
- package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
- package/dist/src/import/tui/wizard-save-flow.js +20 -0
- package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
- package/dist/src/import/tui/wizard-state-transitions.js +79 -0
- package/dist/src/lib/contentful-urls.d.ts +34 -0
- package/dist/src/lib/contentful-urls.js +41 -0
- package/dist/src/program.js +2 -0
- package/dist/src/runs/export-helpers.d.ts +30 -0
- package/dist/src/runs/export-helpers.js +59 -0
- package/dist/src/runs/fingerprint.d.ts +47 -0
- package/dist/src/runs/fingerprint.js +75 -0
- package/dist/src/runs/ls-command.d.ts +12 -0
- package/dist/src/runs/ls-command.js +137 -0
- package/dist/src/runs/modify-launcher.d.ts +24 -0
- package/dist/src/runs/modify-launcher.js +36 -0
- package/dist/src/runs/path-prompt.d.ts +8 -0
- package/dist/src/runs/path-prompt.js +72 -0
- package/dist/src/runs/push-creds-prompt.d.ts +25 -0
- package/dist/src/runs/push-creds-prompt.js +39 -0
- package/dist/src/runs/push-helpers.d.ts +29 -0
- package/dist/src/runs/push-helpers.js +69 -0
- package/dist/src/runs/replay-helpers.d.ts +67 -0
- package/dist/src/runs/replay-helpers.js +148 -0
- package/dist/src/runs/resolve-run-target.d.ts +12 -0
- package/dist/src/runs/resolve-run-target.js +45 -0
- package/dist/src/runs/run-picker-mount.d.ts +46 -0
- package/dist/src/runs/run-picker-mount.js +72 -0
- package/dist/src/runs/run-picker.d.ts +17 -0
- package/dist/src/runs/run-picker.js +145 -0
- package/dist/src/runs/save-conflict.d.ts +8 -0
- package/dist/src/runs/save-conflict.js +56 -0
- package/dist/src/runs/save-path-resolver.d.ts +55 -0
- package/dist/src/runs/save-path-resolver.js +71 -0
- package/dist/src/runs/staleness.d.ts +38 -0
- package/dist/src/runs/staleness.js +140 -0
- package/dist/src/runs/store.d.ts +78 -0
- package/dist/src/runs/store.js +136 -0
- package/dist/src/session/cache-keys.d.ts +15 -0
- package/dist/src/session/cache-keys.js +38 -0
- package/dist/src/session/db.d.ts +89 -3
- package/dist/src/session/db.js +447 -104
- package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
- package/dist/src/setup/auto-filter-prompt.js +24 -0
- package/dist/src/setup/command.d.ts +7 -0
- package/dist/src/setup/command.js +58 -2
- package/dist/src/types.d.ts +6 -0
- package/package.json +2 -2
- package/skills/generate-components.md +54 -6
- package/skills/select-components.md +13 -4
package/dist/src/session/db.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { dirname, resolve } from 'node:path';
|
|
@@ -46,23 +46,28 @@ CREATE TABLE IF NOT EXISTS raw_components (
|
|
|
46
46
|
extraction_confidence INTEGER,
|
|
47
47
|
review_reasons TEXT NOT NULL DEFAULT '[]',
|
|
48
48
|
needs_review INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
source_path TEXT,
|
|
50
|
+
reject_reason TEXT,
|
|
49
51
|
PRIMARY KEY (session_id, component_id)
|
|
50
52
|
);
|
|
51
53
|
|
|
52
54
|
CREATE TABLE IF NOT EXISTS raw_props (
|
|
53
|
-
session_id
|
|
54
|
-
component_id
|
|
55
|
-
name
|
|
56
|
-
type
|
|
57
|
-
required
|
|
58
|
-
category
|
|
59
|
-
default_value
|
|
60
|
-
description
|
|
61
|
-
token_reference
|
|
62
|
-
position
|
|
63
|
-
cdf_type
|
|
64
|
-
cdf_category
|
|
65
|
-
cdf_token_kind
|
|
55
|
+
session_id TEXT NOT NULL,
|
|
56
|
+
component_id TEXT NOT NULL,
|
|
57
|
+
name TEXT NOT NULL,
|
|
58
|
+
type TEXT NOT NULL,
|
|
59
|
+
required INTEGER NOT NULL CHECK (required IN (0, 1)),
|
|
60
|
+
category TEXT CHECK (category IN ('content', 'design', 'state')),
|
|
61
|
+
default_value TEXT,
|
|
62
|
+
description TEXT,
|
|
63
|
+
token_reference TEXT,
|
|
64
|
+
position INTEGER NOT NULL,
|
|
65
|
+
cdf_type TEXT,
|
|
66
|
+
cdf_category TEXT CHECK (cdf_category IN ('content', 'design', 'state')),
|
|
67
|
+
cdf_token_kind TEXT,
|
|
68
|
+
rationale TEXT,
|
|
69
|
+
source_start_line INTEGER,
|
|
70
|
+
source_end_line INTEGER,
|
|
66
71
|
PRIMARY KEY (session_id, component_id, name),
|
|
67
72
|
FOREIGN KEY (session_id, component_id) REFERENCES raw_components(session_id, component_id) ON DELETE CASCADE
|
|
68
73
|
);
|
|
@@ -155,6 +160,21 @@ export function openPipelineDb(dbPath) {
|
|
|
155
160
|
mkdirSync(dirname(path), { recursive: true });
|
|
156
161
|
try {
|
|
157
162
|
const db = new DatabaseSync(path);
|
|
163
|
+
// Configure connection-level pragmas BEFORE running schema/migrations.
|
|
164
|
+
// - journal_mode=WAL: persistent on the DB file; readers don't block writers
|
|
165
|
+
// and vice versa. (Also set in SCHEMA, but harmless to assert per-open.)
|
|
166
|
+
// - busy_timeout: per-connection; SQLite waits for a lock instead of
|
|
167
|
+
// immediately throwing 'database is locked'. Critical when the wizard
|
|
168
|
+
// main process and select-agent subprocesses contend on writes.
|
|
169
|
+
// - synchronous=NORMAL: safe under WAL, faster than FULL.
|
|
170
|
+
// busy_timeout MUST come first. PRAGMA journal_mode = WAL needs a
|
|
171
|
+
// RESERVED lock briefly even when the DB is already WAL; if the
|
|
172
|
+
// select-agent child is mid-write at that moment, the wizard's open
|
|
173
|
+
// fails instantly without retry. Setting busy_timeout first makes
|
|
174
|
+
// SQLite wait up to 5s for the lock.
|
|
175
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
176
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
177
|
+
db.exec('PRAGMA synchronous = NORMAL');
|
|
158
178
|
db.exec(SCHEMA);
|
|
159
179
|
applyDbMigrations(db);
|
|
160
180
|
return db;
|
|
@@ -162,8 +182,12 @@ export function openPipelineDb(dbPath) {
|
|
|
162
182
|
catch (e) {
|
|
163
183
|
const msg = e instanceof Error ? e.message : String(e);
|
|
164
184
|
if (msg.includes('database is locked')) {
|
|
165
|
-
|
|
166
|
-
|
|
185
|
+
// Throw a typed-ish error rather than process.exit(1). Calling exit
|
|
186
|
+
// from inside Ink's render reconciler crashes the TUI mid-frame and
|
|
187
|
+
// emits a noisy 'setState during render' warning. The CLI entry
|
|
188
|
+
// point (src/index.ts) catches and exits cleanly for non-TUI
|
|
189
|
+
// contexts; the wizard catches and transitions to its error step.
|
|
190
|
+
throw new Error('database is locked: another CLI process may be running. Retry once it exits.');
|
|
167
191
|
}
|
|
168
192
|
throw e;
|
|
169
193
|
}
|
|
@@ -186,6 +210,84 @@ function applyDbMigrations(db) {
|
|
|
186
210
|
if (!rawCompColNames.has('needs_review')) {
|
|
187
211
|
db.exec('ALTER TABLE raw_components ADD COLUMN needs_review INTEGER NOT NULL DEFAULT 0');
|
|
188
212
|
}
|
|
213
|
+
// Feature 1: per-component source path on raw_components, rationale + per-prop
|
|
214
|
+
// source location on raw_props. All nullable, no DEFAULT — existing rows survive.
|
|
215
|
+
if (!rawCompColNames.has('source_path')) {
|
|
216
|
+
db.exec('ALTER TABLE raw_components ADD COLUMN source_path TEXT');
|
|
217
|
+
}
|
|
218
|
+
const rawPropCols = db.prepare('PRAGMA table_info(raw_props)').all();
|
|
219
|
+
const rawPropColNames = new Set(rawPropCols.map((c) => c.name));
|
|
220
|
+
if (!rawPropColNames.has('rationale')) {
|
|
221
|
+
db.exec('ALTER TABLE raw_props ADD COLUMN rationale TEXT');
|
|
222
|
+
}
|
|
223
|
+
if (!rawPropColNames.has('source_start_line')) {
|
|
224
|
+
db.exec('ALTER TABLE raw_props ADD COLUMN source_start_line INTEGER');
|
|
225
|
+
}
|
|
226
|
+
if (!rawPropColNames.has('source_end_line')) {
|
|
227
|
+
db.exec('ALTER TABLE raw_props ADD COLUMN source_end_line INTEGER');
|
|
228
|
+
}
|
|
229
|
+
// Feature 3: persist select-agent's reject reason on raw_components. Nullable,
|
|
230
|
+
// no DEFAULT — accepted components keep NULL, rejected ones store the LLM's
|
|
231
|
+
// reason (or `validation error: <codes>` for validation auto-rejections).
|
|
232
|
+
if (!rawCompColNames.has('reject_reason')) {
|
|
233
|
+
db.exec('ALTER TABLE raw_components ADD COLUMN reject_reason TEXT');
|
|
234
|
+
}
|
|
235
|
+
// Component rationale: WHY a component was classified, why these props,
|
|
236
|
+
// why these slots — surfaced via the `I` panel. All nullable, no DEFAULT
|
|
237
|
+
// so existing rows survive. Plus per-slot rationale on raw_slots.
|
|
238
|
+
if (!rawCompColNames.has('component_description_rationale')) {
|
|
239
|
+
db.exec('ALTER TABLE raw_components ADD COLUMN component_description_rationale TEXT');
|
|
240
|
+
}
|
|
241
|
+
if (!rawCompColNames.has('props_rationale')) {
|
|
242
|
+
db.exec('ALTER TABLE raw_components ADD COLUMN props_rationale TEXT');
|
|
243
|
+
}
|
|
244
|
+
if (!rawCompColNames.has('slots_rationale')) {
|
|
245
|
+
db.exec('ALTER TABLE raw_components ADD COLUMN slots_rationale TEXT');
|
|
246
|
+
}
|
|
247
|
+
const rawSlotColsForRationale = db.prepare('PRAGMA table_info(raw_slots)').all();
|
|
248
|
+
if (!rawSlotColsForRationale.some((c) => c.name === 'rationale')) {
|
|
249
|
+
db.exec('ALTER TABLE raw_slots ADD COLUMN rationale TEXT');
|
|
250
|
+
}
|
|
251
|
+
// Fine-grained cache: extract_cache holds per-file extracted components keyed
|
|
252
|
+
// on (file_hash, cli_version). cli_version is a content hash of bundled skill
|
|
253
|
+
// files so cache busts on a prompt change (not on every npm patch bump).
|
|
254
|
+
const hasExtractCache = db
|
|
255
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='extract_cache'`)
|
|
256
|
+
.all();
|
|
257
|
+
if (hasExtractCache.length === 0) {
|
|
258
|
+
db.exec(`
|
|
259
|
+
CREATE TABLE extract_cache (
|
|
260
|
+
file_path TEXT NOT NULL,
|
|
261
|
+
file_hash TEXT NOT NULL,
|
|
262
|
+
cli_version TEXT NOT NULL,
|
|
263
|
+
created_at TEXT NOT NULL,
|
|
264
|
+
updated_at TEXT NOT NULL,
|
|
265
|
+
components_json TEXT NOT NULL,
|
|
266
|
+
PRIMARY KEY (file_hash, cli_version)
|
|
267
|
+
);
|
|
268
|
+
CREATE INDEX idx_extract_cache_file ON extract_cache(file_path);
|
|
269
|
+
`);
|
|
270
|
+
}
|
|
271
|
+
// Fine-grained cache: select_cache stores accept/reject decisions per
|
|
272
|
+
// (component_hash, prompt_hash, cli_version). On hit, the wizard replays the
|
|
273
|
+
// decision without an LLM call.
|
|
274
|
+
const hasSelectCache = db
|
|
275
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='select_cache'`)
|
|
276
|
+
.all();
|
|
277
|
+
if (hasSelectCache.length === 0) {
|
|
278
|
+
db.exec(`
|
|
279
|
+
CREATE TABLE select_cache (
|
|
280
|
+
component_hash TEXT NOT NULL,
|
|
281
|
+
prompt_hash TEXT NOT NULL,
|
|
282
|
+
cli_version TEXT NOT NULL,
|
|
283
|
+
decision TEXT NOT NULL CHECK (decision IN ('accepted','rejected')),
|
|
284
|
+
reason TEXT,
|
|
285
|
+
created_at TEXT NOT NULL,
|
|
286
|
+
updated_at TEXT NOT NULL,
|
|
287
|
+
PRIMARY KEY (component_hash, prompt_hash, cli_version)
|
|
288
|
+
);
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
189
291
|
// Add generation_cache table if it doesn't exist yet.
|
|
190
292
|
const tables = db
|
|
191
293
|
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='generation_cache'`)
|
|
@@ -206,6 +308,131 @@ function applyDbMigrations(db) {
|
|
|
206
308
|
CREATE INDEX idx_generation_cache_session ON generation_cache(source_session_id);
|
|
207
309
|
`);
|
|
208
310
|
}
|
|
311
|
+
// Fine-grained cache: add prompt_hash to generation_cache so cache busts when
|
|
312
|
+
// the operator switches prompt files. Existing rows get '' (matches the
|
|
313
|
+
// historical "default bundled prompt" — note that lookups using a non-empty
|
|
314
|
+
// hash for the bundled prompt won't match, so first run after upgrade busts
|
|
315
|
+
// the cache. Documented as acceptable.
|
|
316
|
+
//
|
|
317
|
+
// The primary key also needs to expand to include prompt_hash so multiple
|
|
318
|
+
// prompt variants can coexist per (input_hash, entity_type, entity_id).
|
|
319
|
+
// SQLite doesn't allow ALTERing a PK, so when promoting the schema we rebuild
|
|
320
|
+
// the table in-place.
|
|
321
|
+
const genCacheCols = db.prepare(`PRAGMA table_info(generation_cache)`).all();
|
|
322
|
+
const hasPromptHash = genCacheCols.some((c) => c.name === 'prompt_hash');
|
|
323
|
+
const oldPkCols = genCacheCols.filter((c) => c.pk > 0).map((c) => c.name);
|
|
324
|
+
const promptHashInPk = oldPkCols.includes('prompt_hash');
|
|
325
|
+
if (!hasPromptHash) {
|
|
326
|
+
db.exec(`ALTER TABLE generation_cache ADD COLUMN prompt_hash TEXT NOT NULL DEFAULT ''`);
|
|
327
|
+
}
|
|
328
|
+
if (!promptHashInPk) {
|
|
329
|
+
db.exec('BEGIN');
|
|
330
|
+
try {
|
|
331
|
+
db.exec(`
|
|
332
|
+
CREATE TABLE generation_cache__new (
|
|
333
|
+
input_hash TEXT NOT NULL,
|
|
334
|
+
entity_type TEXT NOT NULL CHECK (entity_type IN ('component', 'token_set')),
|
|
335
|
+
entity_id TEXT NOT NULL,
|
|
336
|
+
source_session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
337
|
+
human_edited INTEGER NOT NULL DEFAULT 0 CHECK (human_edited IN (0, 1)),
|
|
338
|
+
created_at TEXT NOT NULL,
|
|
339
|
+
updated_at TEXT NOT NULL,
|
|
340
|
+
prompt_hash TEXT NOT NULL DEFAULT '',
|
|
341
|
+
PRIMARY KEY (input_hash, prompt_hash, entity_type, entity_id)
|
|
342
|
+
);
|
|
343
|
+
INSERT INTO generation_cache__new
|
|
344
|
+
(input_hash, entity_type, entity_id, source_session_id, human_edited, created_at, updated_at, prompt_hash)
|
|
345
|
+
SELECT input_hash, entity_type, entity_id, source_session_id, human_edited, created_at, updated_at, prompt_hash
|
|
346
|
+
FROM generation_cache;
|
|
347
|
+
DROP TABLE generation_cache;
|
|
348
|
+
ALTER TABLE generation_cache__new RENAME TO generation_cache;
|
|
349
|
+
CREATE INDEX IF NOT EXISTS idx_generation_cache_entity ON generation_cache(entity_type, entity_id);
|
|
350
|
+
CREATE INDEX IF NOT EXISTS idx_generation_cache_session ON generation_cache(source_session_id);
|
|
351
|
+
`);
|
|
352
|
+
db.exec('COMMIT');
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
db.exec('ROLLBACK');
|
|
356
|
+
throw e;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
export function loadComponentReviewMetadata(db, sessionId, componentName) {
|
|
361
|
+
const compRow = db
|
|
362
|
+
.prepare(`SELECT component_id, source, source_path FROM raw_components WHERE session_id = ? AND name = ?`)
|
|
363
|
+
.get(sessionId, componentName);
|
|
364
|
+
if (!compRow)
|
|
365
|
+
return null;
|
|
366
|
+
const propRows = db
|
|
367
|
+
.prepare(`SELECT name, rationale, source_start_line, source_end_line FROM raw_props WHERE session_id = ? AND component_id = ?`)
|
|
368
|
+
.all(sessionId, compRow.component_id);
|
|
369
|
+
const props = {};
|
|
370
|
+
for (const r of propRows) {
|
|
371
|
+
props[r.name] = {
|
|
372
|
+
rationale: r.rationale,
|
|
373
|
+
sourceStartLine: r.source_start_line,
|
|
374
|
+
sourceEndLine: r.source_end_line,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// raw_components.source historically stores the file path, not the file
|
|
378
|
+
// text. For the source-view panel to render real lines, prefer reading
|
|
379
|
+
// the file from disk via source_path. Fall back to whatever's in `source`
|
|
380
|
+
// (handles in-memory fixtures and tests).
|
|
381
|
+
let componentSource = compRow.source ?? null;
|
|
382
|
+
if (compRow.source_path) {
|
|
383
|
+
try {
|
|
384
|
+
componentSource = readFileSync(compRow.source_path, 'utf8');
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// File no longer exists or unreadable — leave fallback.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
sourcePath: compRow.source_path,
|
|
392
|
+
componentSource,
|
|
393
|
+
props,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Component-rationale loader: data contract for the `I` ComponentRationalePanel.
|
|
398
|
+
* Returns the component's description + the three component-level rationale
|
|
399
|
+
* strings (why-description / why-props / why-slots), plus per-prop and per-slot
|
|
400
|
+
* rationale rows for the operator-facing panel. Returns null when no component
|
|
401
|
+
* matches.
|
|
402
|
+
*/
|
|
403
|
+
export function loadComponentRationale(db, sessionId, componentName) {
|
|
404
|
+
const compRow = db
|
|
405
|
+
.prepare(`SELECT component_id, name, description, component_description_rationale, props_rationale, slots_rationale
|
|
406
|
+
FROM raw_components WHERE session_id = ? AND name = ?`)
|
|
407
|
+
.get(sessionId, componentName);
|
|
408
|
+
if (!compRow)
|
|
409
|
+
return null;
|
|
410
|
+
const propRows = db
|
|
411
|
+
.prepare(`SELECT name, cdf_category, category, description, rationale FROM raw_props
|
|
412
|
+
WHERE session_id = ? AND component_id = ? ORDER BY position`)
|
|
413
|
+
.all(sessionId, compRow.component_id);
|
|
414
|
+
const slotRows = db
|
|
415
|
+
.prepare(`SELECT name, description, rationale FROM raw_slots
|
|
416
|
+
WHERE session_id = ? AND component_id = ? ORDER BY position`)
|
|
417
|
+
.all(sessionId, compRow.component_id);
|
|
418
|
+
return {
|
|
419
|
+
name: compRow.name,
|
|
420
|
+
description: compRow.description,
|
|
421
|
+
descriptionRationale: compRow.component_description_rationale,
|
|
422
|
+
propsRationale: compRow.props_rationale,
|
|
423
|
+
slotsRationale: compRow.slots_rationale,
|
|
424
|
+
props: propRows.map((p) => ({
|
|
425
|
+
name: p.name,
|
|
426
|
+
category: p.cdf_category ?? p.category ?? null,
|
|
427
|
+
description: p.description,
|
|
428
|
+
rationale: p.rationale,
|
|
429
|
+
})),
|
|
430
|
+
slots: slotRows.map((s) => ({
|
|
431
|
+
name: s.name,
|
|
432
|
+
description: s.description,
|
|
433
|
+
rationale: s.rationale,
|
|
434
|
+
})),
|
|
435
|
+
};
|
|
209
436
|
}
|
|
210
437
|
export function applyToolCalls(db, sessionId, componentId, componentName, calls, incomingWarnings) {
|
|
211
438
|
const now = new Date().toISOString();
|
|
@@ -213,9 +440,9 @@ export function applyToolCalls(db, sessionId, componentId, componentName, calls,
|
|
|
213
440
|
let classified = 0;
|
|
214
441
|
let excluded = 0;
|
|
215
442
|
let slots = 0;
|
|
216
|
-
const updateProp = db.prepare(`UPDATE raw_props SET cdf_type = ?, cdf_category = ?, cdf_token_kind = ?, required = ?, description = ?
|
|
443
|
+
const updateProp = db.prepare(`UPDATE raw_props SET cdf_type = ?, cdf_category = ?, cdf_token_kind = ?, required = ?, description = ?, rationale = ?
|
|
217
444
|
WHERE session_id = ? AND component_id = ? AND name = ?`);
|
|
218
|
-
const clearProp = db.prepare(`UPDATE raw_props SET cdf_type = 'excluded', cdf_category = NULL, cdf_token_kind = NULL
|
|
445
|
+
const clearProp = db.prepare(`UPDATE raw_props SET cdf_type = 'excluded', cdf_category = NULL, cdf_token_kind = NULL, rationale = ?
|
|
219
446
|
WHERE session_id = ? AND component_id = ? AND name = ?`);
|
|
220
447
|
const deleteAllowedValues = db.prepare(`DELETE FROM raw_prop_allowed_values WHERE session_id = ? AND component_id = ? AND prop_name = ?`);
|
|
221
448
|
const insertAllowedValue = db.prepare(`INSERT OR IGNORE INTO raw_prop_allowed_values (session_id, component_id, prop_name, value, position)
|
|
@@ -231,9 +458,22 @@ export function applyToolCalls(db, sessionId, componentId, componentName, calls,
|
|
|
231
458
|
if (call.description !== undefined) {
|
|
232
459
|
db.prepare('UPDATE raw_components SET description = ? WHERE session_id = ? AND component_id = ?').run(call.description, sessionId, componentId);
|
|
233
460
|
}
|
|
461
|
+
// Component-level rationale: sparse-update each field only when present
|
|
462
|
+
// so missing keys don't overwrite previously-stored values.
|
|
463
|
+
if (call.rationale) {
|
|
464
|
+
if (call.rationale.description !== undefined) {
|
|
465
|
+
db.prepare('UPDATE raw_components SET component_description_rationale = ? WHERE session_id = ? AND component_id = ?').run(call.rationale.description, sessionId, componentId);
|
|
466
|
+
}
|
|
467
|
+
if (call.rationale.props !== undefined) {
|
|
468
|
+
db.prepare('UPDATE raw_components SET props_rationale = ? WHERE session_id = ? AND component_id = ?').run(call.rationale.props, sessionId, componentId);
|
|
469
|
+
}
|
|
470
|
+
if (call.rationale.slots !== undefined) {
|
|
471
|
+
db.prepare('UPDATE raw_components SET slots_rationale = ? WHERE session_id = ? AND component_id = ?').run(call.rationale.slots, sessionId, componentId);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
234
474
|
}
|
|
235
475
|
else if (call.tool === 'classify_prop') {
|
|
236
|
-
const changes = updateProp.run(call.cdf_type, call.cdf_category, call.token_kind ?? null, call.required !== undefined ? (call.required ? 1 : 0) : 0, call.description ?? null, sessionId, componentId, call.prop);
|
|
476
|
+
const changes = updateProp.run(call.cdf_type, call.cdf_category, call.token_kind ?? null, call.required !== undefined ? (call.required ? 1 : 0) : 0, call.description ?? null, call.reason ?? null, sessionId, componentId, call.prop);
|
|
237
477
|
if (changes.changes === 0) {
|
|
238
478
|
warnings.push(`${componentName}: classify_prop '${call.prop}' — prop not found, skipped`);
|
|
239
479
|
continue;
|
|
@@ -249,7 +489,7 @@ export function applyToolCalls(db, sessionId, componentId, componentName, calls,
|
|
|
249
489
|
classified++;
|
|
250
490
|
}
|
|
251
491
|
else if (call.tool === 'exclude_prop') {
|
|
252
|
-
clearProp.run(sessionId, componentId, call.prop);
|
|
492
|
+
clearProp.run(call.reason || null, sessionId, componentId, call.prop);
|
|
253
493
|
excluded++;
|
|
254
494
|
}
|
|
255
495
|
else if (call.tool === 'classify_slot') {
|
|
@@ -259,6 +499,11 @@ export function applyToolCalls(db, sessionId, componentId, componentName, calls,
|
|
|
259
499
|
warnings.push(`${componentName}: classify_slot '${call.slot}' — slot not found, skipped`);
|
|
260
500
|
continue;
|
|
261
501
|
}
|
|
502
|
+
// Per-slot rationale: sparse-update only when present so omitting it
|
|
503
|
+
// doesn't blank existing values.
|
|
504
|
+
if (call.rationale !== undefined) {
|
|
505
|
+
db.prepare('UPDATE raw_slots SET rationale = ? WHERE session_id = ? AND component_id = ? AND name = ?').run(call.rationale, sessionId, componentId, call.slot);
|
|
506
|
+
}
|
|
262
507
|
if (call.allowed_components !== undefined) {
|
|
263
508
|
deleteAllowedComponents.run(sessionId, componentId, call.slot);
|
|
264
509
|
call.allowed_components.forEach((ac, i) => insertAllowedComponent.run(sessionId, componentId, call.slot, ac, i));
|
|
@@ -276,15 +521,13 @@ export function applyToolCalls(db, sessionId, componentId, componentName, calls,
|
|
|
276
521
|
}
|
|
277
522
|
return { classified, excluded, slots, warnings };
|
|
278
523
|
}
|
|
279
|
-
export function getOrCreateSession(db, sessionFlag, sessionName,
|
|
524
|
+
export function getOrCreateSession(db, sessionFlag, sessionName,
|
|
525
|
+
// `_hints` is retained for call-site compatibility but no longer drives an
|
|
526
|
+
// implicit auto-match by inputPath/outDir — operators who want to resume must
|
|
527
|
+
// pass an explicit --session <id>.
|
|
528
|
+
_hints) {
|
|
280
529
|
const now = new Date().toISOString();
|
|
281
530
|
if (sessionFlag === 'new' || sessionFlag === undefined) {
|
|
282
|
-
if (sessionFlag !== 'new') {
|
|
283
|
-
const match = findMatchingSession(db, hints);
|
|
284
|
-
if (match) {
|
|
285
|
-
return { sessionId: match, isNew: false, isResumed: true };
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
531
|
const id = generateSessionId();
|
|
289
532
|
db.prepare('INSERT INTO sessions (id, name, created_at, updated_at) VALUES (?, ?, ?, ?)').run(id, sessionName ?? null, now, now);
|
|
290
533
|
return { sessionId: id, isNew: true, isResumed: false };
|
|
@@ -295,48 +538,6 @@ export function getOrCreateSession(db, sessionFlag, sessionName, hints) {
|
|
|
295
538
|
}
|
|
296
539
|
return { sessionId: sessionFlag, isNew: false, isResumed: false };
|
|
297
540
|
}
|
|
298
|
-
function findMatchingSession(db, hints) {
|
|
299
|
-
if (!hints.inputPath && !hints.outDir)
|
|
300
|
-
return null;
|
|
301
|
-
const rows = db
|
|
302
|
-
.prepare(`SELECT s.id, st.inputs, st.outputs, st.status
|
|
303
|
-
FROM sessions s
|
|
304
|
-
JOIN steps st ON st.session_id = s.id
|
|
305
|
-
WHERE st.command = ?
|
|
306
|
-
AND st.status IN ('pending', 'interrupted')
|
|
307
|
-
ORDER BY st.started_at DESC
|
|
308
|
-
LIMIT 20`)
|
|
309
|
-
.all(hints.command);
|
|
310
|
-
for (const row of rows) {
|
|
311
|
-
let inputs = {};
|
|
312
|
-
try {
|
|
313
|
-
inputs = JSON.parse(row.inputs);
|
|
314
|
-
}
|
|
315
|
-
catch {
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (hints.inputPath) {
|
|
319
|
-
const inputValues = Object.values(inputs);
|
|
320
|
-
if (inputValues.some((v) => v === hints.inputPath)) {
|
|
321
|
-
return row.id;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (hints.outDir) {
|
|
325
|
-
let outputs = {};
|
|
326
|
-
try {
|
|
327
|
-
outputs = JSON.parse(row.outputs);
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
// ignore
|
|
331
|
-
}
|
|
332
|
-
const allValues = [...Object.values(inputs), ...Object.values(outputs)];
|
|
333
|
-
if (allValues.some((v) => v === hints.outDir)) {
|
|
334
|
-
return row.id;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
541
|
export function createStep(db, sessionId, command, inputs) {
|
|
341
542
|
const now = new Date().toISOString();
|
|
342
543
|
db.exec('BEGIN');
|
|
@@ -374,11 +575,11 @@ export function updateStep(db, stepId, status, outputs, error) {
|
|
|
374
575
|
}
|
|
375
576
|
export function storeRawComponents(db, sessionId, components, options) {
|
|
376
577
|
const now = new Date().toISOString();
|
|
377
|
-
const insertComp = db.prepare(`INSERT INTO raw_components (session_id, component_id, name, source, framework, extracted_at, extraction_confidence, review_reasons, needs_review)
|
|
378
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
379
|
-
const insertProp = db.prepare(`INSERT INTO raw_props
|
|
380
|
-
(session_id, component_id, name, type, required, category, default_value, description, token_reference, position)
|
|
578
|
+
const insertComp = db.prepare(`INSERT INTO raw_components (session_id, component_id, name, source, framework, extracted_at, extraction_confidence, review_reasons, needs_review, source_path)
|
|
381
579
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
580
|
+
const insertProp = db.prepare(`INSERT INTO raw_props
|
|
581
|
+
(session_id, component_id, name, type, required, category, default_value, description, token_reference, position, source_start_line, source_end_line)
|
|
582
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
382
583
|
const insertAllowedValue = db.prepare(`INSERT INTO raw_prop_allowed_values (session_id, component_id, prop_name, position, value)
|
|
383
584
|
VALUES (?, ?, ?, ?, ?)`);
|
|
384
585
|
const insertSlot = db.prepare(`INSERT INTO raw_slots (session_id, component_id, name, is_default, description, position)
|
|
@@ -409,10 +610,10 @@ export function storeRawComponents(db, sessionId, components, options) {
|
|
|
409
610
|
db.prepare('DELETE FROM raw_components WHERE session_id = ?').run(sessionId);
|
|
410
611
|
for (const comp of components) {
|
|
411
612
|
const componentId = deriveComponentId(comp.name, comp.source);
|
|
412
|
-
insertComp.run(sessionId, componentId, comp.name, comp.source, comp.framework, now, comp.extractionConfidence ?? null, JSON.stringify(comp.reviewReasons ?? []), comp.needsReview ? 1 : 0);
|
|
613
|
+
insertComp.run(sessionId, componentId, comp.name, comp.source, comp.framework, now, comp.extractionConfidence ?? null, JSON.stringify(comp.reviewReasons ?? []), comp.needsReview ? 1 : 0, comp.sourcePath ?? null);
|
|
413
614
|
for (let i = 0; i < comp.props.length; i++) {
|
|
414
615
|
const prop = comp.props[i];
|
|
415
|
-
insertProp.run(sessionId, componentId, prop.name, prop.type, prop.required ? 1 : 0, prop.category ?? null, prop.defaultValue ?? null, prop.description ?? null, prop.tokenReference ?? null, i);
|
|
616
|
+
insertProp.run(sessionId, componentId, prop.name, prop.type, prop.required ? 1 : 0, prop.category ?? null, prop.defaultValue ?? null, prop.description ?? null, prop.tokenReference ?? null, i, prop.sourceStartLine ?? null, prop.sourceEndLine ?? null);
|
|
416
617
|
if (prop.allowedValues) {
|
|
417
618
|
prop.allowedValues.forEach((v, j) => insertAllowedValue.run(sessionId, componentId, prop.name, j, v));
|
|
418
619
|
}
|
|
@@ -489,13 +690,14 @@ export function storeRawComponents(db, sessionId, components, options) {
|
|
|
489
690
|
}
|
|
490
691
|
export function loadRawComponents(db, sessionId, allowedNames) {
|
|
491
692
|
const all = db
|
|
492
|
-
.prepare('SELECT component_id, name, source, framework, extraction_confidence, review_reasons, needs_review FROM raw_components WHERE session_id = ? ORDER BY rowid')
|
|
693
|
+
.prepare('SELECT component_id, name, source, framework, extraction_confidence, review_reasons, needs_review, source_path FROM raw_components WHERE session_id = ? ORDER BY rowid')
|
|
493
694
|
.all(sessionId);
|
|
494
695
|
const components = allowedNames ? all.filter((c) => allowedNames.has(c.name)) : all;
|
|
495
696
|
if (components.length === 0)
|
|
496
697
|
return [];
|
|
497
698
|
const props = db
|
|
498
|
-
.prepare(`SELECT component_id, name, type, required, category, default_value, description, token_reference, position
|
|
699
|
+
.prepare(`SELECT component_id, name, type, required, category, default_value, description, token_reference, position,
|
|
700
|
+
rationale, source_start_line, source_end_line
|
|
499
701
|
FROM raw_props WHERE session_id = ? ORDER BY component_id, position`)
|
|
500
702
|
.all(sessionId);
|
|
501
703
|
const allowedValues = db
|
|
@@ -530,6 +732,7 @@ export function loadRawComponents(db, sessionId, allowedNames) {
|
|
|
530
732
|
}
|
|
531
733
|
})(),
|
|
532
734
|
needsReview: Boolean(c.needs_review),
|
|
735
|
+
sourcePath: c.source_path ?? undefined,
|
|
533
736
|
props: (propsByComponent.get(c.component_id) ?? []).map((p) => {
|
|
534
737
|
const av = allowedValuesByProp.get(`${c.component_id}::${p.name}`);
|
|
535
738
|
const prop = {
|
|
@@ -547,6 +750,10 @@ export function loadRawComponents(db, sessionId, allowedNames) {
|
|
|
547
750
|
prop.tokenReference = p.token_reference;
|
|
548
751
|
if (av && av.length > 0)
|
|
549
752
|
prop.allowedValues = av.map((v) => v.value);
|
|
753
|
+
if (p.source_start_line !== null)
|
|
754
|
+
prop.sourceStartLine = p.source_start_line;
|
|
755
|
+
if (p.source_end_line !== null)
|
|
756
|
+
prop.sourceEndLine = p.source_end_line;
|
|
550
757
|
return prop;
|
|
551
758
|
}),
|
|
552
759
|
slots: (slotsByComponent.get(c.component_id) ?? []).map((s) => {
|
|
@@ -706,13 +913,16 @@ export function loadCDFComponents(db, sessionId) {
|
|
|
706
913
|
const allowedValuesByProp = groupBy(allowedValues, (av) => `${av.component_id}::${av.prop_name}`);
|
|
707
914
|
const slotsByComponent = groupBy(slots, (s) => s.component_id);
|
|
708
915
|
const allowedComponentsBySlot = groupBy(allowedComponents, (ac) => `${ac.component_id}::${ac.slot_name}`);
|
|
709
|
-
return components
|
|
710
|
-
.map(({ component_id, name, description }) => {
|
|
916
|
+
return components.map(({ component_id, name, description }) => {
|
|
711
917
|
const compProps = propsByComponent.get(component_id) ?? [];
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
|
|
715
|
-
|
|
918
|
+
// NOTE: Components with zero CDF-classified props are intentionally
|
|
919
|
+
// returned with an empty `$properties` object. Filtering them out here
|
|
920
|
+
// silently dropped them from the wizard's final-review (INTEG-4257),
|
|
921
|
+
// so the user couldn't see that the LLM had failed to classify anything
|
|
922
|
+
// and couldn't reject the empty component. Surface them instead — the
|
|
923
|
+
// wizard tags them with a ⚠ badge and an in-panel banner so the user
|
|
924
|
+
// can manually add props in FieldEditor or reject the component.
|
|
925
|
+
// Downstream consumers (buildManifest, push) tolerate empty $properties.
|
|
716
926
|
const $properties = {};
|
|
717
927
|
for (const p of compProps) {
|
|
718
928
|
// Hallucination insurance: drop any prop whose name didn't survive trim.
|
|
@@ -768,8 +978,42 @@ export function loadCDFComponents(db, sessionId) {
|
|
|
768
978
|
if (Object.keys($slots).length > 0)
|
|
769
979
|
entry.$slots = $slots;
|
|
770
980
|
return { key: name, entry };
|
|
771
|
-
})
|
|
772
|
-
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
export function loadScopeComponents(db, sessionId) {
|
|
984
|
+
// Loads extract-stage components: those still pending operator review (`extracted`)
|
|
985
|
+
// plus those the Feature 3 auto-filter has already classified (`accepted` /
|
|
986
|
+
// `rejected`). Excludes `generated` (already past scope-gate) so re-runs of the
|
|
987
|
+
// wizard don't show already-confirmed components.
|
|
988
|
+
const rows = db
|
|
989
|
+
.prepare(`SELECT name, component_id, status, reject_reason FROM raw_components
|
|
990
|
+
WHERE session_id = ? AND status IN ('extracted', 'accepted', 'rejected')
|
|
991
|
+
ORDER BY name`)
|
|
992
|
+
.all(sessionId);
|
|
993
|
+
return rows.map((r) => ({
|
|
994
|
+
name: r.name,
|
|
995
|
+
componentId: r.component_id,
|
|
996
|
+
aiDecision: r.status === 'accepted' ? 'accepted' : r.status === 'rejected' ? 'rejected' : null,
|
|
997
|
+
aiReason: r.reject_reason,
|
|
998
|
+
}));
|
|
999
|
+
}
|
|
1000
|
+
export function applyScopeDecisions(db, sessionId, decisions) {
|
|
1001
|
+
const now = new Date().toISOString();
|
|
1002
|
+
const acceptedSet = new Set(decisions.accepted);
|
|
1003
|
+
const accepted = [...acceptedSet];
|
|
1004
|
+
// Conflict precedence: accepted wins over rejected.
|
|
1005
|
+
const rejected = [...new Set(decisions.rejected)].filter((n) => !acceptedSet.has(n));
|
|
1006
|
+
// Rejected first so accepted writes can flip back to 'generated' afterwards
|
|
1007
|
+
// if a name appears in both lists.
|
|
1008
|
+
if (rejected.length > 0) {
|
|
1009
|
+
const placeholders = rejected.map(() => '?').join(',');
|
|
1010
|
+
db.prepare(`UPDATE raw_components SET status = 'rejected' WHERE session_id = ? AND name IN (${placeholders})`).run(sessionId, ...rejected);
|
|
1011
|
+
}
|
|
1012
|
+
if (accepted.length > 0) {
|
|
1013
|
+
const placeholders = accepted.map(() => '?').join(',');
|
|
1014
|
+
db.prepare(`UPDATE raw_components SET status = 'generated' WHERE session_id = ? AND name IN (${placeholders})`).run(sessionId, ...accepted);
|
|
1015
|
+
}
|
|
1016
|
+
db.prepare('UPDATE sessions SET updated_at = ? WHERE id = ?').run(now, sessionId);
|
|
773
1017
|
}
|
|
774
1018
|
export function storeDTCGTokens(db, sessionId, groups, tokens) {
|
|
775
1019
|
const now = new Date().toISOString();
|
|
@@ -1053,37 +1297,36 @@ function mapServerTypeToCDFType(serverType) {
|
|
|
1053
1297
|
}
|
|
1054
1298
|
}
|
|
1055
1299
|
export function computeComponentInputHash(component) {
|
|
1300
|
+
// Narrow the hashed payload to extractor-only fields. Any field that
|
|
1301
|
+
// `applyToolCalls` may overwrite (prop description/required/default/allowed
|
|
1302
|
+
// values, slot description/allowed components) is excluded so that the cache
|
|
1303
|
+
// key does not depend on its own output — otherwise re-reading raw props
|
|
1304
|
+
// after a generation pass would drift the hash even when the underlying
|
|
1305
|
+
// source has not changed.
|
|
1056
1306
|
const payload = {
|
|
1057
1307
|
framework: component.framework,
|
|
1058
1308
|
name: component.name,
|
|
1309
|
+
source: component.source,
|
|
1059
1310
|
props: component.props.map((p) => ({
|
|
1060
|
-
allowedValues: p.allowedValues ?? [],
|
|
1061
|
-
defaultValue: p.defaultValue ?? null,
|
|
1062
|
-
description: p.description ?? null,
|
|
1063
1311
|
name: p.name,
|
|
1064
|
-
required: p.required,
|
|
1065
|
-
tokenReference: p.tokenReference ?? null,
|
|
1066
1312
|
type: p.type,
|
|
1067
1313
|
})),
|
|
1068
1314
|
slots: component.slots.map((s) => ({
|
|
1069
|
-
allowedComponents: s.allowedComponents ?? [],
|
|
1070
|
-
description: s.description ?? null,
|
|
1071
|
-
isDefault: s.isDefault,
|
|
1072
1315
|
name: s.name,
|
|
1316
|
+
isDefault: s.isDefault,
|
|
1073
1317
|
})),
|
|
1074
|
-
source: component.source,
|
|
1075
1318
|
};
|
|
1076
1319
|
return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
1077
1320
|
}
|
|
1078
1321
|
export function computeTokenInputHash(rawTokenContent) {
|
|
1079
1322
|
return createHash('sha256').update(rawTokenContent.trim()).digest('hex');
|
|
1080
1323
|
}
|
|
1081
|
-
export function lookupCache(db, inputHash, entityType, entityId) {
|
|
1324
|
+
export function lookupCache(db, inputHash, entityType, entityId, promptHash = '') {
|
|
1082
1325
|
const row = db
|
|
1083
|
-
.prepare(`SELECT input_hash, entity_type, entity_id, source_session_id, human_edited, created_at, updated_at
|
|
1326
|
+
.prepare(`SELECT input_hash, entity_type, entity_id, source_session_id, human_edited, created_at, updated_at, prompt_hash
|
|
1084
1327
|
FROM generation_cache
|
|
1085
|
-
WHERE input_hash = ? AND entity_type = ? AND entity_id = ?`)
|
|
1086
|
-
.get(inputHash, entityType, entityId);
|
|
1328
|
+
WHERE input_hash = ? AND entity_type = ? AND entity_id = ? AND prompt_hash = ?`)
|
|
1329
|
+
.get(inputHash, entityType, entityId, promptHash);
|
|
1087
1330
|
if (!row)
|
|
1088
1331
|
return null;
|
|
1089
1332
|
return {
|
|
@@ -1092,13 +1335,14 @@ export function lookupCache(db, inputHash, entityType, entityId) {
|
|
|
1092
1335
|
entityId: row.entity_id,
|
|
1093
1336
|
sourceSessionId: row.source_session_id,
|
|
1094
1337
|
humanEdited: row.human_edited === 1,
|
|
1338
|
+
promptHash: row.prompt_hash,
|
|
1095
1339
|
createdAt: row.created_at,
|
|
1096
1340
|
updatedAt: row.updated_at,
|
|
1097
1341
|
};
|
|
1098
1342
|
}
|
|
1099
1343
|
export function lookupCacheByEntity(db, entityType, entityId) {
|
|
1100
1344
|
const row = db
|
|
1101
|
-
.prepare(`SELECT input_hash, entity_type, entity_id, source_session_id, human_edited, created_at, updated_at
|
|
1345
|
+
.prepare(`SELECT input_hash, entity_type, entity_id, source_session_id, human_edited, created_at, updated_at, prompt_hash
|
|
1102
1346
|
FROM generation_cache
|
|
1103
1347
|
WHERE entity_type = ? AND entity_id = ?
|
|
1104
1348
|
ORDER BY updated_at DESC LIMIT 1`)
|
|
@@ -1111,18 +1355,19 @@ export function lookupCacheByEntity(db, entityType, entityId) {
|
|
|
1111
1355
|
entityId: row.entity_id,
|
|
1112
1356
|
sourceSessionId: row.source_session_id,
|
|
1113
1357
|
humanEdited: row.human_edited === 1,
|
|
1358
|
+
promptHash: row.prompt_hash,
|
|
1114
1359
|
createdAt: row.created_at,
|
|
1115
1360
|
updatedAt: row.updated_at,
|
|
1116
1361
|
};
|
|
1117
1362
|
}
|
|
1118
|
-
export function storeCache(db, inputHash, entityType, entityId, sourceSessionId, humanEdited) {
|
|
1363
|
+
export function storeCache(db, inputHash, entityType, entityId, sourceSessionId, humanEdited, promptHash = '') {
|
|
1119
1364
|
const now = new Date().toISOString();
|
|
1120
|
-
db.prepare(`INSERT INTO generation_cache (input_hash, entity_type, entity_id, source_session_id, human_edited, created_at, updated_at)
|
|
1121
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1122
|
-
ON CONFLICT(input_hash, entity_type, entity_id) DO UPDATE SET
|
|
1365
|
+
db.prepare(`INSERT INTO generation_cache (input_hash, entity_type, entity_id, source_session_id, human_edited, created_at, updated_at, prompt_hash)
|
|
1366
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1367
|
+
ON CONFLICT(input_hash, prompt_hash, entity_type, entity_id) DO UPDATE SET
|
|
1123
1368
|
source_session_id = excluded.source_session_id,
|
|
1124
1369
|
human_edited = CASE WHEN generation_cache.human_edited = 1 THEN 1 ELSE excluded.human_edited END,
|
|
1125
|
-
updated_at = excluded.updated_at`).run(inputHash, entityType, entityId, sourceSessionId, humanEdited ? 1 : 0, now, now);
|
|
1370
|
+
updated_at = excluded.updated_at`).run(inputHash, entityType, entityId, sourceSessionId, humanEdited ? 1 : 0, now, now, promptHash);
|
|
1126
1371
|
}
|
|
1127
1372
|
export function storeScannedFiles(db, sessionId, filePaths) {
|
|
1128
1373
|
db.exec('BEGIN');
|
|
@@ -1213,3 +1458,101 @@ export function copyTokensFromCache(db, sourceSessionId, targetSessionId) {
|
|
|
1213
1458
|
throw e;
|
|
1214
1459
|
}
|
|
1215
1460
|
}
|
|
1461
|
+
// ---------------------------------------------------------------------------
|
|
1462
|
+
// Fine-grained cache: extract_cache helpers
|
|
1463
|
+
// ---------------------------------------------------------------------------
|
|
1464
|
+
let _cliCacheVersionCache = null;
|
|
1465
|
+
/**
|
|
1466
|
+
* Returns the CLI cache version used to namespace cache rows. To avoid busting
|
|
1467
|
+
* the cache on every npm patch bump, this hashes the content of the bundled
|
|
1468
|
+
* skill prompts. When the prompts change, cache invalidates; when only library
|
|
1469
|
+
* code or test fixtures change, the cache stays warm.
|
|
1470
|
+
*
|
|
1471
|
+
* Memoized per-process. Falls back to package version on any I/O error.
|
|
1472
|
+
*/
|
|
1473
|
+
export async function getCliCacheVersion() {
|
|
1474
|
+
if (_cliCacheVersionCache)
|
|
1475
|
+
return _cliCacheVersionCache;
|
|
1476
|
+
try {
|
|
1477
|
+
const { hashContent } = await import('./cache-keys.js');
|
|
1478
|
+
const { resolveSkillPath } = await import('../generate/prompt-builder.js');
|
|
1479
|
+
const skills = ['components', 'tokens', 'select'];
|
|
1480
|
+
const parts = [];
|
|
1481
|
+
for (const s of skills) {
|
|
1482
|
+
try {
|
|
1483
|
+
const p = resolveSkillPath(s);
|
|
1484
|
+
parts.push(readFileSync(p, 'utf8'));
|
|
1485
|
+
}
|
|
1486
|
+
catch {
|
|
1487
|
+
parts.push(`<missing:${s}>`);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
_cliCacheVersionCache = hashContent(parts.join('\n---\n'));
|
|
1491
|
+
return _cliCacheVersionCache;
|
|
1492
|
+
}
|
|
1493
|
+
catch {
|
|
1494
|
+
_cliCacheVersionCache = 'fallback';
|
|
1495
|
+
return _cliCacheVersionCache;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
export function storeExtractCache(db, filePath, fileHash, cliVersion, components) {
|
|
1499
|
+
const now = new Date().toISOString();
|
|
1500
|
+
const componentsJson = JSON.stringify(components);
|
|
1501
|
+
db.prepare(`INSERT INTO extract_cache (file_path, file_hash, cli_version, created_at, updated_at, components_json)
|
|
1502
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1503
|
+
ON CONFLICT(file_hash, cli_version) DO UPDATE SET
|
|
1504
|
+
file_path = excluded.file_path,
|
|
1505
|
+
updated_at = excluded.updated_at,
|
|
1506
|
+
components_json = excluded.components_json`).run(filePath, fileHash, cliVersion, now, now, componentsJson);
|
|
1507
|
+
}
|
|
1508
|
+
export function lookupExtractCache(db, fileHash, cliVersion) {
|
|
1509
|
+
const row = db
|
|
1510
|
+
.prepare(`SELECT file_path, file_hash, cli_version, created_at, updated_at, components_json
|
|
1511
|
+
FROM extract_cache
|
|
1512
|
+
WHERE file_hash = ? AND cli_version = ?`)
|
|
1513
|
+
.get(fileHash, cliVersion);
|
|
1514
|
+
if (!row)
|
|
1515
|
+
return null;
|
|
1516
|
+
let components = [];
|
|
1517
|
+
try {
|
|
1518
|
+
components = JSON.parse(row.components_json);
|
|
1519
|
+
}
|
|
1520
|
+
catch {
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
return {
|
|
1524
|
+
filePath: row.file_path,
|
|
1525
|
+
fileHash: row.file_hash,
|
|
1526
|
+
cliVersion: row.cli_version,
|
|
1527
|
+
createdAt: row.created_at,
|
|
1528
|
+
updatedAt: row.updated_at,
|
|
1529
|
+
components,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
export function storeSelectCache(db, componentHash, promptHash, cliVersion, decision, reason) {
|
|
1533
|
+
const now = new Date().toISOString();
|
|
1534
|
+
db.prepare(`INSERT INTO select_cache (component_hash, prompt_hash, cli_version, decision, reason, created_at, updated_at)
|
|
1535
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1536
|
+
ON CONFLICT(component_hash, prompt_hash, cli_version) DO UPDATE SET
|
|
1537
|
+
decision = excluded.decision,
|
|
1538
|
+
reason = excluded.reason,
|
|
1539
|
+
updated_at = excluded.updated_at`).run(componentHash, promptHash, cliVersion, decision, reason, now, now);
|
|
1540
|
+
}
|
|
1541
|
+
export function lookupSelectCache(db, componentHash, promptHash, cliVersion) {
|
|
1542
|
+
const row = db
|
|
1543
|
+
.prepare(`SELECT component_hash, prompt_hash, cli_version, decision, reason, created_at, updated_at
|
|
1544
|
+
FROM select_cache
|
|
1545
|
+
WHERE component_hash = ? AND prompt_hash = ? AND cli_version = ?`)
|
|
1546
|
+
.get(componentHash, promptHash, cliVersion);
|
|
1547
|
+
if (!row)
|
|
1548
|
+
return null;
|
|
1549
|
+
return {
|
|
1550
|
+
componentHash: row.component_hash,
|
|
1551
|
+
promptHash: row.prompt_hash,
|
|
1552
|
+
cliVersion: row.cli_version,
|
|
1553
|
+
decision: row.decision,
|
|
1554
|
+
reason: row.reason,
|
|
1555
|
+
createdAt: row.created_at,
|
|
1556
|
+
updatedAt: row.updated_at,
|
|
1557
|
+
};
|
|
1558
|
+
}
|