mustflow 2.22.4 → 2.22.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +17 -75
  2. package/dist/cli/commands/classify.js +2 -0
  3. package/dist/cli/commands/contract-lint.js +2 -2
  4. package/dist/cli/commands/dashboard.js +23 -75
  5. package/dist/cli/commands/help.js +8 -9
  6. package/dist/cli/commands/impact.js +2 -3
  7. package/dist/cli/commands/init.js +61 -5
  8. package/dist/cli/commands/run/receipt.js +1 -0
  9. package/dist/cli/commands/run.js +14 -1
  10. package/dist/cli/commands/update.js +2 -2
  11. package/dist/cli/commands/verify/evidence-input.js +269 -0
  12. package/dist/cli/commands/verify/input.js +212 -0
  13. package/dist/cli/commands/verify.js +23 -482
  14. package/dist/cli/commands/version-sources.js +2 -3
  15. package/dist/cli/i18n/en.js +5 -0
  16. package/dist/cli/i18n/es.js +5 -0
  17. package/dist/cli/i18n/fr.js +5 -0
  18. package/dist/cli/i18n/hi.js +5 -0
  19. package/dist/cli/i18n/ko.js +5 -0
  20. package/dist/cli/i18n/zh.js +5 -0
  21. package/dist/cli/lib/agent-context.js +6 -11
  22. package/dist/cli/lib/dashboard-export.js +2 -0
  23. package/dist/cli/lib/dashboard-mutations.js +79 -0
  24. package/dist/cli/lib/local-index/command-effect-index.js +25 -0
  25. package/dist/cli/lib/local-index/hashing.js +7 -0
  26. package/dist/cli/lib/local-index/index.js +127 -823
  27. package/dist/cli/lib/local-index/source-index.js +137 -0
  28. package/dist/cli/lib/local-index/verification-evidence.js +451 -0
  29. package/dist/cli/lib/local-index/workflow-documents.js +204 -0
  30. package/dist/cli/lib/mustflow-read.js +41 -0
  31. package/dist/cli/lib/project-root.js +1 -2
  32. package/dist/cli/lib/repo-map.js +65 -16
  33. package/dist/cli/lib/run-root-trust.js +27 -0
  34. package/dist/cli/lib/templates.js +124 -8
  35. package/dist/cli/lib/toml.js +6 -1
  36. package/dist/cli/lib/validation/constants.js +2 -0
  37. package/dist/cli/lib/validation/index.js +291 -22
  38. package/dist/cli/lib/validation/primitives.js +2 -2
  39. package/dist/cli/lib/validation/test-selection.js +2 -2
  40. package/dist/core/bounded-output.js +32 -7
  41. package/dist/core/change-classification-policy.js +47 -0
  42. package/dist/core/change-classification.js +10 -43
  43. package/dist/core/check-issues.js +7 -1
  44. package/dist/core/command-contract-validation.js +28 -4
  45. package/dist/core/command-env.js +1 -1
  46. package/dist/core/config-loading.js +9 -3
  47. package/dist/core/contract-lint.js +8 -3
  48. package/dist/core/correlation-id.js +16 -0
  49. package/dist/core/run-receipt.js +1 -0
  50. package/dist/core/safe-filesystem.js +11 -4
  51. package/dist/core/skill-route-alignment.js +1 -0
  52. package/dist/core/skill-route-explanation.js +9 -3
  53. package/dist/core/test-selection.js +2 -3
  54. package/dist/core/verification-scheduler.js +7 -6
  55. package/dist/core/version-sources.js +2 -3
  56. package/package.json +4 -1
  57. package/schemas/README.md +4 -0
  58. package/schemas/change-verification-report.schema.json +4 -0
  59. package/schemas/classify-report.schema.json +4 -0
  60. package/schemas/commands.schema.json +1 -0
  61. package/schemas/dashboard-export.schema.json +4 -0
  62. package/schemas/latest-run-pointer.schema.json +4 -0
  63. package/schemas/run-receipt.schema.json +4 -0
  64. package/schemas/verify-report.schema.json +4 -0
  65. package/schemas/verify-run-manifest.schema.json +4 -0
  66. package/templates/default/i18n.toml +3 -3
  67. package/templates/default/locales/en/.mustflow/skills/INDEX.md +10 -6
  68. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +25 -2
  69. package/templates/default/locales/en/.mustflow/skills/routes.toml +2 -2
  70. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +9 -1
  71. package/templates/default/locales/en/.mustflow/skills/test-design-guard/SKILL.md +9 -1
  72. package/templates/default/manifest.toml +1 -1
@@ -1,68 +1,20 @@
1
- import { existsSync, readFileSync, statSync } from 'node:fs';
2
- import { createHash } from 'node:crypto';
1
+ import { existsSync, readFileSync } from 'node:fs';
3
2
  import path from 'node:path';
4
- import { isRecord, readCommandContract, readString, readStringArray } from '../command-contract.js';
5
- import { listFilesRecursive, toPosixPath } from '../filesystem.js';
6
- import { readTomlFile } from '../toml.js';
3
+ import { isRecord, readStringArray } from '../command-contract.js';
4
+ import { toPosixPath } from '../filesystem.js';
7
5
  import { collectSourceAnchorIndexRecords, hasHighRiskSourceAnchorRiskTags, } from '../../../core/source-anchor-status.js';
8
- import { listSourceAnchorFiles } from '../../../core/source-anchors.js';
9
- import { normalizeCommandEffects } from '../../../core/command-effects.js';
10
6
  import { listChangeClassificationRuleDescriptors } from '../../../core/change-classification.js';
11
7
  import { writeFileInsideWithoutSymlinks } from '../../../core/safe-filesystem.js';
12
- import { DEFAULT_DATABASE_RELATIVE_PATH, DEFAULT_PROMPT_CACHE_STABLE_READ, DEFAULT_PROMPT_CACHE_TASK_SOURCES, DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES, INDEX_CONFIG_RELATIVE_PATH, LOCAL_INDEX_CONTENT_MODE, LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS, LOCAL_INDEX_PARSER_VERSION, LOCAL_INDEX_SCHEMA_VERSION, LOCAL_INDEX_STORE_FULL_CONTENT, LATEST_RUN_STATE_RELATIVE_PATH, MAX_SEARCH_MATCH_SNIPPET_CHARS, MAX_SNIPPET_BYTES_PER_DOCUMENT, MUSTFLOW_RELATIVE_PATH, SEARCH_BACKEND_FTS5, SEARCH_BACKEND_TABLE_SCAN, SEARCH_MATCH_CONTEXT_AFTER_CHARS, SEARCH_MATCH_CONTEXT_BEFORE_CHARS, SEARCH_MATCH_TRUNCATION_MARKER, SEARCH_NGRAM_MAX_GRAMS_PER_TARGET, SEARCH_NGRAM_MAX_LENGTH, SEARCH_NGRAM_MAX_TOKEN_CHARS, SEARCH_NGRAM_MIN_LENGTH, SOURCE_INDEX_MAX_FILE_BYTES, TEST_DISABLE_FTS5_ENV, } from './constants.js';
8
+ import { DEFAULT_DATABASE_RELATIVE_PATH, DEFAULT_PROMPT_CACHE_STABLE_READ, DEFAULT_PROMPT_CACHE_TASK_SOURCES, DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES, LOCAL_INDEX_CONTENT_MODE, LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS, LOCAL_INDEX_PARSER_VERSION, LOCAL_INDEX_SCHEMA_VERSION, LOCAL_INDEX_STORE_FULL_CONTENT, MAX_SEARCH_MATCH_SNIPPET_CHARS, MAX_SNIPPET_BYTES_PER_DOCUMENT, SEARCH_BACKEND_FTS5, SEARCH_BACKEND_TABLE_SCAN, SEARCH_MATCH_CONTEXT_AFTER_CHARS, SEARCH_MATCH_CONTEXT_BEFORE_CHARS, SEARCH_MATCH_TRUNCATION_MARKER, SEARCH_NGRAM_MAX_GRAMS_PER_TARGET, SEARCH_NGRAM_MAX_LENGTH, SEARCH_NGRAM_MAX_TOKEN_CHARS, SEARCH_NGRAM_MIN_LENGTH, SOURCE_INDEX_MAX_FILE_BYTES, TEST_DISABLE_FTS5_ENV, } from './constants.js';
9
+ import { collectCommandIntents } from './command-effect-index.js';
10
+ import { sha256Text } from './hashing.js';
13
11
  import { loadSqlJs } from './sql.js';
12
+ import { collectFastPreflightIndexedFileMetadataRecords, collectIndexedFileRecords, collectSourceAnchorCandidatePaths, getSourceScopeHash, hashIndexedFileMetadataRecords, normalizeIndexedFileSourceScope, readIndexedFileRecord, readLocalIndexSourceConfig, readMustflowToml, } from './source-index.js';
13
+ import { createVerificationEvidenceIndex } from './verification-evidence.js';
14
+ import { collectDocuments, collectDocumentsFromPaths, collectSkillRoutes, collectSkills, getExistingIndexablePaths, readText, skillRouteKey, splitVerificationIntents, } from './workflow-documents.js';
14
15
  export function getLocalIndexDatabasePath(projectRoot) {
15
16
  return path.join(projectRoot, ...DEFAULT_DATABASE_RELATIVE_PATH.split('/'));
16
17
  }
17
- function getExistingIndexablePaths(projectRoot) {
18
- const paths = new Set();
19
- const addIfExists = (relativePath) => {
20
- if (existsSync(path.join(projectRoot, ...relativePath.split('/')))) {
21
- paths.add(relativePath);
22
- }
23
- };
24
- addIfExists('AGENTS.md');
25
- for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'docs'))) {
26
- if (relativePath.endsWith('.md')) {
27
- paths.add(toPosixPath(path.join('.mustflow', 'docs', relativePath)));
28
- }
29
- }
30
- for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'context'))) {
31
- if (relativePath.endsWith('.md')) {
32
- paths.add(toPosixPath(path.join('.mustflow', 'context', relativePath)));
33
- }
34
- }
35
- for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'skills'))) {
36
- if (relativePath === 'INDEX.md' || relativePath.endsWith('/SKILL.md')) {
37
- paths.add(toPosixPath(path.join('.mustflow', 'skills', relativePath)));
38
- }
39
- }
40
- for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'config'))) {
41
- if (relativePath.endsWith('.toml')) {
42
- paths.add(toPosixPath(path.join('.mustflow', 'config', relativePath)));
43
- }
44
- }
45
- return Array.from(paths).sort((left, right) => left.localeCompare(right));
46
- }
47
- function readText(projectRoot, relativePath) {
48
- return readFileSync(path.join(projectRoot, ...relativePath.split('/')), 'utf8');
49
- }
50
- function readMustflowToml(projectRoot) {
51
- const mustflowPath = path.join(projectRoot, ...MUSTFLOW_RELATIVE_PATH.split('/'));
52
- if (!existsSync(mustflowPath)) {
53
- return undefined;
54
- }
55
- const parsed = readTomlFile(mustflowPath);
56
- return isRecord(parsed) ? parsed : undefined;
57
- }
58
- function readIndexToml(projectRoot) {
59
- const indexConfigPath = path.join(projectRoot, ...INDEX_CONFIG_RELATIVE_PATH.split('/'));
60
- if (!existsSync(indexConfigPath)) {
61
- return undefined;
62
- }
63
- const parsed = readTomlFile(indexConfigPath);
64
- return isRecord(parsed) ? parsed : undefined;
65
- }
66
18
  function readNestedTable(table, key) {
67
19
  if (!table || !isRecord(table[key])) {
68
20
  return undefined;
@@ -72,297 +24,6 @@ function readNestedTable(table, key) {
72
24
  function readOptionalStringArray(table, key) {
73
25
  return table ? readStringArray(table, key) ?? null : null;
74
26
  }
75
- function readBoolean(table, key) {
76
- const value = table?.[key];
77
- return typeof value === 'boolean' ? value : undefined;
78
- }
79
- function readPositiveInteger(table, key) {
80
- const value = table?.[key];
81
- if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
82
- return null;
83
- }
84
- return value;
85
- }
86
- function readLocalIndexSourceConfig(projectRoot) {
87
- const sourceIndexTable = readNestedTable(readIndexToml(projectRoot), 'source_index');
88
- const configuredMaxFileBytes = readPositiveInteger(sourceIndexTable, 'max_file_bytes');
89
- return {
90
- enabledByDefault: readBoolean(sourceIndexTable, 'enabled_by_default') === true,
91
- include: readOptionalStringArray(sourceIndexTable, 'include') ?? [],
92
- exclude: readOptionalStringArray(sourceIndexTable, 'exclude') ?? [],
93
- maxFileBytes: Math.min(configuredMaxFileBytes ?? SOURCE_INDEX_MAX_FILE_BYTES, SOURCE_INDEX_MAX_FILE_BYTES),
94
- allowedExtensions: readOptionalStringArray(sourceIndexTable, 'allowed_extensions') ?? [],
95
- };
96
- }
97
- function sha256Text(content) {
98
- return `sha256:${createHash('sha256').update(content).digest('hex')}`;
99
- }
100
- function sha256Bytes(content) {
101
- return `sha256:${createHash('sha256').update(content).digest('hex')}`;
102
- }
103
- function getSourceScopeHash(includeSource, sourceConfig) {
104
- return sha256Text(JSON.stringify({
105
- includeSource,
106
- sourceConfig,
107
- }));
108
- }
109
- function getDocumentType(relativePath) {
110
- if (relativePath === 'AGENTS.md') {
111
- return 'agent_rules';
112
- }
113
- if (relativePath.startsWith('.mustflow/config/')) {
114
- return 'config';
115
- }
116
- if (relativePath === '.mustflow/skills/INDEX.md') {
117
- return 'skill_index';
118
- }
119
- if (relativePath === '.mustflow/context/INDEX.md') {
120
- return 'context_index';
121
- }
122
- if (relativePath.startsWith('.mustflow/context/')) {
123
- return 'context';
124
- }
125
- if (relativePath.endsWith('/SKILL.md')) {
126
- return 'skill';
127
- }
128
- if (relativePath.startsWith('.mustflow/docs/')) {
129
- return 'workflow_doc';
130
- }
131
- return 'document';
132
- }
133
- function parseFrontmatter(content) {
134
- if (!content.startsWith('---')) {
135
- return {};
136
- }
137
- const end = content.indexOf('\n---', 3);
138
- if (end === -1) {
139
- return {};
140
- }
141
- const result = {};
142
- const rawFrontmatter = content.slice(3, end);
143
- for (const line of rawFrontmatter.split(/\r?\n/)) {
144
- const separatorIndex = line.indexOf(':');
145
- if (separatorIndex === -1) {
146
- continue;
147
- }
148
- const key = line.slice(0, separatorIndex).trim();
149
- const value = line.slice(separatorIndex + 1).trim();
150
- if (key.length > 0 && value.length > 0) {
151
- result[key] = value;
152
- }
153
- }
154
- return result;
155
- }
156
- function getTitle(relativePath, content) {
157
- const heading = content.match(/^#\s+(.+)$/mu)?.[1]?.trim();
158
- return heading && heading.length > 0 ? heading : path.posix.basename(relativePath);
159
- }
160
- function getSections(content) {
161
- return [...content.matchAll(/^##\s+(.+)$/gmu)].map((match) => match[1]?.trim()).filter((value) => Boolean(value));
162
- }
163
- function truncateUtf8(value, maxBytes) {
164
- const buffer = Buffer.from(value, 'utf8');
165
- if (buffer.byteLength <= maxBytes) {
166
- return value;
167
- }
168
- return buffer.subarray(0, maxBytes).toString('utf8').replace(/\uFFFD$/u, '');
169
- }
170
- function collectDocuments(projectRoot) {
171
- return getExistingIndexablePaths(projectRoot).map((relativePath) => {
172
- const content = readText(projectRoot, relativePath);
173
- const frontmatter = parseFrontmatter(content);
174
- const revision = Number.parseInt(frontmatter.revision ?? '', 10);
175
- return {
176
- path: relativePath,
177
- type: getDocumentType(relativePath),
178
- title: getTitle(relativePath, content),
179
- locale: frontmatter.locale ?? null,
180
- revision: Number.isInteger(revision) ? revision : null,
181
- contentHash: sha256Text(content),
182
- contentSnippet: truncateUtf8(content, MAX_SNIPPET_BYTES_PER_DOCUMENT),
183
- sections: getSections(content),
184
- };
185
- });
186
- }
187
- function collectSkills(documents) {
188
- return documents
189
- .filter((document) => document.type === 'skill')
190
- .map((document) => ({
191
- name: document.path.split('/').at(-2) ?? document.title,
192
- path: document.path,
193
- title: document.title,
194
- }))
195
- .sort((left, right) => left.name.localeCompare(right.name));
196
- }
197
- function normalizeMarkdownCell(value) {
198
- return value
199
- .replace(/<br\s*\/?>/giu, ' ')
200
- .replace(/`([^`]+)`/gu, '$1')
201
- .replace(/\s+/gu, ' ')
202
- .trim();
203
- }
204
- function parseMarkdownTableRow(line) {
205
- return line
206
- .trim()
207
- .replace(/^\|/u, '')
208
- .replace(/\|$/u, '')
209
- .split('|')
210
- .map((cell) => normalizeMarkdownCell(cell));
211
- }
212
- function isMarkdownSeparatorRow(cells) {
213
- return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/u.test(cell));
214
- }
215
- function skillNameFromPath(skillPath) {
216
- return skillPath.split('/').at(-2) ?? path.posix.basename(skillPath, '.md');
217
- }
218
- function splitVerificationIntents(value) {
219
- return value
220
- .split(',')
221
- .map((item) => item.trim())
222
- .filter(Boolean)
223
- .sort((left, right) => left.localeCompare(right));
224
- }
225
- function collectSkillRoutes(projectRoot) {
226
- const indexPath = path.join(projectRoot, '.mustflow', 'skills', 'INDEX.md');
227
- if (!existsSync(indexPath)) {
228
- return [];
229
- }
230
- const content = readFileSync(indexPath, 'utf8');
231
- const routes = [];
232
- let inRouteTable = false;
233
- for (const line of content.split(/\r?\n/u)) {
234
- if (!line.trim().startsWith('|')) {
235
- if (inRouteTable && line.trim() === '') {
236
- inRouteTable = false;
237
- }
238
- continue;
239
- }
240
- const cells = parseMarkdownTableRow(line);
241
- if (cells.includes('Skill Document') && cells.includes('Trigger')) {
242
- inRouteTable = true;
243
- continue;
244
- }
245
- if (!inRouteTable || isMarkdownSeparatorRow(cells) || cells.length < 7) {
246
- continue;
247
- }
248
- const [trigger, skillPath, requiredInput, editScope, risk, verificationIntents, expectedOutput] = cells;
249
- if (!skillPath?.startsWith('.mustflow/skills/') || !skillPath.endsWith('/SKILL.md')) {
250
- continue;
251
- }
252
- routes.push({
253
- skillName: skillNameFromPath(skillPath),
254
- skillPath,
255
- trigger: trigger ?? '',
256
- requiredInput: requiredInput ?? '',
257
- editScope: editScope ?? '',
258
- risk: risk ?? '',
259
- verificationIntents: splitVerificationIntents(verificationIntents ?? ''),
260
- expectedOutput: expectedOutput ?? '',
261
- });
262
- }
263
- return routes.sort((left, right) => {
264
- const skillOrder = left.skillName.localeCompare(right.skillName);
265
- return skillOrder === 0 ? left.trigger.localeCompare(right.trigger) : skillOrder;
266
- });
267
- }
268
- function collectCommandIntents(projectRoot) {
269
- if (!existsSync(path.join(projectRoot, '.mustflow', 'config', 'commands.toml'))) {
270
- return [];
271
- }
272
- const contract = readCommandContract(projectRoot);
273
- const intents = [];
274
- for (const [name, intent] of Object.entries(contract.intents).sort(([left], [right]) => left.localeCompare(right))) {
275
- if (!isRecord(intent)) {
276
- continue;
277
- }
278
- intents.push({
279
- name,
280
- status: readString(intent, 'status') ?? 'unknown',
281
- lifecycle: readString(intent, 'lifecycle') ?? null,
282
- runPolicy: readString(intent, 'run_policy') ?? null,
283
- description: readString(intent, 'description') ?? null,
284
- effects: normalizeCommandEffects(projectRoot, contract, name),
285
- });
286
- }
287
- return intents;
288
- }
289
- function normalizeIndexedFileSourceScope(value) {
290
- const sourceScope = toSearchString(value);
291
- if (sourceScope === 'source_anchor' || sourceScope === 'state') {
292
- return sourceScope;
293
- }
294
- return 'workflow';
295
- }
296
- function readIndexedFileRecord(projectRoot, relativePath, sourceScope, contentHash = null) {
297
- const metadata = readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope);
298
- return {
299
- ...metadata,
300
- contentHash: contentHash ?? sha256Bytes(readFileSync(path.join(projectRoot, ...relativePath.split('/')))),
301
- };
302
- }
303
- function readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope) {
304
- const fullPath = path.join(projectRoot, ...relativePath.split('/'));
305
- const stats = statSync(fullPath);
306
- return {
307
- path: relativePath,
308
- sourceScope,
309
- sizeBytes: stats.size,
310
- mtimeMs: Math.round(stats.mtimeMs),
311
- };
312
- }
313
- function hashIndexedFileMetadataRecord(projectRoot, metadata) {
314
- return {
315
- ...metadata,
316
- contentHash: sha256Bytes(readFileSync(path.join(projectRoot, ...metadata.path.split('/')))),
317
- };
318
- }
319
- function hashIndexedFileMetadataRecords(projectRoot, metadataRecords) {
320
- return metadataRecords.map((metadata) => hashIndexedFileMetadataRecord(projectRoot, metadata));
321
- }
322
- function collectIndexedFileRecords(projectRoot, documents, sourceAnchors, sourceAnchorCandidatePaths = []) {
323
- const records = new Map();
324
- for (const document of documents) {
325
- records.set(document.path, readIndexedFileRecord(projectRoot, document.path, 'workflow', document.contentHash));
326
- }
327
- const sourcePaths = new Set([...sourceAnchorCandidatePaths, ...sourceAnchors.map((anchor) => anchor.path)]);
328
- for (const anchorPath of [...sourcePaths].sort((left, right) => left.localeCompare(right))) {
329
- if (!records.has(anchorPath)) {
330
- records.set(anchorPath, readIndexedFileRecord(projectRoot, anchorPath, 'source_anchor'));
331
- }
332
- }
333
- if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
334
- records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
335
- }
336
- return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
337
- }
338
- function collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) {
339
- return listSourceAnchorFiles(projectRoot, {
340
- ...sourceConfig,
341
- excludeGeneratedOrVendor: true,
342
- });
343
- }
344
- function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig) {
345
- const records = new Map();
346
- for (const relativePath of getExistingIndexablePaths(projectRoot)) {
347
- records.set(relativePath, readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
348
- }
349
- if (includeSource) {
350
- try {
351
- for (const sourcePath of collectSourceAnchorCandidatePaths(projectRoot, sourceConfig)) {
352
- if (!records.has(sourcePath)) {
353
- records.set(sourcePath, readIndexedFileMetadataRecord(projectRoot, sourcePath, 'source_anchor'));
354
- }
355
- }
356
- }
357
- catch {
358
- return null;
359
- }
360
- }
361
- if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
362
- records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
363
- }
364
- return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
365
- }
366
27
  function normalizeSearchText(value) {
367
28
  return value.trim().replace(/\s+/g, ' ');
368
29
  }
@@ -414,25 +75,7 @@ function queryRows(database, sql, params = []) {
414
75
  return row;
415
76
  });
416
77
  }
417
- const VALIDATION_RATCHET_RISK_CODES = new Set([
418
- 'related_test_deleted',
419
- 'skip_or_only_marker_present',
420
- 'todo_or_pending_marker_added',
421
- 'assertion_count_decreased',
422
- 'assertion_matcher_weakened',
423
- 'negative_assertion_removed',
424
- 'exception_assertion_removed',
425
- 'snapshot_mass_updated',
426
- 'golden_output_replaced',
427
- 'verification_intent_disabled',
428
- 'verification_required_after_removed',
429
- 'success_exit_codes_widened',
430
- 'command_allows_no_tests',
431
- 'command_forces_snapshot_update',
432
- 'command_hides_failure',
433
- 'coverage_threshold_lowered',
434
- 'test_selection_narrowed',
435
- ]);
78
+ const DIRECT_SEARCH_MAX_WORKFLOW_FILES = 200;
436
79
  function searchCapabilities(fts5Available) {
437
80
  return {
438
81
  backend: fts5Available ? SEARCH_BACKEND_FTS5 : SEARCH_BACKEND_TABLE_SCAN,
@@ -452,446 +95,9 @@ function detectLocalSearchCapabilities(database) {
452
95
  return searchCapabilities(false);
453
96
  }
454
97
  }
455
- function isJsonRecord(value) {
456
- return typeof value === 'object' && value !== null && !Array.isArray(value);
457
- }
458
- function readJsonRecord(filePath) {
459
- try {
460
- const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
461
- return isJsonRecord(parsed) ? parsed : null;
462
- }
463
- catch {
464
- return null;
465
- }
466
- }
467
- function stringField(record, key) {
468
- const value = record?.[key];
469
- return typeof value === 'string' ? value : null;
470
- }
471
- function booleanField(record, key) {
472
- return record?.[key] === true;
473
- }
474
- function numberField(record, key) {
475
- const value = record?.[key];
476
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
477
- }
478
- function recordField(record, key) {
479
- const value = record?.[key];
480
- return isJsonRecord(value) ? value : null;
481
- }
482
- function recordArrayField(record, key) {
483
- const value = record?.[key];
484
- return Array.isArray(value) ? value.filter(isJsonRecord) : [];
485
- }
486
- function stringArrayField(record, key) {
487
- const value = record?.[key];
488
- return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
489
- }
490
98
  function joinedList(values) {
491
99
  return [...values].sort((left, right) => left.localeCompare(right)).join(', ');
492
100
  }
493
- function hashJson(value) {
494
- return sha256Text(JSON.stringify(value));
495
- }
496
- function stringListHash(values) {
497
- const normalized = values.filter((value) => typeof value === 'string' && value.length > 0);
498
- return normalized.length > 0 ? hashJson([...normalized].sort((left, right) => left.localeCompare(right))) : null;
499
- }
500
- function reproObservation(routeId, phase, evidence) {
501
- const status = stringField(evidence, 'status');
502
- const outcome = stringField(evidence, 'outcome') ?? status;
503
- const receiptHash = stringField(evidence, 'receipt_sha256');
504
- const diagnosticFingerprint = stringField(evidence, 'diagnostic_fingerprint') ??
505
- stringField(evidence, 'diagnostic_hash') ??
506
- hashJson({
507
- phase,
508
- status,
509
- outcome,
510
- summary: stringField(evidence, 'summary'),
511
- reason: stringField(evidence, 'reason'),
512
- });
513
- return {
514
- routeId,
515
- phase,
516
- outcome,
517
- receiptHash,
518
- diagnosticFingerprint,
519
- };
520
- }
521
- function evidenceStatusForRunReceipt(latest) {
522
- return stringField(latest, 'status') ?? (booleanField(latest, 'timed_out') ? 'timed_out' : 'unknown');
523
- }
524
- function failedIntentsFromReceipts(receipts) {
525
- return receipts
526
- .filter((receipt) => ['failed', 'timed_out', 'start_failed'].includes(receipt.status))
527
- .map((receipt) => receipt.intent)
528
- .filter((intent) => typeof intent === 'string' && intent.length > 0)
529
- .sort((left, right) => left.localeCompare(right));
530
- }
531
- function createFailureFingerprint(input) {
532
- if (input.status === 'passed' ||
533
- input.status === 'verified' ||
534
- (input.failedIntents.length === 0 && input.riskCodes.length === 0 && input.timedOut !== true && !input.errorKind)) {
535
- return null;
536
- }
537
- return sha256Text(JSON.stringify({
538
- command: input.command,
539
- status: input.status,
540
- verificationPlanId: input.verificationPlanId,
541
- primaryReason: input.primaryReason,
542
- failedIntents: [...input.failedIntents].sort((left, right) => left.localeCompare(right)),
543
- riskCodes: [...input.riskCodes].sort((left, right) => left.localeCompare(right)),
544
- runIntent: input.runIntent ?? null,
545
- timedOut: input.timedOut === true,
546
- exitCodeClass: input.exitCodeClass ?? null,
547
- errorKind: input.errorKind ?? null,
548
- }));
549
- }
550
- function createVerificationEvidenceIndex(projectRoot) {
551
- const latestPath = path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/'));
552
- if (!existsSync(latestPath)) {
553
- return {
554
- summaries: [],
555
- verificationPlans: [],
556
- acceptanceCriteria: [],
557
- criterionCoverage: [],
558
- receipts: [],
559
- commandReceiptSummaries: [],
560
- coverageStates: [],
561
- riskSignals: [],
562
- validationRatchetSignals: [],
563
- completionVerdictSummaries: [],
564
- failureFingerprints: [],
565
- reproRoutes: [],
566
- reproObservations: [],
567
- failureFingerprintReadModels: [],
568
- };
569
- }
570
- const latest = readJsonRecord(latestPath);
571
- if (!latest) {
572
- return {
573
- summaries: [],
574
- verificationPlans: [],
575
- acceptanceCriteria: [],
576
- criterionCoverage: [],
577
- receipts: [],
578
- commandReceiptSummaries: [],
579
- coverageStates: [],
580
- riskSignals: [],
581
- validationRatchetSignals: [],
582
- completionVerdictSummaries: [],
583
- failureFingerprints: [],
584
- reproRoutes: [],
585
- reproObservations: [],
586
- failureFingerprintReadModels: [],
587
- };
588
- }
589
- const sourceHash = sha256Bytes(readFileSync(latestPath));
590
- const command = stringField(latest, 'command') ?? 'unknown';
591
- const kind = stringField(latest, 'kind') ?? (command === 'verify' ? 'verify_run_summary' : 'run_receipt');
592
- const evidenceModel = recordField(latest, 'evidence_model');
593
- const completionVerdict = recordField(latest, 'completion_verdict');
594
- const completionEvidence = recordField(completionVerdict, 'evidence');
595
- const verificationPlanId = stringField(latest, 'verification_plan_id') ?? stringField(evidenceModel, 'verification_plan_id');
596
- const primaryReason = stringField(completionVerdict, 'primary_reason');
597
- const status = stringField(latest, 'status') ?? stringField(completionVerdict, 'status') ?? 'unknown';
598
- const completionStatus = stringField(completionVerdict, 'status');
599
- const rawReceipts = recordArrayField(evidenceModel, 'receipts');
600
- const rawCoverage = recordArrayField(evidenceModel, 'coverage_matrix');
601
- const rawRequirements = recordArrayField(evidenceModel, 'requirements');
602
- const rawRisks = recordArrayField(evidenceModel, 'remaining_risks');
603
- const recordedFailureFingerprintRecord = recordField(latest, 'failure_fingerprint');
604
- const repeatedFailureSummary = recordField(latest, 'repeated_failure_summary');
605
- const reproEvidence = recordField(latest, 'repro_evidence') ?? recordField(evidenceModel, 'repro_evidence');
606
- const reproductionRoute = recordField(reproEvidence, 'reproduction_route');
607
- const recordedFailureFingerprint = stringField(recordedFailureFingerprintRecord, 'fingerprint');
608
- const receipts = rawReceipts.length > 0
609
- ? rawReceipts.map((receipt, index) => ({
610
- sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
611
- ordinal: index + 1,
612
- intent: stringField(receipt, 'intent'),
613
- status: stringField(receipt, 'status') ?? 'unknown',
614
- skipped: booleanField(receipt, 'skipped'),
615
- verificationPlanId: stringField(receipt, 'verification_plan_id'),
616
- receiptPath: stringField(receipt, 'receipt_path'),
617
- receiptSha256: stringField(receipt, 'receipt_sha256'),
618
- commandFingerprint: stringField(receipt, 'command_fingerprint'),
619
- contractFingerprint: stringField(receipt, 'contract_fingerprint'),
620
- currentStateHash: stringField(receipt, 'head_tree_hash') ??
621
- stringField(receipt, 'changed_files_hash') ??
622
- stringField(receipt, 'changed_file_hash'),
623
- writeDriftStatus: stringField(receipt, 'write_drift_status'),
624
- }))
625
- : [
626
- {
627
- sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
628
- ordinal: 1,
629
- intent: stringField(latest, 'intent'),
630
- status: evidenceStatusForRunReceipt(latest),
631
- skipped: false,
632
- verificationPlanId: null,
633
- receiptPath: stringField(latest, 'receipt_path') ?? LATEST_RUN_STATE_RELATIVE_PATH,
634
- receiptSha256: sourceHash,
635
- commandFingerprint: stringField(recordField(latest, 'performance'), 'command_fingerprint'),
636
- contractFingerprint: stringField(recordField(latest, 'performance'), 'contract_fingerprint'),
637
- currentStateHash: stringField(latest, 'head_tree_hash') ?? stringField(latest, 'changed_files_hash'),
638
- writeDriftStatus: stringField(recordField(latest, 'write_drift'), 'status'),
639
- },
640
- ];
641
- const coverageStates = rawCoverage.map((coverage) => {
642
- const evidence = recordField(coverage, 'evidence');
643
- return {
644
- sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
645
- criterionId: stringField(coverage, 'criterion_id') ?? 'unknown',
646
- source: stringField(coverage, 'source') ?? 'unknown',
647
- status: stringField(coverage, 'status') ?? 'unknown',
648
- requirementReason: stringField(coverage, 'requirement_reason'),
649
- intents: stringArrayField(evidence, 'intents'),
650
- receiptCount: stringArrayField(evidence, 'receipt_paths').length,
651
- gapCount: stringArrayField(evidence, 'gap_reasons').length,
652
- sourceAnchorCount: stringArrayField(evidence, 'source_anchor_ids').length,
653
- };
654
- });
655
- const riskSignals = rawRisks.map((risk, index) => ({
656
- sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
657
- ordinal: index + 1,
658
- code: stringField(risk, 'code') ?? 'unknown',
659
- severity: stringField(risk, 'severity') ?? 'unknown',
660
- detailHash: sha256Text(stringField(risk, 'detail') ?? ''),
661
- }));
662
- const validationRatchetSignals = rawRisks
663
- .map((risk, index) => {
664
- const code = stringField(risk, 'code') ?? 'unknown';
665
- if (!VALIDATION_RATCHET_RISK_CODES.has(code)) {
666
- return null;
667
- }
668
- const severity = stringField(risk, 'severity') ?? 'unknown';
669
- const pathValue = stringField(risk, 'path');
670
- const detailHash = sha256Text(stringField(risk, 'detail') ?? '');
671
- const pathHash = pathValue === null ? hashJson({ code, detailHash }) : sha256Text(pathValue);
672
- const beforeHash = stringField(risk, 'before_hash') ?? stringField(risk, 'before_digest');
673
- const afterHash = stringField(risk, 'after_hash') ?? stringField(risk, 'after_digest');
674
- return {
675
- signalId: hashJson({
676
- sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
677
- ordinal: index + 1,
678
- planId: verificationPlanId,
679
- code,
680
- pathHash,
681
- beforeHash,
682
- afterHash,
683
- }),
684
- planId: verificationPlanId,
685
- code,
686
- severity,
687
- pathHash,
688
- beforeHash,
689
- afterHash,
690
- };
691
- })
692
- .filter((signal) => signal !== null);
693
- const verificationPlans = verificationPlanId === null
694
- ? []
695
- : [
696
- {
697
- planId: verificationPlanId,
698
- sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
699
- classificationHash: rawRequirements.length > 0 || rawCoverage.length > 0
700
- ? hashJson({
701
- requirements: rawRequirements.map((requirement) => ({
702
- id: stringField(requirement, 'requirement_id') ?? stringField(requirement, 'id'),
703
- reason: stringField(requirement, 'reason'),
704
- source: stringField(requirement, 'source'),
705
- })),
706
- coverage: rawCoverage.map((coverage) => ({
707
- id: stringField(coverage, 'criterion_id'),
708
- reason: stringField(coverage, 'requirement_reason'),
709
- source: stringField(coverage, 'source'),
710
- status: stringField(coverage, 'status'),
711
- })),
712
- })
713
- : null,
714
- commandContractHash: stringListHash(receipts.map((receipt) => receipt.contractFingerprint)),
715
- selectedIntentsHash: stringListHash(receipts.map((receipt) => receipt.intent)),
716
- createdAt: stringField(latest, 'started_at') ?? stringField(latest, 'created_at'),
717
- sourceHash,
718
- },
719
- ];
720
- const acceptanceCriteria = verificationPlanId === null
721
- ? []
722
- : rawCoverage.map((coverage) => {
723
- const evidence = recordField(coverage, 'evidence');
724
- const pathRefs = [
725
- ...stringArrayField(evidence, 'paths'),
726
- ...stringArrayField(evidence, 'changed_paths'),
727
- ...stringArrayField(evidence, 'source_anchor_ids'),
728
- ];
729
- return {
730
- criterionId: stringField(coverage, 'criterion_id') ?? 'unknown',
731
- planId: verificationPlanId,
732
- source: stringField(coverage, 'source') ?? 'unknown',
733
- statementHash: stringField(coverage, 'statement') ? sha256Text(stringField(coverage, 'statement') ?? '') : null,
734
- reason: stringField(coverage, 'requirement_reason'),
735
- surface: stringField(coverage, 'surface'),
736
- pathHash: pathRefs.length > 0 ? stringListHash(pathRefs) : null,
737
- };
738
- });
739
- const criterionCoverage = verificationPlanId === null
740
- ? []
741
- : coverageStates.map((coverage) => ({
742
- criterionId: coverage.criterionId,
743
- planId: verificationPlanId,
744
- status: coverage.status,
745
- receiptCount: coverage.receiptCount,
746
- gapCount: coverage.gapCount,
747
- riskCount: coverage.sourceAnchorCount,
748
- }));
749
- const commandReceiptSummaries = verificationPlanId === null
750
- ? []
751
- : receipts
752
- .filter((receipt) => receipt.verificationPlanId === verificationPlanId || receipt.verificationPlanId === null)
753
- .map((receipt) => ({
754
- receiptHash: receipt.receiptSha256 ??
755
- hashJson({
756
- sourcePath: receipt.sourcePath,
757
- ordinal: receipt.ordinal,
758
- intent: receipt.intent,
759
- status: receipt.status,
760
- verificationPlanId,
761
- }),
762
- planId: verificationPlanId,
763
- intent: receipt.intent,
764
- status: receipt.status,
765
- commandFingerprint: receipt.commandFingerprint,
766
- contractFingerprint: receipt.contractFingerprint,
767
- currentStateHash: receipt.currentStateHash,
768
- writeDriftStatus: receipt.writeDriftStatus,
769
- }));
770
- const completionVerdictSummaries = verificationPlanId === null || completionStatus === null
771
- ? []
772
- : [
773
- {
774
- claimId: hashJson({
775
- sourceHash,
776
- verificationPlanId,
777
- completionStatus,
778
- primaryReason,
779
- }),
780
- planId: verificationPlanId,
781
- status: completionStatus,
782
- primaryReason,
783
- riskCount: riskSignals.length,
784
- contradictionCount: stringArrayField(completionVerdict, 'contradictions').length,
785
- blockerCount: stringArrayField(completionVerdict, 'blockers').length,
786
- },
787
- ];
788
- const failedIntents = failedIntentsFromReceipts(receipts);
789
- const failureFingerprint = recordedFailureFingerprint ??
790
- createFailureFingerprint({
791
- command,
792
- status: completionStatus ?? status,
793
- verificationPlanId,
794
- primaryReason,
795
- failedIntents,
796
- riskCodes: riskSignals.map((risk) => risk.code),
797
- runIntent: stringField(latest, 'intent'),
798
- timedOut: booleanField(latest, 'timed_out'),
799
- exitCodeClass: stringField(recordField(recordField(latest, 'performance'), 'result_summary'), 'exit_code_class'),
800
- errorKind: stringField(recordField(recordField(latest, 'performance'), 'result_summary'), 'error_kind'),
801
- });
802
- const failureFingerprints = failureFingerprint === null
803
- ? []
804
- : [
805
- {
806
- sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
807
- fingerprint: failureFingerprint,
808
- verificationPlanId,
809
- status: completionStatus ?? status,
810
- failedIntents,
811
- primaryReason,
812
- failedIntentsHash: stringField(recordedFailureFingerprintRecord, 'failed_intents_hash') ??
813
- stringField(repeatedFailureSummary, 'failed_intents_hash'),
814
- riskCodesHash: stringField(recordedFailureFingerprintRecord, 'risk_codes_hash') ??
815
- stringField(repeatedFailureSummary, 'risk_codes_hash'),
816
- affectedSurfacesHash: stringField(recordedFailureFingerprintRecord, 'affected_surfaces_hash') ??
817
- stringField(repeatedFailureSummary, 'affected_surfaces_hash'),
818
- firstSeenAt: stringField(repeatedFailureSummary, 'first_seen_at'),
819
- lastSeenAt: stringField(repeatedFailureSummary, 'last_seen_at'),
820
- seenCount: Math.max(1, numberField(repeatedFailureSummary, 'seen_count')),
821
- requiresNewEvidence: booleanField(repeatedFailureSummary, 'requires_new_evidence'),
822
- },
823
- ];
824
- const routeId = stringField(reproductionRoute, 'route_id');
825
- const reproRoutes = routeId === null || reproEvidence === null
826
- ? []
827
- : [
828
- {
829
- routeId,
830
- taskHash: hashJson({
831
- reported_symptom: stringField(reproEvidence, 'reported_symptom'),
832
- expected_behavior: stringField(reproEvidence, 'expected_behavior'),
833
- observed_behavior: stringField(reproEvidence, 'observed_behavior'),
834
- }),
835
- routeDigest: stringField(reproductionRoute, 'route_digest'),
836
- routeKind: stringField(reproductionRoute, 'route_kind'),
837
- failureOracleHash: stringField(reproductionRoute, 'failure_oracle_hash'),
838
- },
839
- ];
840
- const reproObservations = routeId === null || reproEvidence === null
841
- ? []
842
- : [
843
- reproObservation(routeId, 'before_fix', recordField(reproEvidence, 'before_fix')),
844
- reproObservation(routeId, 'after_fix', recordField(reproEvidence, 'after_fix')),
845
- reproObservation(routeId, 'regression_guard', recordField(reproEvidence, 'regression_guard')),
846
- ];
847
- const failureFingerprintReadModels = failureFingerprints.map((fingerprint) => ({
848
- fingerprint: fingerprint.fingerprint,
849
- planId: fingerprint.verificationPlanId,
850
- failedIntentsHash: fingerprint.failedIntentsHash ?? stringListHash(fingerprint.failedIntents),
851
- riskCodesHash: fingerprint.riskCodesHash,
852
- seenCount: fingerprint.seenCount,
853
- firstSeenAt: fingerprint.firstSeenAt,
854
- lastSeenAt: fingerprint.lastSeenAt,
855
- }));
856
- return {
857
- summaries: [
858
- {
859
- sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
860
- sourceHash,
861
- command,
862
- kind,
863
- status,
864
- runDir: stringField(latest, 'run_dir'),
865
- manifestPath: stringField(latest, 'manifest_path'),
866
- verificationPlanId,
867
- completionStatus,
868
- primaryReason,
869
- matchedIntents: numberField(completionEvidence, 'matched_intents'),
870
- ranIntents: numberField(completionEvidence, 'ran_intents'),
871
- passedIntents: numberField(completionEvidence, 'passed_intents'),
872
- failedIntents: numberField(completionEvidence, 'failed_intents'),
873
- skippedIntents: numberField(completionEvidence, 'skipped_intents'),
874
- receiptCount: receipts.length,
875
- coverageCount: coverageStates.length,
876
- remainingRiskCount: riskSignals.length,
877
- failureFingerprint,
878
- },
879
- ],
880
- verificationPlans,
881
- acceptanceCriteria,
882
- criterionCoverage,
883
- receipts,
884
- commandReceiptSummaries,
885
- coverageStates,
886
- riskSignals,
887
- validationRatchetSignals,
888
- completionVerdictSummaries,
889
- failureFingerprints,
890
- reproRoutes,
891
- reproObservations,
892
- failureFingerprintReadModels,
893
- };
894
- }
895
101
  function readMetadataValue(database, key) {
896
102
  return toSearchString(queryRows(database, 'SELECT value FROM metadata WHERE key = ?', [key])[0]?.value) || undefined;
897
103
  }
@@ -1591,9 +797,6 @@ function populatePathSurfaceReadModel(database) {
1591
797
  insertPathSurfaceReasons(database, rule.id, 'drift_check', rule.surface.driftChecks);
1592
798
  }
1593
799
  }
1594
- function skillRouteKey(route) {
1595
- return `${route.skillName}\u0000${route.trigger}`;
1596
- }
1597
800
  function populateSearchTables(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors) {
1598
801
  for (const document of documents) {
1599
802
  const documentTerms = queryRows(database, 'SELECT term FROM document_terms WHERE document_path = ? ORDER BY term', [
@@ -2075,7 +1278,7 @@ function indexedFilesMatch(database, currentFiles) {
2075
1278
  if (!current) {
2076
1279
  return false;
2077
1280
  }
2078
- if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
1281
+ if (normalizeIndexedFileSourceScope(toSearchString(row.source_scope)) !== current.sourceScope ||
2079
1282
  toSearchString(row.content_hash) !== current.contentHash ||
2080
1283
  toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
2081
1284
  return false;
@@ -2095,7 +1298,7 @@ function indexedFileMetadataMatch(database, currentFiles) {
2095
1298
  if (!current) {
2096
1299
  return false;
2097
1300
  }
2098
- if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
1301
+ if (normalizeIndexedFileSourceScope(toSearchString(row.source_scope)) !== current.sourceScope ||
2099
1302
  toNullableNumber(row.size_bytes) !== current.sizeBytes ||
2100
1303
  toNullableNumber(row.mtime_ms) !== current.mtimeMs ||
2101
1304
  toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
@@ -2296,7 +1499,7 @@ function getStalePaths(projectRoot, database) {
2296
1499
  const indexedPaths = new Set(indexedRows.map((row) => toSearchString(row.path)));
2297
1500
  for (const row of indexedRows) {
2298
1501
  const indexedPath = toSearchString(row.path);
2299
- const sourceScope = normalizeIndexedFileSourceScope(row.source_scope);
1502
+ const sourceScope = normalizeIndexedFileSourceScope(toSearchString(row.source_scope));
2300
1503
  try {
2301
1504
  const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
2302
1505
  if (current.contentHash !== toSearchString(row.content_hash)) {
@@ -2989,6 +2192,99 @@ function scoreIndexedOrTableScan(primaryFields, secondaryFields, query, indexedM
2989
2192
  const tableScore = scoreMatch(primaryFields, secondaryFields, query);
2990
2193
  return indexedMatches.active && matchSet.size > 0 && matchSet.has(key) ? Math.max(tableScore, 20) : tableScore;
2991
2194
  }
2195
+ function sortLocalSearchResults(results, scope, limit) {
2196
+ return [...results]
2197
+ .sort((left, right) => {
2198
+ if (scope === 'all' && left.authority_rank !== right.authority_rank) {
2199
+ return left.authority_rank - right.authority_rank;
2200
+ }
2201
+ return right.score - left.score || (left.path ?? left.name ?? '').localeCompare(right.path ?? right.name ?? '');
2202
+ })
2203
+ .slice(0, limit);
2204
+ }
2205
+ function collectBoundedDirectSearchDocuments(projectRoot) {
2206
+ const documents = [];
2207
+ const relativePaths = getExistingIndexablePaths(projectRoot).slice(0, DIRECT_SEARCH_MAX_WORKFLOW_FILES);
2208
+ for (const relativePath of relativePaths) {
2209
+ try {
2210
+ documents.push(...collectDocumentsFromPaths(projectRoot, [relativePath]));
2211
+ }
2212
+ catch {
2213
+ continue;
2214
+ }
2215
+ }
2216
+ return documents;
2217
+ }
2218
+ function searchLocalWorkflowFilesDirectly(projectRoot, databasePath, normalizedQuery, limit, scope) {
2219
+ const cacheLayers = readCacheLayerSets(projectRoot);
2220
+ const results = [];
2221
+ if (scope === 'workflow' || scope === 'all') {
2222
+ const documents = collectBoundedDirectSearchDocuments(projectRoot);
2223
+ for (const document of documents) {
2224
+ let searchableContent = document.contentSnippet;
2225
+ try {
2226
+ searchableContent = readText(projectRoot, document.path);
2227
+ }
2228
+ catch {
2229
+ searchableContent = document.contentSnippet;
2230
+ }
2231
+ const primaryFields = [document.path, document.title];
2232
+ const secondaryFields = [document.type, searchableContent, ...document.sections];
2233
+ const fields = [...primaryFields, ...secondaryFields];
2234
+ if (!isMatched(fields, normalizedQuery)) {
2235
+ continue;
2236
+ }
2237
+ results.push(withCacheHint({
2238
+ kind: 'document',
2239
+ path: document.path,
2240
+ title: document.title,
2241
+ document_type: document.type,
2242
+ ...workflowAuthorityForDocument(document.type),
2243
+ match: getMatchSnippet(fields, normalizedQuery),
2244
+ score: scoreMatch(primaryFields, secondaryFields, normalizedQuery),
2245
+ }, cacheLayers));
2246
+ }
2247
+ for (const skill of collectSkills(documents)) {
2248
+ const fields = [skill.name, skill.path, skill.title];
2249
+ if (!isMatched(fields, normalizedQuery)) {
2250
+ continue;
2251
+ }
2252
+ results.push(withCacheHint({
2253
+ kind: 'skill',
2254
+ name: skill.name,
2255
+ path: skill.path,
2256
+ title: skill.title,
2257
+ ...skillAuthority(),
2258
+ match: getMatchSnippet(fields, normalizedQuery),
2259
+ score: scoreMatch(fields, [], normalizedQuery),
2260
+ }, cacheLayers));
2261
+ }
2262
+ }
2263
+ const sortedResults = sortLocalSearchResults(results, scope, limit);
2264
+ return {
2265
+ schema_version: LOCAL_INDEX_SCHEMA_VERSION,
2266
+ command: 'search',
2267
+ ok: true,
2268
+ mustflow_root: path.resolve(projectRoot),
2269
+ database_path: databasePath,
2270
+ query: normalizedQuery,
2271
+ limit,
2272
+ scope,
2273
+ index_fresh: false,
2274
+ stale_paths: [],
2275
+ search_backend: SEARCH_BACKEND_TABLE_SCAN,
2276
+ search_fts5_available: false,
2277
+ result_count: sortedResults.length,
2278
+ results: sortedResults,
2279
+ };
2280
+ }
2281
+ function isLocalIndexStaleError(error) {
2282
+ return error instanceof Error && error.message.startsWith('Local mustflow index is stale:');
2283
+ }
2284
+ function isLocalIndexRuntimeUnavailableError(error) {
2285
+ const message = error instanceof Error ? error.message : String(error);
2286
+ return /file is not a database|database disk image is malformed|no such table|no such column|sqlite|sql\.js/iu.test(message);
2287
+ }
2992
2288
  /**
2993
2289
  * mf:anchor cli.search.local-index
2994
2290
  * purpose: Search the local index while preserving workflow authority above source navigation hints.
@@ -3001,14 +2297,20 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
3001
2297
  const limit = Math.max(1, Math.min(options.limit ?? 10, 50));
3002
2298
  const scope = options.scope ?? 'workflow';
3003
2299
  const databasePath = getLocalIndexDatabasePath(projectRoot);
3004
- if (!existsSync(databasePath)) {
3005
- throw new Error('Local mustflow index not found. Run `mf index` before searching.');
3006
- }
3007
2300
  if (normalizedQuery.length === 0) {
3008
2301
  throw new Error('Search query must not be empty.');
3009
2302
  }
3010
- const SQL = await loadSqlJs();
3011
- const database = new SQL.Database(readFileSync(databasePath));
2303
+ if (!existsSync(databasePath)) {
2304
+ return searchLocalWorkflowFilesDirectly(projectRoot, databasePath, normalizedQuery, limit, scope);
2305
+ }
2306
+ let database;
2307
+ try {
2308
+ const SQL = await loadSqlJs();
2309
+ database = new SQL.Database(readFileSync(databasePath));
2310
+ }
2311
+ catch {
2312
+ return searchLocalWorkflowFilesDirectly(projectRoot, databasePath, normalizedQuery, limit, scope);
2313
+ }
3012
2314
  const cacheLayers = readCacheLayerSets(projectRoot);
3013
2315
  let capabilities = searchCapabilities(false);
3014
2316
  const results = [];
@@ -3158,17 +2460,19 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
3158
2460
  }
3159
2461
  }
3160
2462
  }
2463
+ catch (error) {
2464
+ if (isLocalIndexStaleError(error)) {
2465
+ throw error;
2466
+ }
2467
+ if (isLocalIndexRuntimeUnavailableError(error)) {
2468
+ return searchLocalWorkflowFilesDirectly(projectRoot, databasePath, normalizedQuery, limit, scope);
2469
+ }
2470
+ throw error;
2471
+ }
3161
2472
  finally {
3162
2473
  database.close();
3163
2474
  }
3164
- const sortedResults = results
3165
- .sort((left, right) => {
3166
- if (scope === 'all' && left.authority_rank !== right.authority_rank) {
3167
- return left.authority_rank - right.authority_rank;
3168
- }
3169
- return right.score - left.score || (left.path ?? left.name ?? '').localeCompare(right.path ?? right.name ?? '');
3170
- })
3171
- .slice(0, limit);
2475
+ const sortedResults = sortLocalSearchResults(results, scope, limit);
3172
2476
  return {
3173
2477
  schema_version: LOCAL_INDEX_SCHEMA_VERSION,
3174
2478
  command: 'search',