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