mustflow 1.31.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -9
- package/dist/cli/commands/classify.js +61 -6
- package/dist/cli/commands/contract-lint.js +13 -4
- package/dist/cli/commands/dashboard.js +77 -2
- package/dist/cli/commands/explain-verify.js +11 -1
- package/dist/cli/commands/index.js +14 -0
- package/dist/cli/commands/run.js +4 -1
- package/dist/cli/commands/verify.js +986 -43
- package/dist/cli/i18n/en.js +61 -10
- package/dist/cli/i18n/es.js +61 -10
- package/dist/cli/i18n/fr.js +61 -10
- package/dist/cli/i18n/hi.js +61 -10
- package/dist/cli/i18n/ko.js +61 -10
- package/dist/cli/i18n/zh.js +61 -10
- package/dist/cli/lib/dashboard-export.js +62 -12
- package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
- package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
- package/dist/cli/lib/dashboard-html/styles.js +572 -0
- package/dist/cli/lib/dashboard-html/template.js +134 -0
- package/dist/cli/lib/dashboard-html/types.js +1 -0
- package/dist/cli/lib/dashboard-html.js +1 -1907
- package/dist/cli/lib/dashboard-locale.js +37 -0
- package/dist/cli/lib/local-index/constants.js +48 -0
- package/dist/cli/lib/local-index/index.js +2951 -0
- package/dist/cli/lib/local-index/sql.js +15 -0
- package/dist/cli/lib/local-index/types.js +1 -0
- package/dist/cli/lib/local-index.js +1 -1911
- package/dist/cli/lib/run-plan.js +76 -1
- package/dist/cli/lib/templates.js +18 -1
- package/dist/cli/lib/validation/command-intents.js +11 -0
- package/dist/cli/lib/validation/constants.js +238 -0
- package/dist/cli/lib/validation/index.js +1384 -0
- package/dist/cli/lib/validation/primitives.js +198 -0
- package/dist/cli/lib/validation/test-selection.js +95 -0
- package/dist/cli/lib/validation/types.js +1 -0
- package/dist/cli/lib/validation.js +1 -1770
- package/dist/core/check-issues.js +6 -0
- package/dist/core/completion-verdict.js +341 -0
- package/dist/core/contract-lint.js +221 -6
- package/dist/core/external-evidence.js +9 -0
- package/dist/core/public-json-contracts.js +21 -0
- package/dist/core/repeated-failure.js +179 -0
- package/dist/core/repro-evidence.js +134 -0
- package/dist/core/scope-risk.js +64 -0
- package/dist/core/skill-route-alignment.js +20 -0
- package/dist/core/source-anchor-status.js +4 -1
- package/dist/core/test-selection.js +3 -0
- package/dist/core/validation-ratchet.js +196 -0
- package/dist/core/verification-evidence.js +249 -0
- package/examples/README.md +12 -4
- package/package.json +3 -3
- package/schemas/README.md +13 -3
- package/schemas/change-verification-report.schema.json +16 -2
- package/schemas/commands.schema.json +4 -0
- package/schemas/contract-lint-report.schema.json +29 -0
- package/schemas/dashboard-export.schema.json +310 -0
- package/schemas/explain-report.schema.json +173 -1
- package/schemas/latest-run-pointer.schema.json +601 -0
- package/schemas/run-receipt.schema.json +4 -0
- package/schemas/test-selection.schema.json +81 -0
- package/schemas/verify-report.schema.json +578 -1
- package/schemas/verify-run-manifest.schema.json +627 -0
- package/templates/default/i18n.toml +1 -1
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
- package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
- package/templates/default/manifest.toml +29 -2
|
@@ -1,1911 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { isRecord, readCommandContract, readString, readStringArray } from './command-contract.js';
|
|
6
|
-
import { listFilesRecursive, toPosixPath } from './filesystem.js';
|
|
7
|
-
import { readTomlFile } from './toml.js';
|
|
8
|
-
import { collectSourceAnchorIndexRecords, } from '../../core/source-anchor-status.js';
|
|
9
|
-
import { normalizeCommandEffects } from '../../core/command-effects.js';
|
|
10
|
-
import { listChangeClassificationRuleDescriptors } from '../../core/change-classification.js';
|
|
11
|
-
const LOCAL_INDEX_SCHEMA_VERSION = '13';
|
|
12
|
-
const LOCAL_INDEX_PARSER_VERSION = '1';
|
|
13
|
-
const DEFAULT_DATABASE_RELATIVE_PATH = '.mustflow/cache/mustflow.sqlite';
|
|
14
|
-
const LOCAL_INDEX_CONTENT_MODE = 'metadata_and_snippets';
|
|
15
|
-
const LOCAL_INDEX_STORE_FULL_CONTENT = false;
|
|
16
|
-
const MAX_SNIPPET_BYTES_PER_DOCUMENT = 2048;
|
|
17
|
-
const MAX_SEARCH_MATCH_SNIPPET_CHARS = 240;
|
|
18
|
-
const SEARCH_MATCH_CONTEXT_BEFORE_CHARS = 48;
|
|
19
|
-
const SEARCH_MATCH_CONTEXT_AFTER_CHARS = 96;
|
|
20
|
-
const SEARCH_MATCH_TRUNCATION_MARKER = '...';
|
|
21
|
-
const SEARCH_NGRAM_MIN_LENGTH = 2;
|
|
22
|
-
const SEARCH_NGRAM_MAX_LENGTH = 3;
|
|
23
|
-
const SEARCH_BACKEND_FTS5 = 'fts5';
|
|
24
|
-
const SEARCH_BACKEND_TABLE_SCAN = 'table_scan';
|
|
25
|
-
const TEST_DISABLE_FTS5_ENV = 'MUSTFLOW_TEST_DISABLE_FTS5';
|
|
26
|
-
const MUSTFLOW_RELATIVE_PATH = '.mustflow/config/mustflow.toml';
|
|
27
|
-
const INDEX_CONFIG_RELATIVE_PATH = '.mustflow/config/index.toml';
|
|
28
|
-
const DEFAULT_PROMPT_CACHE_STABLE_READ = [
|
|
29
|
-
'AGENTS.md',
|
|
30
|
-
'.mustflow/docs/agent-workflow.md',
|
|
31
|
-
'.mustflow/config/mustflow.toml',
|
|
32
|
-
'.mustflow/config/commands.toml',
|
|
33
|
-
'.mustflow/skills/INDEX.md',
|
|
34
|
-
];
|
|
35
|
-
const DEFAULT_PROMPT_CACHE_TASK_SOURCES = [
|
|
36
|
-
'.mustflow/context/INDEX.md',
|
|
37
|
-
'REPO_MAP.md',
|
|
38
|
-
'matching_skill',
|
|
39
|
-
'relevant_source_files',
|
|
40
|
-
];
|
|
41
|
-
const DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES = [
|
|
42
|
-
'.mustflow/state/runs/latest.json',
|
|
43
|
-
'changed_files',
|
|
44
|
-
'command_output_tail',
|
|
45
|
-
'current_user_task',
|
|
46
|
-
];
|
|
47
|
-
export function getLocalIndexDatabasePath(projectRoot) {
|
|
48
|
-
return path.join(projectRoot, ...DEFAULT_DATABASE_RELATIVE_PATH.split('/'));
|
|
49
|
-
}
|
|
50
|
-
function getExistingIndexablePaths(projectRoot) {
|
|
51
|
-
const paths = new Set();
|
|
52
|
-
const addIfExists = (relativePath) => {
|
|
53
|
-
if (existsSync(path.join(projectRoot, ...relativePath.split('/')))) {
|
|
54
|
-
paths.add(relativePath);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
addIfExists('AGENTS.md');
|
|
58
|
-
for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'docs'))) {
|
|
59
|
-
if (relativePath.endsWith('.md')) {
|
|
60
|
-
paths.add(toPosixPath(path.join('.mustflow', 'docs', relativePath)));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'context'))) {
|
|
64
|
-
if (relativePath.endsWith('.md')) {
|
|
65
|
-
paths.add(toPosixPath(path.join('.mustflow', 'context', relativePath)));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'skills'))) {
|
|
69
|
-
if (relativePath === 'INDEX.md' || relativePath.endsWith('/SKILL.md')) {
|
|
70
|
-
paths.add(toPosixPath(path.join('.mustflow', 'skills', relativePath)));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'config'))) {
|
|
74
|
-
if (relativePath.endsWith('.toml')) {
|
|
75
|
-
paths.add(toPosixPath(path.join('.mustflow', 'config', relativePath)));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return Array.from(paths).sort((left, right) => left.localeCompare(right));
|
|
79
|
-
}
|
|
80
|
-
function readText(projectRoot, relativePath) {
|
|
81
|
-
return readFileSync(path.join(projectRoot, ...relativePath.split('/')), 'utf8');
|
|
82
|
-
}
|
|
83
|
-
function readMustflowToml(projectRoot) {
|
|
84
|
-
const mustflowPath = path.join(projectRoot, ...MUSTFLOW_RELATIVE_PATH.split('/'));
|
|
85
|
-
if (!existsSync(mustflowPath)) {
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
const parsed = readTomlFile(mustflowPath);
|
|
89
|
-
return isRecord(parsed) ? parsed : undefined;
|
|
90
|
-
}
|
|
91
|
-
function readIndexToml(projectRoot) {
|
|
92
|
-
const indexConfigPath = path.join(projectRoot, ...INDEX_CONFIG_RELATIVE_PATH.split('/'));
|
|
93
|
-
if (!existsSync(indexConfigPath)) {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
const parsed = readTomlFile(indexConfigPath);
|
|
97
|
-
return isRecord(parsed) ? parsed : undefined;
|
|
98
|
-
}
|
|
99
|
-
function readNestedTable(table, key) {
|
|
100
|
-
if (!table || !isRecord(table[key])) {
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
return table[key];
|
|
104
|
-
}
|
|
105
|
-
function readOptionalStringArray(table, key) {
|
|
106
|
-
return table ? readStringArray(table, key) ?? null : null;
|
|
107
|
-
}
|
|
108
|
-
function readBoolean(table, key) {
|
|
109
|
-
const value = table?.[key];
|
|
110
|
-
return typeof value === 'boolean' ? value : undefined;
|
|
111
|
-
}
|
|
112
|
-
function readPositiveInteger(table, key) {
|
|
113
|
-
const value = table?.[key];
|
|
114
|
-
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
return value;
|
|
118
|
-
}
|
|
119
|
-
function readLocalIndexSourceConfig(projectRoot) {
|
|
120
|
-
const sourceIndexTable = readNestedTable(readIndexToml(projectRoot), 'source_index');
|
|
121
|
-
return {
|
|
122
|
-
enabledByDefault: readBoolean(sourceIndexTable, 'enabled_by_default') === true,
|
|
123
|
-
include: readOptionalStringArray(sourceIndexTable, 'include') ?? [],
|
|
124
|
-
exclude: readOptionalStringArray(sourceIndexTable, 'exclude') ?? [],
|
|
125
|
-
maxFileBytes: readPositiveInteger(sourceIndexTable, 'max_file_bytes'),
|
|
126
|
-
allowedExtensions: readOptionalStringArray(sourceIndexTable, 'allowed_extensions') ?? [],
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function sha256Text(content) {
|
|
130
|
-
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
131
|
-
}
|
|
132
|
-
function sha256Bytes(content) {
|
|
133
|
-
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
134
|
-
}
|
|
135
|
-
function getSourceScopeHash(includeSource, sourceConfig) {
|
|
136
|
-
return sha256Text(JSON.stringify({
|
|
137
|
-
includeSource,
|
|
138
|
-
sourceConfig,
|
|
139
|
-
}));
|
|
140
|
-
}
|
|
141
|
-
function getDocumentType(relativePath) {
|
|
142
|
-
if (relativePath === 'AGENTS.md') {
|
|
143
|
-
return 'agent_rules';
|
|
144
|
-
}
|
|
145
|
-
if (relativePath.startsWith('.mustflow/config/')) {
|
|
146
|
-
return 'config';
|
|
147
|
-
}
|
|
148
|
-
if (relativePath === '.mustflow/skills/INDEX.md') {
|
|
149
|
-
return 'skill_index';
|
|
150
|
-
}
|
|
151
|
-
if (relativePath === '.mustflow/context/INDEX.md') {
|
|
152
|
-
return 'context_index';
|
|
153
|
-
}
|
|
154
|
-
if (relativePath.startsWith('.mustflow/context/')) {
|
|
155
|
-
return 'context';
|
|
156
|
-
}
|
|
157
|
-
if (relativePath.endsWith('/SKILL.md')) {
|
|
158
|
-
return 'skill';
|
|
159
|
-
}
|
|
160
|
-
if (relativePath.startsWith('.mustflow/docs/')) {
|
|
161
|
-
return 'workflow_doc';
|
|
162
|
-
}
|
|
163
|
-
return 'document';
|
|
164
|
-
}
|
|
165
|
-
function parseFrontmatter(content) {
|
|
166
|
-
if (!content.startsWith('---')) {
|
|
167
|
-
return {};
|
|
168
|
-
}
|
|
169
|
-
const end = content.indexOf('\n---', 3);
|
|
170
|
-
if (end === -1) {
|
|
171
|
-
return {};
|
|
172
|
-
}
|
|
173
|
-
const result = {};
|
|
174
|
-
const rawFrontmatter = content.slice(3, end);
|
|
175
|
-
for (const line of rawFrontmatter.split(/\r?\n/)) {
|
|
176
|
-
const separatorIndex = line.indexOf(':');
|
|
177
|
-
if (separatorIndex === -1) {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
const key = line.slice(0, separatorIndex).trim();
|
|
181
|
-
const value = line.slice(separatorIndex + 1).trim();
|
|
182
|
-
if (key.length > 0 && value.length > 0) {
|
|
183
|
-
result[key] = value;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
188
|
-
function getTitle(relativePath, content) {
|
|
189
|
-
const heading = content.match(/^#\s+(.+)$/mu)?.[1]?.trim();
|
|
190
|
-
return heading && heading.length > 0 ? heading : path.posix.basename(relativePath);
|
|
191
|
-
}
|
|
192
|
-
function getSections(content) {
|
|
193
|
-
return [...content.matchAll(/^##\s+(.+)$/gmu)].map((match) => match[1]?.trim()).filter((value) => Boolean(value));
|
|
194
|
-
}
|
|
195
|
-
function truncateUtf8(value, maxBytes) {
|
|
196
|
-
const buffer = Buffer.from(value, 'utf8');
|
|
197
|
-
if (buffer.byteLength <= maxBytes) {
|
|
198
|
-
return value;
|
|
199
|
-
}
|
|
200
|
-
return buffer.subarray(0, maxBytes).toString('utf8').replace(/\uFFFD$/u, '');
|
|
201
|
-
}
|
|
202
|
-
function collectDocuments(projectRoot) {
|
|
203
|
-
return getExistingIndexablePaths(projectRoot).map((relativePath) => {
|
|
204
|
-
const content = readText(projectRoot, relativePath);
|
|
205
|
-
const frontmatter = parseFrontmatter(content);
|
|
206
|
-
const revision = Number.parseInt(frontmatter.revision ?? '', 10);
|
|
207
|
-
return {
|
|
208
|
-
path: relativePath,
|
|
209
|
-
type: getDocumentType(relativePath),
|
|
210
|
-
title: getTitle(relativePath, content),
|
|
211
|
-
locale: frontmatter.locale ?? null,
|
|
212
|
-
revision: Number.isInteger(revision) ? revision : null,
|
|
213
|
-
contentHash: sha256Text(content),
|
|
214
|
-
contentSnippet: truncateUtf8(content, MAX_SNIPPET_BYTES_PER_DOCUMENT),
|
|
215
|
-
sections: getSections(content),
|
|
216
|
-
};
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
function collectSkills(documents) {
|
|
220
|
-
return documents
|
|
221
|
-
.filter((document) => document.type === 'skill')
|
|
222
|
-
.map((document) => ({
|
|
223
|
-
name: document.path.split('/').at(-2) ?? document.title,
|
|
224
|
-
path: document.path,
|
|
225
|
-
title: document.title,
|
|
226
|
-
}))
|
|
227
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
228
|
-
}
|
|
229
|
-
function normalizeMarkdownCell(value) {
|
|
230
|
-
return value
|
|
231
|
-
.replace(/<br\s*\/?>/giu, ' ')
|
|
232
|
-
.replace(/`([^`]+)`/gu, '$1')
|
|
233
|
-
.replace(/\s+/gu, ' ')
|
|
234
|
-
.trim();
|
|
235
|
-
}
|
|
236
|
-
function parseMarkdownTableRow(line) {
|
|
237
|
-
return line
|
|
238
|
-
.trim()
|
|
239
|
-
.replace(/^\|/u, '')
|
|
240
|
-
.replace(/\|$/u, '')
|
|
241
|
-
.split('|')
|
|
242
|
-
.map((cell) => normalizeMarkdownCell(cell));
|
|
243
|
-
}
|
|
244
|
-
function isMarkdownSeparatorRow(cells) {
|
|
245
|
-
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/u.test(cell));
|
|
246
|
-
}
|
|
247
|
-
function skillNameFromPath(skillPath) {
|
|
248
|
-
return skillPath.split('/').at(-2) ?? path.posix.basename(skillPath, '.md');
|
|
249
|
-
}
|
|
250
|
-
function splitVerificationIntents(value) {
|
|
251
|
-
return value
|
|
252
|
-
.split(',')
|
|
253
|
-
.map((item) => item.trim())
|
|
254
|
-
.filter(Boolean)
|
|
255
|
-
.sort((left, right) => left.localeCompare(right));
|
|
256
|
-
}
|
|
257
|
-
function collectSkillRoutes(projectRoot) {
|
|
258
|
-
const indexPath = path.join(projectRoot, '.mustflow', 'skills', 'INDEX.md');
|
|
259
|
-
if (!existsSync(indexPath)) {
|
|
260
|
-
return [];
|
|
261
|
-
}
|
|
262
|
-
const content = readFileSync(indexPath, 'utf8');
|
|
263
|
-
const routes = [];
|
|
264
|
-
let inRouteTable = false;
|
|
265
|
-
for (const line of content.split(/\r?\n/u)) {
|
|
266
|
-
if (!line.trim().startsWith('|')) {
|
|
267
|
-
if (inRouteTable && line.trim() === '') {
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const cells = parseMarkdownTableRow(line);
|
|
273
|
-
if (cells.includes('Skill Document') && cells.includes('Trigger')) {
|
|
274
|
-
inRouteTable = true;
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
if (!inRouteTable || isMarkdownSeparatorRow(cells) || cells.length < 7) {
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
const [trigger, skillPath, requiredInput, editScope, risk, verificationIntents, expectedOutput] = cells;
|
|
281
|
-
if (!skillPath?.startsWith('.mustflow/skills/') || !skillPath.endsWith('/SKILL.md')) {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
routes.push({
|
|
285
|
-
skillName: skillNameFromPath(skillPath),
|
|
286
|
-
skillPath,
|
|
287
|
-
trigger: trigger ?? '',
|
|
288
|
-
requiredInput: requiredInput ?? '',
|
|
289
|
-
editScope: editScope ?? '',
|
|
290
|
-
risk: risk ?? '',
|
|
291
|
-
verificationIntents: splitVerificationIntents(verificationIntents ?? ''),
|
|
292
|
-
expectedOutput: expectedOutput ?? '',
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
return routes.sort((left, right) => {
|
|
296
|
-
const skillOrder = left.skillName.localeCompare(right.skillName);
|
|
297
|
-
return skillOrder === 0 ? left.trigger.localeCompare(right.trigger) : skillOrder;
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
function collectCommandIntents(projectRoot) {
|
|
301
|
-
if (!existsSync(path.join(projectRoot, '.mustflow', 'config', 'commands.toml'))) {
|
|
302
|
-
return [];
|
|
303
|
-
}
|
|
304
|
-
const contract = readCommandContract(projectRoot);
|
|
305
|
-
const intents = [];
|
|
306
|
-
for (const [name, intent] of Object.entries(contract.intents).sort(([left], [right]) => left.localeCompare(right))) {
|
|
307
|
-
if (!isRecord(intent)) {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
intents.push({
|
|
311
|
-
name,
|
|
312
|
-
status: readString(intent, 'status') ?? 'unknown',
|
|
313
|
-
lifecycle: readString(intent, 'lifecycle') ?? null,
|
|
314
|
-
runPolicy: readString(intent, 'run_policy') ?? null,
|
|
315
|
-
description: readString(intent, 'description') ?? null,
|
|
316
|
-
effects: normalizeCommandEffects(projectRoot, contract, name),
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
return intents;
|
|
320
|
-
}
|
|
321
|
-
function readIndexedFileRecord(projectRoot, relativePath, sourceScope, contentHash = null) {
|
|
322
|
-
const fullPath = path.join(projectRoot, ...relativePath.split('/'));
|
|
323
|
-
const stats = statSync(fullPath);
|
|
324
|
-
return {
|
|
325
|
-
path: relativePath,
|
|
326
|
-
sourceScope,
|
|
327
|
-
sizeBytes: stats.size,
|
|
328
|
-
mtimeMs: Math.round(stats.mtimeMs),
|
|
329
|
-
contentHash: contentHash ?? sha256Bytes(readFileSync(fullPath)),
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
function collectIndexedFileRecords(projectRoot, documents, sourceAnchors) {
|
|
333
|
-
const records = new Map();
|
|
334
|
-
for (const document of documents) {
|
|
335
|
-
records.set(document.path, readIndexedFileRecord(projectRoot, document.path, 'workflow', document.contentHash));
|
|
336
|
-
}
|
|
337
|
-
for (const anchorPath of [...new Set(sourceAnchors.map((anchor) => anchor.path))].sort((left, right) => left.localeCompare(right))) {
|
|
338
|
-
if (!records.has(anchorPath)) {
|
|
339
|
-
records.set(anchorPath, readIndexedFileRecord(projectRoot, anchorPath, 'source_anchor'));
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
343
|
-
}
|
|
344
|
-
async function loadSqlJs() {
|
|
345
|
-
const require = createRequire(import.meta.url);
|
|
346
|
-
const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm');
|
|
347
|
-
const sqlJsModule = (await import('sql.js'));
|
|
348
|
-
const initSqlJs = typeof sqlJsModule === 'function' ? sqlJsModule : sqlJsModule.default;
|
|
349
|
-
if (!initSqlJs) {
|
|
350
|
-
throw new Error('Unable to load sql.js');
|
|
351
|
-
}
|
|
352
|
-
return initSqlJs({
|
|
353
|
-
locateFile(fileName) {
|
|
354
|
-
return fileName.endsWith('.wasm') ? wasmPath : fileName;
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
function normalizeSearchText(value) {
|
|
359
|
-
return value.trim().replace(/\s+/g, ' ');
|
|
360
|
-
}
|
|
361
|
-
function normalizeSearchTokenText(value) {
|
|
362
|
-
return normalizeSearchText(value).normalize('NFKC').toLowerCase();
|
|
363
|
-
}
|
|
364
|
-
function extractSearchTokens(value) {
|
|
365
|
-
return [...normalizeSearchTokenText(value).matchAll(/[\p{L}\p{N}]+/gu)]
|
|
366
|
-
.map((match) => match[0])
|
|
367
|
-
.filter((token) => Boolean(token));
|
|
368
|
-
}
|
|
369
|
-
function buildSearchNgrams(values) {
|
|
370
|
-
const grams = new Set();
|
|
371
|
-
for (const value of values) {
|
|
372
|
-
for (const token of extractSearchTokens(value)) {
|
|
373
|
-
const maxLength = Math.min(SEARCH_NGRAM_MAX_LENGTH, token.length);
|
|
374
|
-
for (let length = SEARCH_NGRAM_MIN_LENGTH; length <= maxLength; length += 1) {
|
|
375
|
-
for (let index = 0; index <= token.length - length; index += 1) {
|
|
376
|
-
grams.add(token.slice(index, index + length));
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return [...grams].sort((left, right) => left.localeCompare(right));
|
|
382
|
-
}
|
|
383
|
-
function toSearchString(value) {
|
|
384
|
-
if (value === null || value === undefined) {
|
|
385
|
-
return '';
|
|
386
|
-
}
|
|
387
|
-
if (value instanceof Uint8Array) {
|
|
388
|
-
return '';
|
|
389
|
-
}
|
|
390
|
-
return String(value);
|
|
391
|
-
}
|
|
392
|
-
function queryRows(database, sql, params = []) {
|
|
393
|
-
const [result] = database.exec(sql, params);
|
|
394
|
-
if (!result) {
|
|
395
|
-
return [];
|
|
396
|
-
}
|
|
397
|
-
return result.values.map((values) => {
|
|
398
|
-
const row = {};
|
|
399
|
-
result.columns.forEach((column, index) => {
|
|
400
|
-
row[column] = values[index] ?? null;
|
|
401
|
-
});
|
|
402
|
-
return row;
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
function searchCapabilities(fts5Available) {
|
|
406
|
-
return {
|
|
407
|
-
backend: fts5Available ? SEARCH_BACKEND_FTS5 : SEARCH_BACKEND_TABLE_SCAN,
|
|
408
|
-
fts5Available,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
function detectLocalSearchCapabilities(database) {
|
|
412
|
-
if (process.env[TEST_DISABLE_FTS5_ENV] === '1') {
|
|
413
|
-
return searchCapabilities(false);
|
|
414
|
-
}
|
|
415
|
-
try {
|
|
416
|
-
database.run('CREATE VIRTUAL TABLE temp.mustflow_fts5_probe USING fts5(value)');
|
|
417
|
-
database.run('DROP TABLE temp.mustflow_fts5_probe');
|
|
418
|
-
return searchCapabilities(true);
|
|
419
|
-
}
|
|
420
|
-
catch {
|
|
421
|
-
return searchCapabilities(false);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
function readMetadataValue(database, key) {
|
|
425
|
-
return toSearchString(queryRows(database, 'SELECT value FROM metadata WHERE key = ?', [key])[0]?.value) || undefined;
|
|
426
|
-
}
|
|
427
|
-
function hasTable(database, tableName) {
|
|
428
|
-
return queryRows(database, 'SELECT name FROM sqlite_master WHERE type = "table" AND name = ?', [tableName]).length > 0;
|
|
429
|
-
}
|
|
430
|
-
function readStoredSearchCapabilities(database) {
|
|
431
|
-
const fts5Available = readMetadataValue(database, 'search_fts5_available') === 'true';
|
|
432
|
-
const backend = readMetadataValue(database, 'search_backend');
|
|
433
|
-
if (backend === SEARCH_BACKEND_FTS5 && hasTable(database, 'search_documents_fts')) {
|
|
434
|
-
return { backend: SEARCH_BACKEND_FTS5, fts5Available };
|
|
435
|
-
}
|
|
436
|
-
return { backend: SEARCH_BACKEND_TABLE_SCAN, fts5Available };
|
|
437
|
-
}
|
|
438
|
-
function toNullableNumber(value) {
|
|
439
|
-
if (typeof value !== 'number') {
|
|
440
|
-
return null;
|
|
441
|
-
}
|
|
442
|
-
return Number.isFinite(value) ? value : null;
|
|
443
|
-
}
|
|
444
|
-
function splitIndexedList(value) {
|
|
445
|
-
return toSearchString(value)
|
|
446
|
-
.split(',')
|
|
447
|
-
.map((item) => item.trim())
|
|
448
|
-
.filter(Boolean)
|
|
449
|
-
.sort((left, right) => left.localeCompare(right));
|
|
450
|
-
}
|
|
451
|
-
function createCommandEffectGraphStatus(databasePath, status, stalePaths = []) {
|
|
452
|
-
return {
|
|
453
|
-
source: 'local_index',
|
|
454
|
-
authority: 'explanation_only',
|
|
455
|
-
commandAuthority: '.mustflow/config/commands.toml',
|
|
456
|
-
grantsCommandAuthority: false,
|
|
457
|
-
status,
|
|
458
|
-
databasePath,
|
|
459
|
-
indexFresh: status === 'fresh',
|
|
460
|
-
stalePaths,
|
|
461
|
-
writeLocks: [],
|
|
462
|
-
lockConflicts: [],
|
|
463
|
-
refreshHint: status === 'fresh' ? null : 'Run `mf index` to refresh command-effect graph explanations.',
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
async function readPreviousSourceAnchorSnapshots(databasePath) {
|
|
467
|
-
if (!existsSync(databasePath)) {
|
|
468
|
-
return [];
|
|
469
|
-
}
|
|
470
|
-
const SQL = await loadSqlJs();
|
|
471
|
-
const database = new SQL.Database(readFileSync(databasePath));
|
|
472
|
-
try {
|
|
473
|
-
const rows = queryRows(database, `
|
|
474
|
-
SELECT
|
|
475
|
-
source_anchors.id,
|
|
476
|
-
source_anchors.path,
|
|
477
|
-
source_anchors.line_start,
|
|
478
|
-
source_anchors.purpose,
|
|
479
|
-
source_anchors.search_terms,
|
|
480
|
-
source_anchors.invariant,
|
|
481
|
-
source_anchors.risk,
|
|
482
|
-
source_anchor_fingerprints.anchor_metadata_hash,
|
|
483
|
-
source_anchor_fingerprints.anchor_text_hash,
|
|
484
|
-
source_anchor_fingerprints.context_hash,
|
|
485
|
-
source_anchor_fingerprints.search_terms_hash,
|
|
486
|
-
source_anchor_fingerprints.invariant_hash,
|
|
487
|
-
source_anchor_fingerprints.risk_hash,
|
|
488
|
-
source_anchor_fingerprints.symbol_kind,
|
|
489
|
-
source_anchor_fingerprints.symbol_name,
|
|
490
|
-
source_anchor_fingerprints.symbol_exported,
|
|
491
|
-
source_anchor_fingerprints.signature_hash,
|
|
492
|
-
source_anchor_fingerprints.body_hash,
|
|
493
|
-
source_anchor_fingerprints.symbol_start_line,
|
|
494
|
-
source_anchor_fingerprints.symbol_end_line
|
|
495
|
-
FROM source_anchors
|
|
496
|
-
JOIN source_anchor_fingerprints ON source_anchor_fingerprints.anchor_id = source_anchors.id
|
|
497
|
-
`);
|
|
498
|
-
return rows.map((row) => {
|
|
499
|
-
const symbol = {
|
|
500
|
-
kind: toSearchString(row.symbol_kind),
|
|
501
|
-
name: toSearchString(row.symbol_name) || null,
|
|
502
|
-
exported: Number(row.symbol_exported) === 1,
|
|
503
|
-
signatureHash: toSearchString(row.signature_hash) || null,
|
|
504
|
-
bodyHash: toSearchString(row.body_hash) || null,
|
|
505
|
-
startLine: toNullableNumber(row.symbol_start_line),
|
|
506
|
-
endLine: toNullableNumber(row.symbol_end_line),
|
|
507
|
-
};
|
|
508
|
-
return {
|
|
509
|
-
id: toSearchString(row.id),
|
|
510
|
-
path: toSearchString(row.path),
|
|
511
|
-
lineStart: Number(row.line_start),
|
|
512
|
-
purpose: toSearchString(row.purpose) || null,
|
|
513
|
-
search: toSearchString(row.search_terms)
|
|
514
|
-
.split(/[,;]/u)
|
|
515
|
-
.map((value) => value.trim())
|
|
516
|
-
.filter((value) => value.length > 0),
|
|
517
|
-
invariant: toSearchString(row.invariant) || null,
|
|
518
|
-
risk: toSearchString(row.risk)
|
|
519
|
-
.split(/[,;]/u)
|
|
520
|
-
.map((value) => value.trim())
|
|
521
|
-
.filter((value) => value.length > 0),
|
|
522
|
-
navigationOnly: true,
|
|
523
|
-
canInstructAgent: false,
|
|
524
|
-
fingerprint: {
|
|
525
|
-
anchorMetadataHash: toSearchString(row.anchor_metadata_hash),
|
|
526
|
-
anchorTextHash: toSearchString(row.anchor_text_hash),
|
|
527
|
-
contextHash: toSearchString(row.context_hash),
|
|
528
|
-
searchTermsHash: toSearchString(row.search_terms_hash) || null,
|
|
529
|
-
invariantHash: toSearchString(row.invariant_hash) || null,
|
|
530
|
-
riskHash: toSearchString(row.risk_hash),
|
|
531
|
-
symbol,
|
|
532
|
-
},
|
|
533
|
-
};
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
catch {
|
|
537
|
-
return [];
|
|
538
|
-
}
|
|
539
|
-
finally {
|
|
540
|
-
database.close();
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
function readCacheLayerSets(projectRoot) {
|
|
544
|
-
const mustflow = readMustflowToml(projectRoot);
|
|
545
|
-
const promptCache = readNestedTable(mustflow, 'prompt_cache');
|
|
546
|
-
const layers = readNestedTable(promptCache, 'layers');
|
|
547
|
-
const stable = readNestedTable(layers, 'stable');
|
|
548
|
-
const task = readNestedTable(layers, 'task');
|
|
549
|
-
const volatile = readNestedTable(layers, 'volatile');
|
|
550
|
-
const normalize = (values) => new Set(values.map((value) => toPosixPath(value)));
|
|
551
|
-
return {
|
|
552
|
-
stable: normalize(readOptionalStringArray(stable, 'read') ?? [...DEFAULT_PROMPT_CACHE_STABLE_READ]),
|
|
553
|
-
task: normalize(readOptionalStringArray(task, 'sources') ?? [...DEFAULT_PROMPT_CACHE_TASK_SOURCES]),
|
|
554
|
-
volatile: normalize(readOptionalStringArray(volatile, 'sources') ?? [...DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES]),
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
function inferCacheLayer(relativePath, kind, cacheLayers) {
|
|
558
|
-
const normalized = relativePath ? toPosixPath(relativePath) : null;
|
|
559
|
-
if (normalized && cacheLayers.volatile.has(normalized)) {
|
|
560
|
-
return 'volatile';
|
|
561
|
-
}
|
|
562
|
-
if (normalized && cacheLayers.stable.has(normalized)) {
|
|
563
|
-
return 'stable';
|
|
564
|
-
}
|
|
565
|
-
if (kind === 'command_intent' && cacheLayers.stable.has('.mustflow/config/commands.toml')) {
|
|
566
|
-
return 'stable';
|
|
567
|
-
}
|
|
568
|
-
if (normalized &&
|
|
569
|
-
(cacheLayers.task.has(normalized) || normalized.startsWith('.mustflow/context/') || normalized.endsWith('/SKILL.md'))) {
|
|
570
|
-
return 'task';
|
|
571
|
-
}
|
|
572
|
-
if (normalized?.startsWith('.mustflow/state/') || normalized?.startsWith('.mustflow/cache/')) {
|
|
573
|
-
return 'volatile';
|
|
574
|
-
}
|
|
575
|
-
return 'task';
|
|
576
|
-
}
|
|
577
|
-
function withCacheHint(item, cacheLayers) {
|
|
578
|
-
const layer = inferCacheLayer(item.path ?? null, item.kind, cacheLayers);
|
|
579
|
-
return {
|
|
580
|
-
...item,
|
|
581
|
-
cache_layer: layer,
|
|
582
|
-
volatile: layer === 'volatile',
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
function workflowAuthorityForDocument(documentType) {
|
|
586
|
-
if (documentType === 'agent_rules' || documentType === 'config' || documentType === 'workflow_doc') {
|
|
587
|
-
return {
|
|
588
|
-
authority_rank: 2,
|
|
589
|
-
authority_label: 'workflow_authority',
|
|
590
|
-
source_scope: 'workflow',
|
|
591
|
-
navigation_only: false,
|
|
592
|
-
can_instruct_agent: true,
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
return {
|
|
596
|
-
authority_rank: 4,
|
|
597
|
-
authority_label: 'workflow_context',
|
|
598
|
-
source_scope: 'workflow',
|
|
599
|
-
navigation_only: false,
|
|
600
|
-
can_instruct_agent: false,
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
function skillAuthority() {
|
|
604
|
-
return {
|
|
605
|
-
authority_rank: 3,
|
|
606
|
-
authority_label: 'skill_procedure',
|
|
607
|
-
source_scope: 'workflow',
|
|
608
|
-
navigation_only: false,
|
|
609
|
-
can_instruct_agent: true,
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
function commandIntentAuthority() {
|
|
613
|
-
return {
|
|
614
|
-
authority_rank: 1,
|
|
615
|
-
authority_label: 'command_contract',
|
|
616
|
-
source_scope: 'workflow',
|
|
617
|
-
navigation_only: false,
|
|
618
|
-
can_instruct_agent: true,
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
function sourceAnchorAuthority() {
|
|
622
|
-
return {
|
|
623
|
-
authority_rank: 5,
|
|
624
|
-
authority_label: 'source_navigation_hint',
|
|
625
|
-
source_scope: 'source',
|
|
626
|
-
navigation_only: true,
|
|
627
|
-
can_instruct_agent: false,
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
function getMatchSnippet(fields, query) {
|
|
631
|
-
const normalized = normalizeSearchText(fields.join(' '));
|
|
632
|
-
const lower = normalized.toLowerCase();
|
|
633
|
-
let start = lower.indexOf(query.toLowerCase());
|
|
634
|
-
let matchLength = query.length;
|
|
635
|
-
if (start === -1) {
|
|
636
|
-
const [firstGram] = buildSearchNgrams([query]).filter((gram) => lower.includes(gram));
|
|
637
|
-
if (!firstGram) {
|
|
638
|
-
return truncateSearchMatchSnippet(normalized);
|
|
639
|
-
}
|
|
640
|
-
start = lower.indexOf(firstGram);
|
|
641
|
-
matchLength = firstGram.length;
|
|
642
|
-
}
|
|
643
|
-
const from = Math.max(0, start - SEARCH_MATCH_CONTEXT_BEFORE_CHARS);
|
|
644
|
-
const to = Math.min(normalized.length, start + matchLength + SEARCH_MATCH_CONTEXT_AFTER_CHARS);
|
|
645
|
-
const prefix = from > 0 ? SEARCH_MATCH_TRUNCATION_MARKER : '';
|
|
646
|
-
const suffix = to < normalized.length ? SEARCH_MATCH_TRUNCATION_MARKER : '';
|
|
647
|
-
return truncateSearchMatchSnippet(`${prefix}${normalized.slice(from, to)}${suffix}`);
|
|
648
|
-
}
|
|
649
|
-
function truncateSearchMatchSnippet(value) {
|
|
650
|
-
if (value.length <= MAX_SEARCH_MATCH_SNIPPET_CHARS) {
|
|
651
|
-
return value;
|
|
652
|
-
}
|
|
653
|
-
return `${value.slice(0, MAX_SEARCH_MATCH_SNIPPET_CHARS - SEARCH_MATCH_TRUNCATION_MARKER.length)}${SEARCH_MATCH_TRUNCATION_MARKER}`;
|
|
654
|
-
}
|
|
655
|
-
function scoreMatch(primaryFields, secondaryFields, query) {
|
|
656
|
-
const lowerQuery = query.toLowerCase();
|
|
657
|
-
if (primaryFields.some((field) => field.toLowerCase() === lowerQuery)) {
|
|
658
|
-
return 100;
|
|
659
|
-
}
|
|
660
|
-
if (primaryFields.some((field) => field.toLowerCase().includes(lowerQuery))) {
|
|
661
|
-
return 80;
|
|
662
|
-
}
|
|
663
|
-
if (secondaryFields.some((field) => field.toLowerCase().includes(lowerQuery))) {
|
|
664
|
-
return 40;
|
|
665
|
-
}
|
|
666
|
-
return 0;
|
|
667
|
-
}
|
|
668
|
-
function isMatched(fields, query) {
|
|
669
|
-
const lowerQuery = query.toLowerCase();
|
|
670
|
-
return fields.some((field) => field.toLowerCase().includes(lowerQuery));
|
|
671
|
-
}
|
|
672
|
-
function createSchema(database, capabilities) {
|
|
673
|
-
database.run(`
|
|
674
|
-
CREATE TABLE metadata (
|
|
675
|
-
key TEXT PRIMARY KEY,
|
|
676
|
-
value TEXT NOT NULL
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
CREATE TABLE indexed_files (
|
|
680
|
-
path TEXT PRIMARY KEY,
|
|
681
|
-
source_scope TEXT NOT NULL,
|
|
682
|
-
size_bytes INTEGER NOT NULL,
|
|
683
|
-
mtime_ms INTEGER NOT NULL,
|
|
684
|
-
content_hash TEXT NOT NULL,
|
|
685
|
-
indexed_at TEXT NOT NULL,
|
|
686
|
-
index_mode TEXT NOT NULL,
|
|
687
|
-
parser_version TEXT NOT NULL
|
|
688
|
-
);
|
|
689
|
-
|
|
690
|
-
CREATE TABLE documents (
|
|
691
|
-
path TEXT PRIMARY KEY,
|
|
692
|
-
type TEXT NOT NULL,
|
|
693
|
-
title TEXT NOT NULL,
|
|
694
|
-
locale TEXT,
|
|
695
|
-
revision INTEGER,
|
|
696
|
-
content_hash TEXT NOT NULL,
|
|
697
|
-
content_snippet TEXT NOT NULL
|
|
698
|
-
);
|
|
699
|
-
|
|
700
|
-
CREATE TABLE sections (
|
|
701
|
-
document_path TEXT NOT NULL,
|
|
702
|
-
ordinal INTEGER NOT NULL,
|
|
703
|
-
heading TEXT NOT NULL,
|
|
704
|
-
PRIMARY KEY (document_path, ordinal)
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
CREATE TABLE document_terms (
|
|
708
|
-
document_path TEXT NOT NULL,
|
|
709
|
-
term TEXT NOT NULL,
|
|
710
|
-
source TEXT NOT NULL,
|
|
711
|
-
PRIMARY KEY (document_path, term, source)
|
|
712
|
-
);
|
|
713
|
-
|
|
714
|
-
CREATE TABLE search_ngrams (
|
|
715
|
-
target_kind TEXT NOT NULL,
|
|
716
|
-
target_key TEXT NOT NULL,
|
|
717
|
-
gram TEXT NOT NULL,
|
|
718
|
-
source TEXT NOT NULL,
|
|
719
|
-
PRIMARY KEY (target_kind, target_key, gram, source)
|
|
720
|
-
);
|
|
721
|
-
|
|
722
|
-
CREATE INDEX search_ngrams_lookup ON search_ngrams(target_kind, gram, target_key);
|
|
723
|
-
|
|
724
|
-
CREATE TABLE skills (
|
|
725
|
-
name TEXT PRIMARY KEY,
|
|
726
|
-
path TEXT NOT NULL,
|
|
727
|
-
title TEXT NOT NULL
|
|
728
|
-
);
|
|
729
|
-
|
|
730
|
-
CREATE TABLE skill_routes (
|
|
731
|
-
skill_name TEXT NOT NULL,
|
|
732
|
-
skill_path TEXT NOT NULL,
|
|
733
|
-
trigger TEXT NOT NULL,
|
|
734
|
-
required_input TEXT NOT NULL,
|
|
735
|
-
edit_scope TEXT NOT NULL,
|
|
736
|
-
risk TEXT NOT NULL,
|
|
737
|
-
verification_intents TEXT NOT NULL,
|
|
738
|
-
expected_output TEXT NOT NULL,
|
|
739
|
-
PRIMARY KEY (skill_name, trigger)
|
|
740
|
-
);
|
|
741
|
-
|
|
742
|
-
CREATE TABLE command_intents (
|
|
743
|
-
name TEXT PRIMARY KEY,
|
|
744
|
-
status TEXT NOT NULL,
|
|
745
|
-
lifecycle TEXT,
|
|
746
|
-
run_policy TEXT,
|
|
747
|
-
description TEXT
|
|
748
|
-
);
|
|
749
|
-
|
|
750
|
-
CREATE TABLE command_effects (
|
|
751
|
-
intent TEXT NOT NULL,
|
|
752
|
-
source TEXT NOT NULL,
|
|
753
|
-
access TEXT NOT NULL,
|
|
754
|
-
mode TEXT NOT NULL,
|
|
755
|
-
path TEXT,
|
|
756
|
-
lock TEXT NOT NULL,
|
|
757
|
-
concurrency TEXT NOT NULL
|
|
758
|
-
);
|
|
759
|
-
|
|
760
|
-
CREATE VIEW command_write_locks AS
|
|
761
|
-
SELECT
|
|
762
|
-
intent,
|
|
763
|
-
lock,
|
|
764
|
-
group_concat(DISTINCT path) AS paths,
|
|
765
|
-
group_concat(DISTINCT mode) AS modes,
|
|
766
|
-
group_concat(DISTINCT source) AS sources,
|
|
767
|
-
group_concat(DISTINCT concurrency) AS concurrencies,
|
|
768
|
-
count(*) AS effect_count
|
|
769
|
-
FROM command_effects
|
|
770
|
-
WHERE access = 'write'
|
|
771
|
-
GROUP BY intent, lock;
|
|
772
|
-
|
|
773
|
-
CREATE VIEW command_lock_conflicts AS
|
|
774
|
-
SELECT
|
|
775
|
-
a.intent AS left_intent,
|
|
776
|
-
b.intent AS right_intent,
|
|
777
|
-
a.lock AS lock,
|
|
778
|
-
group_concat(DISTINCT a.path) AS left_paths,
|
|
779
|
-
group_concat(DISTINCT b.path) AS right_paths,
|
|
780
|
-
group_concat(DISTINCT a.mode) AS left_modes,
|
|
781
|
-
group_concat(DISTINCT b.mode) AS right_modes,
|
|
782
|
-
group_concat(DISTINCT a.concurrency) AS left_concurrencies,
|
|
783
|
-
group_concat(DISTINCT b.concurrency) AS right_concurrencies
|
|
784
|
-
FROM command_effects a
|
|
785
|
-
JOIN command_effects b
|
|
786
|
-
ON a.lock = b.lock
|
|
787
|
-
AND a.intent < b.intent
|
|
788
|
-
WHERE
|
|
789
|
-
a.access = 'write'
|
|
790
|
-
OR b.access = 'write'
|
|
791
|
-
OR a.concurrency = 'exclusive'
|
|
792
|
-
OR b.concurrency = 'exclusive'
|
|
793
|
-
OR a.mode = 'delete_recreate'
|
|
794
|
-
OR b.mode = 'delete_recreate'
|
|
795
|
-
GROUP BY a.intent, b.intent, a.lock;
|
|
796
|
-
|
|
797
|
-
CREATE TABLE path_surfaces (
|
|
798
|
-
rule_id TEXT PRIMARY KEY,
|
|
799
|
-
pattern_kind TEXT NOT NULL,
|
|
800
|
-
pattern TEXT NOT NULL,
|
|
801
|
-
pattern_flags TEXT NOT NULL,
|
|
802
|
-
surface_kind TEXT NOT NULL,
|
|
803
|
-
category TEXT NOT NULL,
|
|
804
|
-
is_public_surface INTEGER NOT NULL,
|
|
805
|
-
update_policy TEXT NOT NULL
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
CREATE TABLE path_surface_reasons (
|
|
809
|
-
rule_id TEXT NOT NULL,
|
|
810
|
-
reason_kind TEXT NOT NULL,
|
|
811
|
-
reason TEXT NOT NULL,
|
|
812
|
-
ordinal INTEGER NOT NULL,
|
|
813
|
-
PRIMARY KEY (rule_id, reason_kind, reason)
|
|
814
|
-
);
|
|
815
|
-
|
|
816
|
-
CREATE TABLE source_anchors (
|
|
817
|
-
id TEXT PRIMARY KEY,
|
|
818
|
-
path TEXT NOT NULL,
|
|
819
|
-
line_start INTEGER NOT NULL,
|
|
820
|
-
purpose TEXT,
|
|
821
|
-
search_terms TEXT NOT NULL,
|
|
822
|
-
invariant TEXT,
|
|
823
|
-
risk TEXT NOT NULL,
|
|
824
|
-
navigation_only INTEGER NOT NULL,
|
|
825
|
-
can_instruct_agent INTEGER NOT NULL
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
CREATE TABLE source_anchor_fingerprints (
|
|
829
|
-
anchor_id TEXT PRIMARY KEY,
|
|
830
|
-
path TEXT NOT NULL,
|
|
831
|
-
line_start INTEGER NOT NULL,
|
|
832
|
-
anchor_metadata_hash TEXT NOT NULL,
|
|
833
|
-
anchor_text_hash TEXT NOT NULL,
|
|
834
|
-
context_hash TEXT NOT NULL,
|
|
835
|
-
search_terms_hash TEXT,
|
|
836
|
-
invariant_hash TEXT,
|
|
837
|
-
risk_hash TEXT NOT NULL,
|
|
838
|
-
symbol_kind TEXT NOT NULL,
|
|
839
|
-
symbol_name TEXT,
|
|
840
|
-
symbol_exported INTEGER NOT NULL,
|
|
841
|
-
signature_hash TEXT,
|
|
842
|
-
body_hash TEXT,
|
|
843
|
-
symbol_start_line INTEGER,
|
|
844
|
-
symbol_end_line INTEGER
|
|
845
|
-
);
|
|
846
|
-
|
|
847
|
-
CREATE TABLE source_anchor_status (
|
|
848
|
-
anchor_id TEXT PRIMARY KEY,
|
|
849
|
-
status TEXT NOT NULL,
|
|
850
|
-
confidence REAL NOT NULL,
|
|
851
|
-
identity_signal TEXT NOT NULL,
|
|
852
|
-
location_signal TEXT NOT NULL,
|
|
853
|
-
symbol_signal TEXT NOT NULL,
|
|
854
|
-
body_signal TEXT NOT NULL,
|
|
855
|
-
metadata_signal TEXT NOT NULL,
|
|
856
|
-
semantic_signal TEXT NOT NULL,
|
|
857
|
-
risk_signal TEXT NOT NULL,
|
|
858
|
-
navigation_only INTEGER NOT NULL,
|
|
859
|
-
can_instruct_agent INTEGER NOT NULL
|
|
860
|
-
);
|
|
861
|
-
`);
|
|
862
|
-
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
863
|
-
database.run(`
|
|
864
|
-
CREATE VIRTUAL TABLE search_documents_fts USING fts5(
|
|
865
|
-
path UNINDEXED,
|
|
866
|
-
type UNINDEXED,
|
|
867
|
-
title,
|
|
868
|
-
sections,
|
|
869
|
-
terms,
|
|
870
|
-
snippet
|
|
871
|
-
);
|
|
872
|
-
|
|
873
|
-
CREATE VIRTUAL TABLE search_skills_fts USING fts5(
|
|
874
|
-
name UNINDEXED,
|
|
875
|
-
path UNINDEXED,
|
|
876
|
-
title
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
CREATE VIRTUAL TABLE search_skill_routes_fts USING fts5(
|
|
880
|
-
route_key UNINDEXED,
|
|
881
|
-
skill_name UNINDEXED,
|
|
882
|
-
skill_path UNINDEXED,
|
|
883
|
-
trigger,
|
|
884
|
-
required_input,
|
|
885
|
-
edit_scope,
|
|
886
|
-
risk,
|
|
887
|
-
verification_intents,
|
|
888
|
-
expected_output
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
CREATE VIRTUAL TABLE search_command_intents_fts USING fts5(
|
|
892
|
-
name UNINDEXED,
|
|
893
|
-
status UNINDEXED,
|
|
894
|
-
lifecycle UNINDEXED,
|
|
895
|
-
run_policy UNINDEXED,
|
|
896
|
-
description,
|
|
897
|
-
effects
|
|
898
|
-
);
|
|
899
|
-
|
|
900
|
-
CREATE VIRTUAL TABLE search_source_anchors_fts USING fts5(
|
|
901
|
-
id UNINDEXED,
|
|
902
|
-
path UNINDEXED,
|
|
903
|
-
purpose,
|
|
904
|
-
search_terms,
|
|
905
|
-
invariant,
|
|
906
|
-
risk
|
|
907
|
-
);
|
|
908
|
-
`);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
function insertDocumentTerm(database, documentPath, term, source) {
|
|
912
|
-
const normalized = normalizeSearchText(term ?? '');
|
|
913
|
-
if (normalized.length === 0) {
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
database.run('INSERT OR IGNORE INTO document_terms (document_path, term, source) VALUES (?, ?, ?)', [
|
|
917
|
-
documentPath,
|
|
918
|
-
normalized,
|
|
919
|
-
source,
|
|
920
|
-
]);
|
|
921
|
-
}
|
|
922
|
-
function insertSearchNgrams(database, targetKind, targetKey, values, source) {
|
|
923
|
-
for (const gram of buildSearchNgrams(values)) {
|
|
924
|
-
database.run('INSERT OR IGNORE INTO search_ngrams (target_kind, target_key, gram, source) VALUES (?, ?, ?, ?)', [targetKind, targetKey, gram, source]);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
function insertPathSurfaceReasons(database, ruleId, reasonKind, values) {
|
|
928
|
-
values.forEach((value, index) => {
|
|
929
|
-
database.run('INSERT INTO path_surface_reasons (rule_id, reason_kind, reason, ordinal) VALUES (?, ?, ?, ?)', [ruleId, reasonKind, value, index + 1]);
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
function populatePathSurfaceReadModel(database) {
|
|
933
|
-
for (const rule of listChangeClassificationRuleDescriptors()) {
|
|
934
|
-
database.run('INSERT INTO path_surfaces (rule_id, pattern_kind, pattern, pattern_flags, surface_kind, category, is_public_surface, update_policy) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
935
|
-
rule.id,
|
|
936
|
-
rule.patternKind,
|
|
937
|
-
rule.pattern,
|
|
938
|
-
rule.patternFlags,
|
|
939
|
-
rule.surface.kind,
|
|
940
|
-
rule.surface.category,
|
|
941
|
-
rule.surface.isPublicSurface ? 1 : 0,
|
|
942
|
-
rule.surface.updatePolicy,
|
|
943
|
-
]);
|
|
944
|
-
insertPathSurfaceReasons(database, rule.id, 'change_kind', rule.changeKinds);
|
|
945
|
-
insertPathSurfaceReasons(database, rule.id, 'validation_reason', rule.surface.validationReasons);
|
|
946
|
-
insertPathSurfaceReasons(database, rule.id, 'affected_contract', rule.surface.affectedContracts);
|
|
947
|
-
insertPathSurfaceReasons(database, rule.id, 'drift_check', rule.surface.driftChecks);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
function skillRouteKey(route) {
|
|
951
|
-
return `${route.skillName}\u0000${route.trigger}`;
|
|
952
|
-
}
|
|
953
|
-
function populateSearchTables(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors) {
|
|
954
|
-
for (const document of documents) {
|
|
955
|
-
const documentTerms = queryRows(database, 'SELECT term FROM document_terms WHERE document_path = ? ORDER BY term', [
|
|
956
|
-
document.path,
|
|
957
|
-
]).map((row) => toSearchString(row.term));
|
|
958
|
-
insertSearchNgrams(database, 'document', document.path, [
|
|
959
|
-
document.path,
|
|
960
|
-
document.type,
|
|
961
|
-
document.title,
|
|
962
|
-
document.sections.join(' '),
|
|
963
|
-
documentTerms.join(' '),
|
|
964
|
-
document.contentSnippet,
|
|
965
|
-
], 'workflow_document');
|
|
966
|
-
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
967
|
-
database.run('INSERT INTO search_documents_fts (path, type, title, sections, terms, snippet) VALUES (?, ?, ?, ?, ?, ?)', [
|
|
968
|
-
document.path,
|
|
969
|
-
document.type,
|
|
970
|
-
document.title,
|
|
971
|
-
document.sections.join(' '),
|
|
972
|
-
documentTerms.join(' '),
|
|
973
|
-
document.contentSnippet,
|
|
974
|
-
]);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
for (const skill of skills) {
|
|
978
|
-
insertSearchNgrams(database, 'skill', skill.name, [skill.name, skill.path, skill.title], 'skill');
|
|
979
|
-
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
980
|
-
database.run('INSERT INTO search_skills_fts (name, path, title) VALUES (?, ?, ?)', [
|
|
981
|
-
skill.name,
|
|
982
|
-
skill.path,
|
|
983
|
-
skill.title,
|
|
984
|
-
]);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
for (const route of skillRoutes) {
|
|
988
|
-
const verificationIntents = route.verificationIntents.join(' ');
|
|
989
|
-
insertSearchNgrams(database, 'skill_route', skillRouteKey(route), [
|
|
990
|
-
skillRouteKey(route),
|
|
991
|
-
route.skillName,
|
|
992
|
-
route.skillPath,
|
|
993
|
-
route.trigger,
|
|
994
|
-
route.requiredInput,
|
|
995
|
-
route.editScope,
|
|
996
|
-
route.risk,
|
|
997
|
-
verificationIntents,
|
|
998
|
-
route.expectedOutput,
|
|
999
|
-
], 'skill_route');
|
|
1000
|
-
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
1001
|
-
database.run('INSERT INTO search_skill_routes_fts (route_key, skill_name, skill_path, trigger, required_input, edit_scope, risk, verification_intents, expected_output) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1002
|
-
skillRouteKey(route),
|
|
1003
|
-
route.skillName,
|
|
1004
|
-
route.skillPath,
|
|
1005
|
-
route.trigger,
|
|
1006
|
-
route.requiredInput,
|
|
1007
|
-
route.editScope,
|
|
1008
|
-
route.risk,
|
|
1009
|
-
verificationIntents,
|
|
1010
|
-
route.expectedOutput,
|
|
1011
|
-
]);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
for (const intent of commandIntents) {
|
|
1015
|
-
const effects = intent.effects
|
|
1016
|
-
.flatMap((effect) => [effect.lock, effect.path ?? '', effect.mode, effect.access, effect.concurrency])
|
|
1017
|
-
.join(' ');
|
|
1018
|
-
insertSearchNgrams(database, 'command_intent', intent.name, [intent.name, intent.status, intent.lifecycle ?? '', intent.runPolicy ?? '', intent.description ?? '', effects], 'command_intent');
|
|
1019
|
-
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
1020
|
-
database.run('INSERT INTO search_command_intents_fts (name, status, lifecycle, run_policy, description, effects) VALUES (?, ?, ?, ?, ?, ?)', [intent.name, intent.status, intent.lifecycle, intent.runPolicy, intent.description, effects]);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
for (const anchor of sourceAnchors) {
|
|
1024
|
-
insertSearchNgrams(database, 'source_anchor', anchor.id, [
|
|
1025
|
-
anchor.id,
|
|
1026
|
-
anchor.path,
|
|
1027
|
-
anchor.purpose ?? '',
|
|
1028
|
-
anchor.search.join(' '),
|
|
1029
|
-
anchor.invariant ?? '',
|
|
1030
|
-
anchor.risk.join(' '),
|
|
1031
|
-
], 'source_anchor');
|
|
1032
|
-
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
1033
|
-
database.run('INSERT INTO search_source_anchors_fts (id, path, purpose, search_terms, invariant, risk) VALUES (?, ?, ?, ?, ?, ?)', [
|
|
1034
|
-
anchor.id,
|
|
1035
|
-
anchor.path,
|
|
1036
|
-
anchor.purpose,
|
|
1037
|
-
anchor.search.join(' '),
|
|
1038
|
-
anchor.invariant,
|
|
1039
|
-
anchor.risk.join(' '),
|
|
1040
|
-
]);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
function populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, indexMode, sourceScopeHash, sourceIndexEnabled, indexedAt) {
|
|
1045
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['schema_version', LOCAL_INDEX_SCHEMA_VERSION]);
|
|
1046
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['parser_version', LOCAL_INDEX_PARSER_VERSION]);
|
|
1047
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['content_mode', LOCAL_INDEX_CONTENT_MODE]);
|
|
1048
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1049
|
-
'store_full_content',
|
|
1050
|
-
String(LOCAL_INDEX_STORE_FULL_CONTENT),
|
|
1051
|
-
]);
|
|
1052
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1053
|
-
'max_snippet_bytes_per_document',
|
|
1054
|
-
String(MAX_SNIPPET_BYTES_PER_DOCUMENT),
|
|
1055
|
-
]);
|
|
1056
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['search_backend', capabilities.backend]);
|
|
1057
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1058
|
-
'search_fts5_available',
|
|
1059
|
-
String(capabilities.fts5Available),
|
|
1060
|
-
]);
|
|
1061
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['source_scope_hash', sourceScopeHash]);
|
|
1062
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['source_index_enabled', String(sourceIndexEnabled)]);
|
|
1063
|
-
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['index_mode', indexMode]);
|
|
1064
|
-
for (const indexedFile of indexedFiles) {
|
|
1065
|
-
database.run('INSERT INTO indexed_files (path, source_scope, size_bytes, mtime_ms, content_hash, indexed_at, index_mode, parser_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1066
|
-
indexedFile.path,
|
|
1067
|
-
indexedFile.sourceScope,
|
|
1068
|
-
indexedFile.sizeBytes,
|
|
1069
|
-
indexedFile.mtimeMs,
|
|
1070
|
-
indexedFile.contentHash,
|
|
1071
|
-
indexedAt,
|
|
1072
|
-
indexMode,
|
|
1073
|
-
LOCAL_INDEX_PARSER_VERSION,
|
|
1074
|
-
]);
|
|
1075
|
-
}
|
|
1076
|
-
for (const document of documents) {
|
|
1077
|
-
database.run('INSERT INTO documents (path, type, title, locale, revision, content_hash, content_snippet) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
|
1078
|
-
document.path,
|
|
1079
|
-
document.type,
|
|
1080
|
-
document.title,
|
|
1081
|
-
document.locale,
|
|
1082
|
-
document.revision,
|
|
1083
|
-
document.contentHash,
|
|
1084
|
-
document.contentSnippet,
|
|
1085
|
-
]);
|
|
1086
|
-
document.sections.forEach((heading, index) => {
|
|
1087
|
-
database.run('INSERT INTO sections (document_path, ordinal, heading) VALUES (?, ?, ?)', [
|
|
1088
|
-
document.path,
|
|
1089
|
-
index + 1,
|
|
1090
|
-
heading,
|
|
1091
|
-
]);
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
for (const skill of skills) {
|
|
1095
|
-
database.run('INSERT INTO skills (name, path, title) VALUES (?, ?, ?)', [skill.name, skill.path, skill.title]);
|
|
1096
|
-
}
|
|
1097
|
-
for (const route of skillRoutes) {
|
|
1098
|
-
const verificationIntents = route.verificationIntents.join(', ');
|
|
1099
|
-
database.run('INSERT INTO skill_routes (skill_name, skill_path, trigger, required_input, edit_scope, risk, verification_intents, expected_output) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1100
|
-
route.skillName,
|
|
1101
|
-
route.skillPath,
|
|
1102
|
-
route.trigger,
|
|
1103
|
-
route.requiredInput,
|
|
1104
|
-
route.editScope,
|
|
1105
|
-
route.risk,
|
|
1106
|
-
verificationIntents,
|
|
1107
|
-
route.expectedOutput,
|
|
1108
|
-
]);
|
|
1109
|
-
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.skillName, 'skill_route_name');
|
|
1110
|
-
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.trigger, 'skill_route_trigger');
|
|
1111
|
-
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.risk, 'skill_route_risk');
|
|
1112
|
-
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.requiredInput, 'skill_route_required_input');
|
|
1113
|
-
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.editScope, 'skill_route_edit_scope');
|
|
1114
|
-
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', verificationIntents, 'skill_route_verification_intents');
|
|
1115
|
-
}
|
|
1116
|
-
for (const intent of commandIntents) {
|
|
1117
|
-
database.run('INSERT INTO command_intents (name, status, lifecycle, run_policy, description) VALUES (?, ?, ?, ?, ?)', [intent.name, intent.status, intent.lifecycle, intent.runPolicy, intent.description]);
|
|
1118
|
-
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.name, 'command_intent');
|
|
1119
|
-
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.status, 'command_status');
|
|
1120
|
-
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.lifecycle, 'command_lifecycle');
|
|
1121
|
-
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.runPolicy, 'command_run_policy');
|
|
1122
|
-
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.description, 'command_description');
|
|
1123
|
-
for (const effect of intent.effects) {
|
|
1124
|
-
database.run('INSERT INTO command_effects (intent, source, access, mode, path, lock, concurrency) VALUES (?, ?, ?, ?, ?, ?, ?)', [effect.intent, effect.source, effect.access, effect.mode, effect.path, effect.lock, effect.concurrency]);
|
|
1125
|
-
if (effect.path !== null) {
|
|
1126
|
-
insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.path, 'command_effect_path');
|
|
1127
|
-
}
|
|
1128
|
-
insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.lock, 'command_effect_lock');
|
|
1129
|
-
insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.mode, 'command_effect_mode');
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
for (const anchor of sourceAnchors) {
|
|
1133
|
-
database.run('INSERT INTO source_anchors (id, path, line_start, purpose, search_terms, invariant, risk, navigation_only, can_instruct_agent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1134
|
-
anchor.id,
|
|
1135
|
-
anchor.path,
|
|
1136
|
-
anchor.lineStart,
|
|
1137
|
-
anchor.purpose,
|
|
1138
|
-
anchor.search.join(', '),
|
|
1139
|
-
anchor.invariant,
|
|
1140
|
-
anchor.risk.join(', '),
|
|
1141
|
-
anchor.navigationOnly ? 1 : 0,
|
|
1142
|
-
anchor.canInstructAgent ? 1 : 0,
|
|
1143
|
-
]);
|
|
1144
|
-
database.run('INSERT INTO source_anchor_fingerprints (anchor_id, path, line_start, anchor_metadata_hash, anchor_text_hash, context_hash, search_terms_hash, invariant_hash, risk_hash, symbol_kind, symbol_name, symbol_exported, signature_hash, body_hash, symbol_start_line, symbol_end_line) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1145
|
-
anchor.id,
|
|
1146
|
-
anchor.path,
|
|
1147
|
-
anchor.lineStart,
|
|
1148
|
-
anchor.fingerprint.anchorMetadataHash,
|
|
1149
|
-
anchor.fingerprint.anchorTextHash,
|
|
1150
|
-
anchor.fingerprint.contextHash,
|
|
1151
|
-
anchor.fingerprint.searchTermsHash,
|
|
1152
|
-
anchor.fingerprint.invariantHash,
|
|
1153
|
-
anchor.fingerprint.riskHash,
|
|
1154
|
-
anchor.fingerprint.symbol.kind,
|
|
1155
|
-
anchor.fingerprint.symbol.name,
|
|
1156
|
-
anchor.fingerprint.symbol.exported ? 1 : 0,
|
|
1157
|
-
anchor.fingerprint.symbol.signatureHash,
|
|
1158
|
-
anchor.fingerprint.symbol.bodyHash,
|
|
1159
|
-
anchor.fingerprint.symbol.startLine,
|
|
1160
|
-
anchor.fingerprint.symbol.endLine,
|
|
1161
|
-
]);
|
|
1162
|
-
database.run('INSERT INTO source_anchor_status (anchor_id, status, confidence, identity_signal, location_signal, symbol_signal, body_signal, metadata_signal, semantic_signal, risk_signal, navigation_only, can_instruct_agent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1163
|
-
anchor.id,
|
|
1164
|
-
anchor.status,
|
|
1165
|
-
anchor.confidence,
|
|
1166
|
-
anchor.signals.identity,
|
|
1167
|
-
anchor.signals.location,
|
|
1168
|
-
anchor.signals.symbol,
|
|
1169
|
-
anchor.signals.body,
|
|
1170
|
-
anchor.signals.metadata,
|
|
1171
|
-
anchor.signals.semantic,
|
|
1172
|
-
anchor.signals.risk,
|
|
1173
|
-
anchor.navigationOnly ? 1 : 0,
|
|
1174
|
-
anchor.canInstructAgent ? 1 : 0,
|
|
1175
|
-
]);
|
|
1176
|
-
}
|
|
1177
|
-
populatePathSurfaceReadModel(database);
|
|
1178
|
-
populateSearchTables(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors);
|
|
1179
|
-
}
|
|
1180
|
-
function indexedFilesMatch(database, currentFiles) {
|
|
1181
|
-
const rows = queryRows(database, 'SELECT path, source_scope, content_hash, parser_version FROM indexed_files ORDER BY path');
|
|
1182
|
-
if (rows.length !== currentFiles.length) {
|
|
1183
|
-
return false;
|
|
1184
|
-
}
|
|
1185
|
-
const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
|
|
1186
|
-
for (const row of rows) {
|
|
1187
|
-
const storedPath = toSearchString(row.path);
|
|
1188
|
-
const current = currentByPath.get(storedPath);
|
|
1189
|
-
if (!current) {
|
|
1190
|
-
return false;
|
|
1191
|
-
}
|
|
1192
|
-
if (toSearchString(row.source_scope) !== current.sourceScope ||
|
|
1193
|
-
toSearchString(row.content_hash) !== current.contentHash ||
|
|
1194
|
-
toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
|
|
1195
|
-
return false;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
return true;
|
|
1199
|
-
}
|
|
1200
|
-
async function readIncrementalReuseDecision(SQL, databasePath, currentFiles, sourceScopeHash) {
|
|
1201
|
-
if (!existsSync(databasePath)) {
|
|
1202
|
-
return { reusable: false, rebuildReason: 'missing_index', capabilities: null };
|
|
1203
|
-
}
|
|
1204
|
-
let database;
|
|
1205
|
-
try {
|
|
1206
|
-
database = new SQL.Database(readFileSync(databasePath));
|
|
1207
|
-
if (readStoredSchemaVersion(database) !== LOCAL_INDEX_SCHEMA_VERSION) {
|
|
1208
|
-
return { reusable: false, rebuildReason: 'schema_version_mismatch', capabilities: null };
|
|
1209
|
-
}
|
|
1210
|
-
if (readMetadataValue(database, 'parser_version') !== LOCAL_INDEX_PARSER_VERSION) {
|
|
1211
|
-
return { reusable: false, rebuildReason: 'parser_version_mismatch', capabilities: null };
|
|
1212
|
-
}
|
|
1213
|
-
if (readMetadataValue(database, 'source_scope_hash') !== sourceScopeHash) {
|
|
1214
|
-
return { reusable: false, rebuildReason: 'source_scope_mismatch', capabilities: null };
|
|
1215
|
-
}
|
|
1216
|
-
if (!hasTable(database, 'indexed_files')) {
|
|
1217
|
-
return { reusable: false, rebuildReason: 'indexed_files_missing', capabilities: null };
|
|
1218
|
-
}
|
|
1219
|
-
if (!indexedFilesMatch(database, currentFiles)) {
|
|
1220
|
-
return { reusable: false, rebuildReason: 'file_fingerprint_mismatch', capabilities: null };
|
|
1221
|
-
}
|
|
1222
|
-
return {
|
|
1223
|
-
reusable: true,
|
|
1224
|
-
rebuildReason: null,
|
|
1225
|
-
capabilities: readStoredSearchCapabilities(database),
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
catch {
|
|
1229
|
-
return { reusable: false, rebuildReason: 'unreadable_index', capabilities: null };
|
|
1230
|
-
}
|
|
1231
|
-
finally {
|
|
1232
|
-
database?.close();
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
/**
|
|
1236
|
-
* mf:anchor cli.index.create
|
|
1237
|
-
* purpose: Build the local SQLite index for workflow documents and optional source anchors.
|
|
1238
|
-
* search: mf index, local index, sqlite, source anchors, workflow documents
|
|
1239
|
-
* invariant: Source anchors are indexed only when requested by CLI flag or local index configuration.
|
|
1240
|
-
* risk: cache, config
|
|
1241
|
-
*/
|
|
1242
|
-
export async function createLocalIndex(projectRoot, options = {}) {
|
|
1243
|
-
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
1244
|
-
const dryRun = options.dryRun === true;
|
|
1245
|
-
const incremental = options.incremental === true;
|
|
1246
|
-
const indexMode = incremental ? 'incremental' : 'full';
|
|
1247
|
-
const documents = collectDocuments(projectRoot);
|
|
1248
|
-
const skills = collectSkills(documents);
|
|
1249
|
-
const skillRoutes = collectSkillRoutes(projectRoot);
|
|
1250
|
-
const commandIntents = collectCommandIntents(projectRoot);
|
|
1251
|
-
const sourceConfig = readLocalIndexSourceConfig(projectRoot);
|
|
1252
|
-
const includeSource = options.includeSource === true || sourceConfig.enabledByDefault;
|
|
1253
|
-
const sourceScopeHash = getSourceScopeHash(includeSource, sourceConfig);
|
|
1254
|
-
const previousSourceAnchors = includeSource
|
|
1255
|
-
? await readPreviousSourceAnchorSnapshots(databasePath).catch(() => [])
|
|
1256
|
-
: [];
|
|
1257
|
-
const sourceAnchors = includeSource
|
|
1258
|
-
? collectSourceAnchorIndexRecords(projectRoot, previousSourceAnchors, {
|
|
1259
|
-
...sourceConfig,
|
|
1260
|
-
excludeGeneratedOrVendor: true,
|
|
1261
|
-
})
|
|
1262
|
-
: [];
|
|
1263
|
-
const indexedFiles = collectIndexedFileRecords(projectRoot, documents, sourceAnchors);
|
|
1264
|
-
let capabilities = searchCapabilities(false);
|
|
1265
|
-
let reusedExisting = false;
|
|
1266
|
-
let rebuildReason = null;
|
|
1267
|
-
const SQL = await loadSqlJs();
|
|
1268
|
-
const capabilityDatabase = new SQL.Database();
|
|
1269
|
-
capabilities = detectLocalSearchCapabilities(capabilityDatabase);
|
|
1270
|
-
capabilityDatabase.close();
|
|
1271
|
-
if (incremental) {
|
|
1272
|
-
const reuseDecision = await readIncrementalReuseDecision(SQL, databasePath, indexedFiles, sourceScopeHash);
|
|
1273
|
-
reusedExisting = reuseDecision.reusable;
|
|
1274
|
-
rebuildReason = reuseDecision.rebuildReason;
|
|
1275
|
-
capabilities = reuseDecision.capabilities ?? capabilities;
|
|
1276
|
-
}
|
|
1277
|
-
if (!dryRun && !reusedExisting) {
|
|
1278
|
-
const database = new SQL.Database();
|
|
1279
|
-
createSchema(database, capabilities);
|
|
1280
|
-
populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, indexMode, sourceScopeHash, includeSource, new Date().toISOString());
|
|
1281
|
-
mkdirSync(path.dirname(databasePath), { recursive: true });
|
|
1282
|
-
writeFileSync(databasePath, database.export());
|
|
1283
|
-
database.close();
|
|
1284
|
-
}
|
|
1285
|
-
return {
|
|
1286
|
-
schema_version: LOCAL_INDEX_SCHEMA_VERSION,
|
|
1287
|
-
command: 'index',
|
|
1288
|
-
ok: true,
|
|
1289
|
-
mustflow_root: path.resolve(projectRoot),
|
|
1290
|
-
database_path: databasePath,
|
|
1291
|
-
dry_run: dryRun,
|
|
1292
|
-
wrote_files: !dryRun && !reusedExisting,
|
|
1293
|
-
index_mode: indexMode,
|
|
1294
|
-
reused_existing: reusedExisting,
|
|
1295
|
-
rebuild_reason: rebuildReason,
|
|
1296
|
-
document_count: documents.length,
|
|
1297
|
-
skill_count: skills.length,
|
|
1298
|
-
skill_route_count: skillRoutes.length,
|
|
1299
|
-
command_intent_count: commandIntents.length,
|
|
1300
|
-
command_effect_count: commandIntents.reduce((count, intent) => count + intent.effects.length, 0),
|
|
1301
|
-
source_index_enabled: includeSource,
|
|
1302
|
-
source_anchor_count: sourceAnchors.length,
|
|
1303
|
-
search_backend: capabilities.backend,
|
|
1304
|
-
search_fts5_available: capabilities.fts5Available,
|
|
1305
|
-
content_mode: LOCAL_INDEX_CONTENT_MODE,
|
|
1306
|
-
store_full_content: LOCAL_INDEX_STORE_FULL_CONTENT,
|
|
1307
|
-
max_snippet_bytes_per_document: MAX_SNIPPET_BYTES_PER_DOCUMENT,
|
|
1308
|
-
indexed_file_count: indexedFiles.length,
|
|
1309
|
-
indexed_paths: documents.map((document) => document.path),
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
function readStoredSchemaVersion(database) {
|
|
1313
|
-
return readMetadataValue(database, 'schema_version');
|
|
1314
|
-
}
|
|
1315
|
-
function getStalePaths(projectRoot, database) {
|
|
1316
|
-
const schemaVersion = readStoredSchemaVersion(database);
|
|
1317
|
-
if (schemaVersion !== LOCAL_INDEX_SCHEMA_VERSION) {
|
|
1318
|
-
return ['.mustflow/cache/mustflow.sqlite'];
|
|
1319
|
-
}
|
|
1320
|
-
if (hasTable(database, 'indexed_files')) {
|
|
1321
|
-
const stalePaths = new Set();
|
|
1322
|
-
const indexedRows = queryRows(database, 'SELECT path, source_scope, content_hash FROM indexed_files');
|
|
1323
|
-
const indexedPaths = new Set(indexedRows.map((row) => toSearchString(row.path)));
|
|
1324
|
-
for (const row of indexedRows) {
|
|
1325
|
-
const indexedPath = toSearchString(row.path);
|
|
1326
|
-
const sourceScope = toSearchString(row.source_scope) === 'source_anchor' ? 'source_anchor' : 'workflow';
|
|
1327
|
-
try {
|
|
1328
|
-
const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
|
|
1329
|
-
if (current.contentHash !== toSearchString(row.content_hash)) {
|
|
1330
|
-
stalePaths.add(indexedPath);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
catch {
|
|
1334
|
-
stalePaths.add(indexedPath);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
for (const document of collectDocuments(projectRoot)) {
|
|
1338
|
-
if (!indexedPaths.has(document.path)) {
|
|
1339
|
-
stalePaths.add(document.path);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
return Array.from(stalePaths).sort((left, right) => left.localeCompare(right));
|
|
1343
|
-
}
|
|
1344
|
-
const indexedRows = queryRows(database, 'SELECT path, content_hash FROM documents');
|
|
1345
|
-
const indexedHashes = new Map(indexedRows.map((row) => [toSearchString(row.path), toSearchString(row.content_hash)]));
|
|
1346
|
-
const currentDocuments = collectDocuments(projectRoot);
|
|
1347
|
-
const currentHashes = new Map(currentDocuments.map((document) => [document.path, document.contentHash]));
|
|
1348
|
-
const stalePaths = new Set();
|
|
1349
|
-
for (const [indexedPath, indexedHash] of indexedHashes) {
|
|
1350
|
-
if (currentHashes.get(indexedPath) !== indexedHash) {
|
|
1351
|
-
stalePaths.add(indexedPath);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
for (const currentPath of currentHashes.keys()) {
|
|
1355
|
-
if (!indexedHashes.has(currentPath)) {
|
|
1356
|
-
stalePaths.add(currentPath);
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
return Array.from(stalePaths).sort((left, right) => left.localeCompare(right));
|
|
1360
|
-
}
|
|
1361
|
-
function mapCommandLockConflict(row, intent) {
|
|
1362
|
-
const targetIsLeft = toSearchString(row.left_intent) === intent;
|
|
1363
|
-
const targetPrefix = targetIsLeft ? 'left' : 'right';
|
|
1364
|
-
const otherPrefix = targetIsLeft ? 'right' : 'left';
|
|
1365
|
-
return {
|
|
1366
|
-
intent: toSearchString(row[`${otherPrefix}_intent`]),
|
|
1367
|
-
lock: toSearchString(row.lock),
|
|
1368
|
-
paths: splitIndexedList(row[`${targetPrefix}_paths`]),
|
|
1369
|
-
modes: splitIndexedList(row[`${targetPrefix}_modes`]),
|
|
1370
|
-
concurrencies: splitIndexedList(row[`${targetPrefix}_concurrencies`]),
|
|
1371
|
-
conflictingPaths: splitIndexedList(row[`${otherPrefix}_paths`]),
|
|
1372
|
-
conflictingModes: splitIndexedList(row[`${otherPrefix}_modes`]),
|
|
1373
|
-
conflictingConcurrencies: splitIndexedList(row[`${otherPrefix}_concurrencies`]),
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* mf:anchor cli.index.command-effect-graph
|
|
1378
|
-
* purpose: Read command-effect lock and conflict explanations from the local SQLite index.
|
|
1379
|
-
* search: mf explain command, command locks, local index, sqlite graph
|
|
1380
|
-
* invariant: Indexed command-effect rows explain current commands.toml only when the index is fresh and never grant command authority.
|
|
1381
|
-
* risk: cache, config
|
|
1382
|
-
*/
|
|
1383
|
-
function queryLocalCommandEffectGraph(databasePath, database, intent) {
|
|
1384
|
-
const writeLocks = queryRows(database, `
|
|
1385
|
-
SELECT lock, paths, modes, sources, concurrencies, effect_count
|
|
1386
|
-
FROM command_write_locks
|
|
1387
|
-
WHERE intent = ?
|
|
1388
|
-
ORDER BY lock
|
|
1389
|
-
`, [intent]).map((row) => ({
|
|
1390
|
-
lock: toSearchString(row.lock),
|
|
1391
|
-
paths: splitIndexedList(row.paths),
|
|
1392
|
-
modes: splitIndexedList(row.modes),
|
|
1393
|
-
sources: splitIndexedList(row.sources),
|
|
1394
|
-
concurrencies: splitIndexedList(row.concurrencies),
|
|
1395
|
-
effectCount: typeof row.effect_count === 'number' && Number.isFinite(row.effect_count) ? row.effect_count : 0,
|
|
1396
|
-
}));
|
|
1397
|
-
const lockConflicts = queryRows(database, `
|
|
1398
|
-
SELECT
|
|
1399
|
-
left_intent,
|
|
1400
|
-
right_intent,
|
|
1401
|
-
lock,
|
|
1402
|
-
left_paths,
|
|
1403
|
-
right_paths,
|
|
1404
|
-
left_modes,
|
|
1405
|
-
right_modes,
|
|
1406
|
-
left_concurrencies,
|
|
1407
|
-
right_concurrencies
|
|
1408
|
-
FROM command_lock_conflicts
|
|
1409
|
-
WHERE left_intent = ? OR right_intent = ?
|
|
1410
|
-
ORDER BY lock, left_intent, right_intent
|
|
1411
|
-
`, [intent, intent]).map((row) => mapCommandLockConflict(row, intent));
|
|
1412
|
-
return {
|
|
1413
|
-
source: 'local_index',
|
|
1414
|
-
authority: 'explanation_only',
|
|
1415
|
-
commandAuthority: '.mustflow/config/commands.toml',
|
|
1416
|
-
grantsCommandAuthority: false,
|
|
1417
|
-
status: 'fresh',
|
|
1418
|
-
databasePath,
|
|
1419
|
-
indexFresh: true,
|
|
1420
|
-
stalePaths: [],
|
|
1421
|
-
writeLocks,
|
|
1422
|
-
lockConflicts,
|
|
1423
|
-
refreshHint: null,
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
export async function readLocalCommandEffectGraph(projectRoot, intent) {
|
|
1427
|
-
const graphs = await readLocalCommandEffectGraphs(projectRoot, [intent]);
|
|
1428
|
-
return graphs.get(intent) ?? createCommandEffectGraphStatus(getLocalIndexDatabasePath(projectRoot), 'unreadable');
|
|
1429
|
-
}
|
|
1430
|
-
export async function readLocalCommandEffectGraphs(projectRoot, intents) {
|
|
1431
|
-
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
1432
|
-
const intentNames = [...new Set(intents)];
|
|
1433
|
-
const statusMap = (status, stalePaths = []) => new Map(intentNames.map((intent) => [intent, createCommandEffectGraphStatus(databasePath, status, stalePaths)]));
|
|
1434
|
-
if (!existsSync(databasePath)) {
|
|
1435
|
-
return statusMap('missing');
|
|
1436
|
-
}
|
|
1437
|
-
const SQL = await loadSqlJs();
|
|
1438
|
-
const database = new SQL.Database(readFileSync(databasePath));
|
|
1439
|
-
try {
|
|
1440
|
-
const stalePaths = getStalePaths(projectRoot, database);
|
|
1441
|
-
if (stalePaths.length > 0) {
|
|
1442
|
-
return statusMap('stale', stalePaths);
|
|
1443
|
-
}
|
|
1444
|
-
return new Map(intentNames.map((intent) => [intent, queryLocalCommandEffectGraph(databasePath, database, intent)]));
|
|
1445
|
-
}
|
|
1446
|
-
catch {
|
|
1447
|
-
return statusMap('unreadable');
|
|
1448
|
-
}
|
|
1449
|
-
finally {
|
|
1450
|
-
database.close();
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
function createPathSurfaceReadModelStatus(databasePath, status, inputPath, stalePaths = []) {
|
|
1454
|
-
return {
|
|
1455
|
-
source: 'local_index',
|
|
1456
|
-
status,
|
|
1457
|
-
databasePath,
|
|
1458
|
-
indexFresh: status === 'fresh',
|
|
1459
|
-
stalePaths,
|
|
1460
|
-
inputPath,
|
|
1461
|
-
match: null,
|
|
1462
|
-
refreshHint: status === 'fresh' ? null : 'Run `mf index` to refresh path-surface explanations.',
|
|
1463
|
-
};
|
|
1464
|
-
}
|
|
1465
|
-
function createLocalIndexPromptContextStatus(databasePath, status, stalePaths = [], capabilities = null) {
|
|
1466
|
-
return {
|
|
1467
|
-
source: 'local_index',
|
|
1468
|
-
status,
|
|
1469
|
-
databasePath,
|
|
1470
|
-
indexFresh: status === 'fresh',
|
|
1471
|
-
stalePaths,
|
|
1472
|
-
searchBackend: capabilities?.backend ?? null,
|
|
1473
|
-
searchFts5Available: capabilities?.fts5Available ?? null,
|
|
1474
|
-
refreshHint: status === 'fresh' ? null : 'Run `mf index` to refresh prompt-cache task context local-index metadata.',
|
|
1475
|
-
};
|
|
1476
|
-
}
|
|
1477
|
-
export async function readLocalIndexPromptContext(projectRoot) {
|
|
1478
|
-
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
1479
|
-
if (!existsSync(databasePath)) {
|
|
1480
|
-
return createLocalIndexPromptContextStatus(databasePath, 'missing');
|
|
1481
|
-
}
|
|
1482
|
-
let database;
|
|
1483
|
-
try {
|
|
1484
|
-
const SQL = await loadSqlJs();
|
|
1485
|
-
database = new SQL.Database(readFileSync(databasePath));
|
|
1486
|
-
const capabilities = readStoredSearchCapabilities(database);
|
|
1487
|
-
const stalePaths = getStalePaths(projectRoot, database);
|
|
1488
|
-
if (stalePaths.length > 0) {
|
|
1489
|
-
return createLocalIndexPromptContextStatus(databasePath, 'stale', stalePaths, capabilities);
|
|
1490
|
-
}
|
|
1491
|
-
return createLocalIndexPromptContextStatus(databasePath, 'fresh', [], capabilities);
|
|
1492
|
-
}
|
|
1493
|
-
catch {
|
|
1494
|
-
return createLocalIndexPromptContextStatus(databasePath, 'unreadable');
|
|
1495
|
-
}
|
|
1496
|
-
finally {
|
|
1497
|
-
database?.close();
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
function pathSurfaceReadModelWithMatch(base, match) {
|
|
1501
|
-
return {
|
|
1502
|
-
...base,
|
|
1503
|
-
match,
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
function readPathSurfaceReasonMap(database) {
|
|
1507
|
-
const byRule = new Map();
|
|
1508
|
-
for (const row of queryRows(database, 'SELECT rule_id, reason_kind, reason FROM path_surface_reasons ORDER BY rule_id, reason_kind, ordinal')) {
|
|
1509
|
-
const ruleId = toSearchString(row.rule_id);
|
|
1510
|
-
const reasonKind = toSearchString(row.reason_kind);
|
|
1511
|
-
const reason = toSearchString(row.reason);
|
|
1512
|
-
let reasonsByKind = byRule.get(ruleId);
|
|
1513
|
-
if (!reasonsByKind) {
|
|
1514
|
-
reasonsByKind = new Map();
|
|
1515
|
-
byRule.set(ruleId, reasonsByKind);
|
|
1516
|
-
}
|
|
1517
|
-
const reasons = reasonsByKind.get(reasonKind) ?? [];
|
|
1518
|
-
reasons.push(reason);
|
|
1519
|
-
reasonsByKind.set(reasonKind, reasons);
|
|
1520
|
-
}
|
|
1521
|
-
return byRule;
|
|
1522
|
-
}
|
|
1523
|
-
function readPathSurfaceRuleMatches(database) {
|
|
1524
|
-
const reasons = readPathSurfaceReasonMap(database);
|
|
1525
|
-
return queryRows(database, 'SELECT rule_id, pattern_kind, pattern, pattern_flags, surface_kind, category, is_public_surface, update_policy FROM path_surfaces ORDER BY rowid').map((row) => {
|
|
1526
|
-
const ruleId = toSearchString(row.rule_id);
|
|
1527
|
-
const reasonsByKind = reasons.get(ruleId);
|
|
1528
|
-
const reasonList = (kind) => reasonsByKind?.get(kind) ?? [];
|
|
1529
|
-
return {
|
|
1530
|
-
ruleId,
|
|
1531
|
-
patternKind: toSearchString(row.pattern_kind),
|
|
1532
|
-
pattern: toSearchString(row.pattern),
|
|
1533
|
-
patternFlags: toSearchString(row.pattern_flags),
|
|
1534
|
-
changeKinds: reasonList('change_kind'),
|
|
1535
|
-
surface: {
|
|
1536
|
-
kind: toSearchString(row.surface_kind),
|
|
1537
|
-
category: toSearchString(row.category),
|
|
1538
|
-
isPublicSurface: Number(row.is_public_surface) === 1,
|
|
1539
|
-
validationReasons: reasonList('validation_reason'),
|
|
1540
|
-
affectedContracts: reasonList('affected_contract'),
|
|
1541
|
-
updatePolicy: toSearchString(row.update_policy),
|
|
1542
|
-
driftChecks: reasonList('drift_check'),
|
|
1543
|
-
},
|
|
1544
|
-
};
|
|
1545
|
-
});
|
|
1546
|
-
}
|
|
1547
|
-
function matchPathSurfaceRule(relativePath, rules) {
|
|
1548
|
-
if (!relativePath) {
|
|
1549
|
-
return null;
|
|
1550
|
-
}
|
|
1551
|
-
for (const rule of rules) {
|
|
1552
|
-
try {
|
|
1553
|
-
if (new RegExp(rule.pattern, rule.patternFlags).test(relativePath)) {
|
|
1554
|
-
return rule;
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
catch {
|
|
1558
|
-
continue;
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
return null;
|
|
1562
|
-
}
|
|
1563
|
-
export async function readLocalPathSurfaces(projectRoot, relativePaths) {
|
|
1564
|
-
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
1565
|
-
const normalizedPaths = [...new Set(relativePaths.map((relativePath) => toPosixPath(relativePath)).filter(Boolean))];
|
|
1566
|
-
const statusMap = (status, stalePaths = []) => new Map(normalizedPaths.map((relativePath) => [
|
|
1567
|
-
relativePath,
|
|
1568
|
-
createPathSurfaceReadModelStatus(databasePath, status, relativePath, stalePaths),
|
|
1569
|
-
]));
|
|
1570
|
-
if (!existsSync(databasePath)) {
|
|
1571
|
-
return statusMap('missing');
|
|
1572
|
-
}
|
|
1573
|
-
const SQL = await loadSqlJs();
|
|
1574
|
-
const database = new SQL.Database(readFileSync(databasePath));
|
|
1575
|
-
try {
|
|
1576
|
-
const stalePaths = getStalePaths(projectRoot, database);
|
|
1577
|
-
if (stalePaths.length > 0) {
|
|
1578
|
-
return statusMap('stale', stalePaths);
|
|
1579
|
-
}
|
|
1580
|
-
const rules = readPathSurfaceRuleMatches(database);
|
|
1581
|
-
return new Map(normalizedPaths.map((relativePath) => {
|
|
1582
|
-
const base = createPathSurfaceReadModelStatus(databasePath, 'fresh', relativePath);
|
|
1583
|
-
return [relativePath, pathSurfaceReadModelWithMatch(base, matchPathSurfaceRule(relativePath, rules))];
|
|
1584
|
-
}));
|
|
1585
|
-
}
|
|
1586
|
-
catch {
|
|
1587
|
-
return statusMap('unreadable');
|
|
1588
|
-
}
|
|
1589
|
-
finally {
|
|
1590
|
-
database.close();
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
export async function readLocalPathSurface(projectRoot, relativePath) {
|
|
1594
|
-
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
1595
|
-
const inputPath = relativePath ? toPosixPath(relativePath) : null;
|
|
1596
|
-
if (!inputPath) {
|
|
1597
|
-
if (!existsSync(databasePath)) {
|
|
1598
|
-
return createPathSurfaceReadModelStatus(databasePath, 'missing', null);
|
|
1599
|
-
}
|
|
1600
|
-
const SQL = await loadSqlJs();
|
|
1601
|
-
const database = new SQL.Database(readFileSync(databasePath));
|
|
1602
|
-
try {
|
|
1603
|
-
const stalePaths = getStalePaths(projectRoot, database);
|
|
1604
|
-
return createPathSurfaceReadModelStatus(databasePath, stalePaths.length > 0 ? 'stale' : 'fresh', null, stalePaths);
|
|
1605
|
-
}
|
|
1606
|
-
catch {
|
|
1607
|
-
return createPathSurfaceReadModelStatus(databasePath, 'unreadable', null);
|
|
1608
|
-
}
|
|
1609
|
-
finally {
|
|
1610
|
-
database.close();
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
const surfaces = await readLocalPathSurfaces(projectRoot, [inputPath]);
|
|
1614
|
-
return surfaces.get(inputPath) ?? createPathSurfaceReadModelStatus(databasePath, 'unreadable', inputPath);
|
|
1615
|
-
}
|
|
1616
|
-
function getSectionHeadings(database, documentPath) {
|
|
1617
|
-
return queryRows(database, 'SELECT heading FROM sections WHERE document_path = ? ORDER BY ordinal', [documentPath]).map((row) => toSearchString(row.heading));
|
|
1618
|
-
}
|
|
1619
|
-
function getDocumentTerms(database, documentPath) {
|
|
1620
|
-
return queryRows(database, 'SELECT term FROM document_terms WHERE document_path = ? ORDER BY term', [documentPath]).map((row) => toSearchString(row.term));
|
|
1621
|
-
}
|
|
1622
|
-
function getCommandEffects(database, intent) {
|
|
1623
|
-
return queryRows(database, 'SELECT intent, source, access, mode, path, lock, concurrency FROM command_effects WHERE intent = ? ORDER BY lock, path, mode', [intent]).map((row) => ({
|
|
1624
|
-
intent: toSearchString(row.intent),
|
|
1625
|
-
source: toSearchString(row.source),
|
|
1626
|
-
access: toSearchString(row.access),
|
|
1627
|
-
mode: toSearchString(row.mode),
|
|
1628
|
-
path: row.path === null || row.path === undefined ? null : toSearchString(row.path),
|
|
1629
|
-
lock: toSearchString(row.lock),
|
|
1630
|
-
concurrency: toSearchString(row.concurrency),
|
|
1631
|
-
}));
|
|
1632
|
-
}
|
|
1633
|
-
const EMPTY_INDEXED_SEARCH_MATCHES = {
|
|
1634
|
-
active: false,
|
|
1635
|
-
documents: new Set(),
|
|
1636
|
-
skills: new Set(),
|
|
1637
|
-
skillRoutes: new Set(),
|
|
1638
|
-
commandIntents: new Set(),
|
|
1639
|
-
sourceAnchors: new Set(),
|
|
1640
|
-
};
|
|
1641
|
-
function buildFtsQuery(query) {
|
|
1642
|
-
const tokens = extractSearchTokens(query);
|
|
1643
|
-
if (tokens.length === 0) {
|
|
1644
|
-
return null;
|
|
1645
|
-
}
|
|
1646
|
-
return [...new Set(tokens)].map((token) => `"${token.replaceAll('"', '""')}"`).join(' AND ');
|
|
1647
|
-
}
|
|
1648
|
-
function queryFtsSet(database, sql, ftsQuery, column) {
|
|
1649
|
-
return new Set(queryRows(database, sql, [ftsQuery]).map((row) => toSearchString(row[column])));
|
|
1650
|
-
}
|
|
1651
|
-
function mergeSearchSets(left, right) {
|
|
1652
|
-
return new Set([...left, ...right]);
|
|
1653
|
-
}
|
|
1654
|
-
function mergeIndexedSearchMatches(left, right) {
|
|
1655
|
-
return {
|
|
1656
|
-
active: left.active || right.active,
|
|
1657
|
-
documents: mergeSearchSets(left.documents, right.documents),
|
|
1658
|
-
skills: mergeSearchSets(left.skills, right.skills),
|
|
1659
|
-
skillRoutes: mergeSearchSets(left.skillRoutes, right.skillRoutes),
|
|
1660
|
-
commandIntents: mergeSearchSets(left.commandIntents, right.commandIntents),
|
|
1661
|
-
sourceAnchors: mergeSearchSets(left.sourceAnchors, right.sourceAnchors),
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
function queryNgramSet(database, targetKind, grams) {
|
|
1665
|
-
const placeholders = grams.map(() => '?').join(', ');
|
|
1666
|
-
if (!placeholders) {
|
|
1667
|
-
return new Set();
|
|
1668
|
-
}
|
|
1669
|
-
return new Set(queryRows(database, `SELECT target_key
|
|
1670
|
-
FROM search_ngrams
|
|
1671
|
-
WHERE target_kind = ? AND gram IN (${placeholders})
|
|
1672
|
-
GROUP BY target_key
|
|
1673
|
-
HAVING COUNT(DISTINCT gram) = ?`, [targetKind, ...grams, grams.length]).map((row) => toSearchString(row.target_key)));
|
|
1674
|
-
}
|
|
1675
|
-
function getNgramSearchMatches(database, query) {
|
|
1676
|
-
if (!hasTable(database, 'search_ngrams')) {
|
|
1677
|
-
return EMPTY_INDEXED_SEARCH_MATCHES;
|
|
1678
|
-
}
|
|
1679
|
-
const grams = buildSearchNgrams([query]);
|
|
1680
|
-
if (grams.length === 0) {
|
|
1681
|
-
return EMPTY_INDEXED_SEARCH_MATCHES;
|
|
1682
|
-
}
|
|
1683
|
-
return {
|
|
1684
|
-
active: true,
|
|
1685
|
-
documents: queryNgramSet(database, 'document', grams),
|
|
1686
|
-
skills: queryNgramSet(database, 'skill', grams),
|
|
1687
|
-
skillRoutes: queryNgramSet(database, 'skill_route', grams),
|
|
1688
|
-
commandIntents: queryNgramSet(database, 'command_intent', grams),
|
|
1689
|
-
sourceAnchors: queryNgramSet(database, 'source_anchor', grams),
|
|
1690
|
-
};
|
|
1691
|
-
}
|
|
1692
|
-
function getIndexedSearchMatches(database, query) {
|
|
1693
|
-
const capabilities = readStoredSearchCapabilities(database);
|
|
1694
|
-
const ftsQuery = capabilities.backend === SEARCH_BACKEND_FTS5 ? buildFtsQuery(query) : null;
|
|
1695
|
-
const ngramMatches = getNgramSearchMatches(database, query);
|
|
1696
|
-
if (!ftsQuery) {
|
|
1697
|
-
return ngramMatches;
|
|
1698
|
-
}
|
|
1699
|
-
try {
|
|
1700
|
-
const ftsMatches = {
|
|
1701
|
-
active: true,
|
|
1702
|
-
documents: queryFtsSet(database, 'SELECT path FROM search_documents_fts WHERE search_documents_fts MATCH ?', ftsQuery, 'path'),
|
|
1703
|
-
skills: queryFtsSet(database, 'SELECT name FROM search_skills_fts WHERE search_skills_fts MATCH ?', ftsQuery, 'name'),
|
|
1704
|
-
skillRoutes: queryFtsSet(database, 'SELECT route_key FROM search_skill_routes_fts WHERE search_skill_routes_fts MATCH ?', ftsQuery, 'route_key'),
|
|
1705
|
-
commandIntents: queryFtsSet(database, 'SELECT name FROM search_command_intents_fts WHERE search_command_intents_fts MATCH ?', ftsQuery, 'name'),
|
|
1706
|
-
sourceAnchors: queryFtsSet(database, 'SELECT id FROM search_source_anchors_fts WHERE search_source_anchors_fts MATCH ?', ftsQuery, 'id'),
|
|
1707
|
-
};
|
|
1708
|
-
return mergeIndexedSearchMatches(ftsMatches, ngramMatches);
|
|
1709
|
-
}
|
|
1710
|
-
catch {
|
|
1711
|
-
return ngramMatches;
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
function matchesIndexedOrTableScan(fields, query, indexedMatches, matchSet, key) {
|
|
1715
|
-
return (indexedMatches.active && matchSet.has(key)) || isMatched(fields, query);
|
|
1716
|
-
}
|
|
1717
|
-
function scoreIndexedOrTableScan(primaryFields, secondaryFields, query, indexedMatches, matchSet, key) {
|
|
1718
|
-
const tableScore = scoreMatch(primaryFields, secondaryFields, query);
|
|
1719
|
-
return indexedMatches.active && matchSet.has(key) ? Math.max(tableScore, 20) : tableScore;
|
|
1720
|
-
}
|
|
1721
|
-
/**
|
|
1722
|
-
* mf:anchor cli.search.local-index
|
|
1723
|
-
* purpose: Search the local index while preserving workflow authority above source navigation hints.
|
|
1724
|
-
* search: mf search, scope workflow source all, authority rank, navigation only
|
|
1725
|
-
* invariant: Source anchor results remain navigation-only and cannot outrank command or workflow authority.
|
|
1726
|
-
* risk: cache, config
|
|
1727
|
-
*/
|
|
1728
|
-
export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
1729
|
-
const normalizedQuery = normalizeSearchText(query);
|
|
1730
|
-
const limit = Math.max(1, Math.min(options.limit ?? 10, 50));
|
|
1731
|
-
const scope = options.scope ?? 'workflow';
|
|
1732
|
-
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
1733
|
-
if (!existsSync(databasePath)) {
|
|
1734
|
-
throw new Error('Local mustflow index not found. Run `mf index` before searching.');
|
|
1735
|
-
}
|
|
1736
|
-
if (normalizedQuery.length === 0) {
|
|
1737
|
-
throw new Error('Search query must not be empty.');
|
|
1738
|
-
}
|
|
1739
|
-
const SQL = await loadSqlJs();
|
|
1740
|
-
const database = new SQL.Database(readFileSync(databasePath));
|
|
1741
|
-
const cacheLayers = readCacheLayerSets(projectRoot);
|
|
1742
|
-
let capabilities = searchCapabilities(false);
|
|
1743
|
-
const results = [];
|
|
1744
|
-
try {
|
|
1745
|
-
const stalePaths = getStalePaths(projectRoot, database);
|
|
1746
|
-
capabilities = readStoredSearchCapabilities(database);
|
|
1747
|
-
const indexedMatches = getIndexedSearchMatches(database, normalizedQuery);
|
|
1748
|
-
if (stalePaths.length > 0) {
|
|
1749
|
-
throw new Error(`Local mustflow index is stale: ${stalePaths.join(', ')}. Run \`mf index\` before searching. Refresh command: mf index`);
|
|
1750
|
-
}
|
|
1751
|
-
if (scope === 'workflow' || scope === 'all') {
|
|
1752
|
-
for (const row of queryRows(database, 'SELECT path, type, title, content_snippet FROM documents')) {
|
|
1753
|
-
const pathValue = toSearchString(row.path);
|
|
1754
|
-
const typeValue = toSearchString(row.type);
|
|
1755
|
-
const title = toSearchString(row.title);
|
|
1756
|
-
const contentSnippet = toSearchString(row.content_snippet);
|
|
1757
|
-
const sectionHeadings = getSectionHeadings(database, pathValue);
|
|
1758
|
-
const documentTerms = getDocumentTerms(database, pathValue);
|
|
1759
|
-
const primaryFields = [pathValue, title];
|
|
1760
|
-
const secondaryFields = [typeValue, contentSnippet, ...sectionHeadings, ...documentTerms];
|
|
1761
|
-
const fields = [...primaryFields, ...secondaryFields];
|
|
1762
|
-
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.documents, pathValue)) {
|
|
1763
|
-
continue;
|
|
1764
|
-
}
|
|
1765
|
-
results.push(withCacheHint({
|
|
1766
|
-
kind: 'document',
|
|
1767
|
-
path: pathValue,
|
|
1768
|
-
title,
|
|
1769
|
-
document_type: typeValue,
|
|
1770
|
-
...workflowAuthorityForDocument(typeValue),
|
|
1771
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
1772
|
-
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.documents, pathValue),
|
|
1773
|
-
}, cacheLayers));
|
|
1774
|
-
}
|
|
1775
|
-
for (const row of queryRows(database, 'SELECT name, path, title FROM skills')) {
|
|
1776
|
-
const name = toSearchString(row.name);
|
|
1777
|
-
const pathValue = toSearchString(row.path);
|
|
1778
|
-
const title = toSearchString(row.title);
|
|
1779
|
-
const fields = [name, pathValue, title];
|
|
1780
|
-
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.skills, name)) {
|
|
1781
|
-
continue;
|
|
1782
|
-
}
|
|
1783
|
-
results.push(withCacheHint({
|
|
1784
|
-
kind: 'skill',
|
|
1785
|
-
name,
|
|
1786
|
-
path: pathValue,
|
|
1787
|
-
title,
|
|
1788
|
-
...skillAuthority(),
|
|
1789
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
1790
|
-
score: scoreIndexedOrTableScan([name, pathValue, title], [], normalizedQuery, indexedMatches, indexedMatches.skills, name),
|
|
1791
|
-
}, cacheLayers));
|
|
1792
|
-
}
|
|
1793
|
-
for (const row of queryRows(database, 'SELECT skill_name, skill_path, trigger, required_input, edit_scope, risk, verification_intents, expected_output FROM skill_routes')) {
|
|
1794
|
-
const name = toSearchString(row.skill_name);
|
|
1795
|
-
const pathValue = toSearchString(row.skill_path);
|
|
1796
|
-
const trigger = toSearchString(row.trigger);
|
|
1797
|
-
const requiredInput = toSearchString(row.required_input);
|
|
1798
|
-
const editScope = toSearchString(row.edit_scope);
|
|
1799
|
-
const risk = toSearchString(row.risk);
|
|
1800
|
-
const verificationIntents = splitVerificationIntents(toSearchString(row.verification_intents));
|
|
1801
|
-
const expectedOutput = toSearchString(row.expected_output);
|
|
1802
|
-
const primaryFields = [name, trigger];
|
|
1803
|
-
const secondaryFields = [pathValue, requiredInput, editScope, risk, expectedOutput];
|
|
1804
|
-
const fields = [...primaryFields, ...secondaryFields];
|
|
1805
|
-
const routeKey = skillRouteKey({ skillName: name, trigger });
|
|
1806
|
-
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.skillRoutes, routeKey)) {
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
results.push(withCacheHint({
|
|
1810
|
-
kind: 'skill_route',
|
|
1811
|
-
name,
|
|
1812
|
-
path: pathValue,
|
|
1813
|
-
title: name,
|
|
1814
|
-
route_trigger: trigger,
|
|
1815
|
-
route_risk: risk,
|
|
1816
|
-
verification_intents: verificationIntents,
|
|
1817
|
-
...skillAuthority(),
|
|
1818
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
1819
|
-
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.skillRoutes, routeKey),
|
|
1820
|
-
}, cacheLayers));
|
|
1821
|
-
}
|
|
1822
|
-
for (const row of queryRows(database, 'SELECT name, status, lifecycle, run_policy, description FROM command_intents')) {
|
|
1823
|
-
const name = toSearchString(row.name);
|
|
1824
|
-
const status = toSearchString(row.status);
|
|
1825
|
-
const lifecycle = toSearchString(row.lifecycle);
|
|
1826
|
-
const runPolicy = toSearchString(row.run_policy);
|
|
1827
|
-
const description = toSearchString(row.description);
|
|
1828
|
-
const effects = getCommandEffects(database, name);
|
|
1829
|
-
const effectLocks = [...new Set(effects.map((effect) => effect.lock))].sort((left, right) => left.localeCompare(right));
|
|
1830
|
-
const effectPaths = [
|
|
1831
|
-
...new Set(effects.map((effect) => effect.path).filter((effectPath) => effectPath !== null)),
|
|
1832
|
-
].sort((left, right) => left.localeCompare(right));
|
|
1833
|
-
const effectModes = [...new Set(effects.map((effect) => effect.mode))].sort((left, right) => left.localeCompare(right));
|
|
1834
|
-
const primaryFields = [name];
|
|
1835
|
-
const secondaryFields = [status, lifecycle, runPolicy, description, ...effectLocks, ...effectPaths, ...effectModes];
|
|
1836
|
-
const fields = [...primaryFields, ...secondaryFields];
|
|
1837
|
-
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.commandIntents, name)) {
|
|
1838
|
-
continue;
|
|
1839
|
-
}
|
|
1840
|
-
results.push(withCacheHint({
|
|
1841
|
-
kind: 'command_intent',
|
|
1842
|
-
name,
|
|
1843
|
-
title: description || name,
|
|
1844
|
-
effect_locks: effectLocks,
|
|
1845
|
-
effect_paths: effectPaths,
|
|
1846
|
-
effect_modes: effectModes,
|
|
1847
|
-
...commandIntentAuthority(),
|
|
1848
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
1849
|
-
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.commandIntents, name),
|
|
1850
|
-
}, cacheLayers));
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
if (scope === 'source' || scope === 'all') {
|
|
1854
|
-
for (const row of queryRows(database, 'SELECT source_anchors.id, path, line_start, purpose, search_terms, invariant, risk, source_anchors.navigation_only, source_anchors.can_instruct_agent, status, confidence FROM source_anchors LEFT JOIN source_anchor_status ON source_anchor_status.anchor_id = source_anchors.id')) {
|
|
1855
|
-
const id = toSearchString(row.id);
|
|
1856
|
-
const pathValue = toSearchString(row.path);
|
|
1857
|
-
const purpose = toSearchString(row.purpose);
|
|
1858
|
-
const searchTerms = toSearchString(row.search_terms);
|
|
1859
|
-
const invariant = toSearchString(row.invariant);
|
|
1860
|
-
const risk = toSearchString(row.risk);
|
|
1861
|
-
const primaryFields = [id, pathValue];
|
|
1862
|
-
const secondaryFields = [purpose, searchTerms, invariant, risk];
|
|
1863
|
-
const fields = [...primaryFields, ...secondaryFields];
|
|
1864
|
-
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.sourceAnchors, id)) {
|
|
1865
|
-
continue;
|
|
1866
|
-
}
|
|
1867
|
-
results.push(withCacheHint({
|
|
1868
|
-
kind: 'source_anchor',
|
|
1869
|
-
anchor_id: id,
|
|
1870
|
-
name: id,
|
|
1871
|
-
path: pathValue,
|
|
1872
|
-
line_start: Number(row.line_start),
|
|
1873
|
-
title: purpose || id,
|
|
1874
|
-
risk,
|
|
1875
|
-
...sourceAnchorAuthority(),
|
|
1876
|
-
stale_status: toSearchString(row.status),
|
|
1877
|
-
stale_confidence: Number(row.confidence),
|
|
1878
|
-
match: getMatchSnippet(fields, normalizedQuery),
|
|
1879
|
-
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.sourceAnchors, id),
|
|
1880
|
-
}, cacheLayers));
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
finally {
|
|
1885
|
-
database.close();
|
|
1886
|
-
}
|
|
1887
|
-
const sortedResults = results
|
|
1888
|
-
.sort((left, right) => {
|
|
1889
|
-
if (scope === 'all' && left.authority_rank !== right.authority_rank) {
|
|
1890
|
-
return left.authority_rank - right.authority_rank;
|
|
1891
|
-
}
|
|
1892
|
-
return right.score - left.score || (left.path ?? left.name ?? '').localeCompare(right.path ?? right.name ?? '');
|
|
1893
|
-
})
|
|
1894
|
-
.slice(0, limit);
|
|
1895
|
-
return {
|
|
1896
|
-
schema_version: LOCAL_INDEX_SCHEMA_VERSION,
|
|
1897
|
-
command: 'search',
|
|
1898
|
-
ok: true,
|
|
1899
|
-
mustflow_root: path.resolve(projectRoot),
|
|
1900
|
-
database_path: databasePath,
|
|
1901
|
-
query: normalizedQuery,
|
|
1902
|
-
limit,
|
|
1903
|
-
scope,
|
|
1904
|
-
index_fresh: true,
|
|
1905
|
-
stale_paths: [],
|
|
1906
|
-
search_backend: capabilities.backend,
|
|
1907
|
-
search_fts5_available: capabilities.fts5Available,
|
|
1908
|
-
result_count: sortedResults.length,
|
|
1909
|
-
results: sortedResults,
|
|
1910
|
-
};
|
|
1911
|
-
}
|
|
1
|
+
export * from './local-index/index.js';
|