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
|
@@ -0,0 +1,2951 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isRecord, readCommandContract, readString, readStringArray } from '../command-contract.js';
|
|
5
|
+
import { listFilesRecursive, toPosixPath } from '../filesystem.js';
|
|
6
|
+
import { readTomlFile } from '../toml.js';
|
|
7
|
+
import { collectSourceAnchorIndexRecords, hasHighRiskSourceAnchorRiskTags, } from '../../../core/source-anchor-status.js';
|
|
8
|
+
import { normalizeCommandEffects } from '../../../core/command-effects.js';
|
|
9
|
+
import { listChangeClassificationRuleDescriptors } from '../../../core/change-classification.js';
|
|
10
|
+
import { DEFAULT_DATABASE_RELATIVE_PATH, DEFAULT_PROMPT_CACHE_STABLE_READ, DEFAULT_PROMPT_CACHE_TASK_SOURCES, DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES, INDEX_CONFIG_RELATIVE_PATH, LOCAL_INDEX_CONTENT_MODE, LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS, LOCAL_INDEX_PARSER_VERSION, LOCAL_INDEX_SCHEMA_VERSION, LOCAL_INDEX_STORE_FULL_CONTENT, LATEST_RUN_STATE_RELATIVE_PATH, MAX_SEARCH_MATCH_SNIPPET_CHARS, MAX_SNIPPET_BYTES_PER_DOCUMENT, MUSTFLOW_RELATIVE_PATH, SEARCH_BACKEND_FTS5, SEARCH_BACKEND_TABLE_SCAN, SEARCH_MATCH_CONTEXT_AFTER_CHARS, SEARCH_MATCH_CONTEXT_BEFORE_CHARS, SEARCH_MATCH_TRUNCATION_MARKER, SEARCH_NGRAM_MAX_LENGTH, SEARCH_NGRAM_MIN_LENGTH, TEST_DISABLE_FTS5_ENV, } from './constants.js';
|
|
11
|
+
import { loadSqlJs } from './sql.js';
|
|
12
|
+
export function getLocalIndexDatabasePath(projectRoot) {
|
|
13
|
+
return path.join(projectRoot, ...DEFAULT_DATABASE_RELATIVE_PATH.split('/'));
|
|
14
|
+
}
|
|
15
|
+
function getExistingIndexablePaths(projectRoot) {
|
|
16
|
+
const paths = new Set();
|
|
17
|
+
const addIfExists = (relativePath) => {
|
|
18
|
+
if (existsSync(path.join(projectRoot, ...relativePath.split('/')))) {
|
|
19
|
+
paths.add(relativePath);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
addIfExists('AGENTS.md');
|
|
23
|
+
for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'docs'))) {
|
|
24
|
+
if (relativePath.endsWith('.md')) {
|
|
25
|
+
paths.add(toPosixPath(path.join('.mustflow', 'docs', relativePath)));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'context'))) {
|
|
29
|
+
if (relativePath.endsWith('.md')) {
|
|
30
|
+
paths.add(toPosixPath(path.join('.mustflow', 'context', relativePath)));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'skills'))) {
|
|
34
|
+
if (relativePath === 'INDEX.md' || relativePath.endsWith('/SKILL.md')) {
|
|
35
|
+
paths.add(toPosixPath(path.join('.mustflow', 'skills', relativePath)));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'config'))) {
|
|
39
|
+
if (relativePath.endsWith('.toml')) {
|
|
40
|
+
paths.add(toPosixPath(path.join('.mustflow', 'config', relativePath)));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return Array.from(paths).sort((left, right) => left.localeCompare(right));
|
|
44
|
+
}
|
|
45
|
+
function readText(projectRoot, relativePath) {
|
|
46
|
+
return readFileSync(path.join(projectRoot, ...relativePath.split('/')), 'utf8');
|
|
47
|
+
}
|
|
48
|
+
function readMustflowToml(projectRoot) {
|
|
49
|
+
const mustflowPath = path.join(projectRoot, ...MUSTFLOW_RELATIVE_PATH.split('/'));
|
|
50
|
+
if (!existsSync(mustflowPath)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const parsed = readTomlFile(mustflowPath);
|
|
54
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
55
|
+
}
|
|
56
|
+
function readIndexToml(projectRoot) {
|
|
57
|
+
const indexConfigPath = path.join(projectRoot, ...INDEX_CONFIG_RELATIVE_PATH.split('/'));
|
|
58
|
+
if (!existsSync(indexConfigPath)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const parsed = readTomlFile(indexConfigPath);
|
|
62
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
63
|
+
}
|
|
64
|
+
function readNestedTable(table, key) {
|
|
65
|
+
if (!table || !isRecord(table[key])) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return table[key];
|
|
69
|
+
}
|
|
70
|
+
function readOptionalStringArray(table, key) {
|
|
71
|
+
return table ? readStringArray(table, key) ?? null : null;
|
|
72
|
+
}
|
|
73
|
+
function readBoolean(table, key) {
|
|
74
|
+
const value = table?.[key];
|
|
75
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
76
|
+
}
|
|
77
|
+
function readPositiveInteger(table, key) {
|
|
78
|
+
const value = table?.[key];
|
|
79
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function readLocalIndexSourceConfig(projectRoot) {
|
|
85
|
+
const sourceIndexTable = readNestedTable(readIndexToml(projectRoot), 'source_index');
|
|
86
|
+
return {
|
|
87
|
+
enabledByDefault: readBoolean(sourceIndexTable, 'enabled_by_default') === true,
|
|
88
|
+
include: readOptionalStringArray(sourceIndexTable, 'include') ?? [],
|
|
89
|
+
exclude: readOptionalStringArray(sourceIndexTable, 'exclude') ?? [],
|
|
90
|
+
maxFileBytes: readPositiveInteger(sourceIndexTable, 'max_file_bytes'),
|
|
91
|
+
allowedExtensions: readOptionalStringArray(sourceIndexTable, 'allowed_extensions') ?? [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function sha256Text(content) {
|
|
95
|
+
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
96
|
+
}
|
|
97
|
+
function sha256Bytes(content) {
|
|
98
|
+
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
99
|
+
}
|
|
100
|
+
function getSourceScopeHash(includeSource, sourceConfig) {
|
|
101
|
+
return sha256Text(JSON.stringify({
|
|
102
|
+
includeSource,
|
|
103
|
+
sourceConfig,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
function getDocumentType(relativePath) {
|
|
107
|
+
if (relativePath === 'AGENTS.md') {
|
|
108
|
+
return 'agent_rules';
|
|
109
|
+
}
|
|
110
|
+
if (relativePath.startsWith('.mustflow/config/')) {
|
|
111
|
+
return 'config';
|
|
112
|
+
}
|
|
113
|
+
if (relativePath === '.mustflow/skills/INDEX.md') {
|
|
114
|
+
return 'skill_index';
|
|
115
|
+
}
|
|
116
|
+
if (relativePath === '.mustflow/context/INDEX.md') {
|
|
117
|
+
return 'context_index';
|
|
118
|
+
}
|
|
119
|
+
if (relativePath.startsWith('.mustflow/context/')) {
|
|
120
|
+
return 'context';
|
|
121
|
+
}
|
|
122
|
+
if (relativePath.endsWith('/SKILL.md')) {
|
|
123
|
+
return 'skill';
|
|
124
|
+
}
|
|
125
|
+
if (relativePath.startsWith('.mustflow/docs/')) {
|
|
126
|
+
return 'workflow_doc';
|
|
127
|
+
}
|
|
128
|
+
return 'document';
|
|
129
|
+
}
|
|
130
|
+
function parseFrontmatter(content) {
|
|
131
|
+
if (!content.startsWith('---')) {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
const end = content.indexOf('\n---', 3);
|
|
135
|
+
if (end === -1) {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
const result = {};
|
|
139
|
+
const rawFrontmatter = content.slice(3, end);
|
|
140
|
+
for (const line of rawFrontmatter.split(/\r?\n/)) {
|
|
141
|
+
const separatorIndex = line.indexOf(':');
|
|
142
|
+
if (separatorIndex === -1) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
146
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
147
|
+
if (key.length > 0 && value.length > 0) {
|
|
148
|
+
result[key] = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
function getTitle(relativePath, content) {
|
|
154
|
+
const heading = content.match(/^#\s+(.+)$/mu)?.[1]?.trim();
|
|
155
|
+
return heading && heading.length > 0 ? heading : path.posix.basename(relativePath);
|
|
156
|
+
}
|
|
157
|
+
function getSections(content) {
|
|
158
|
+
return [...content.matchAll(/^##\s+(.+)$/gmu)].map((match) => match[1]?.trim()).filter((value) => Boolean(value));
|
|
159
|
+
}
|
|
160
|
+
function truncateUtf8(value, maxBytes) {
|
|
161
|
+
const buffer = Buffer.from(value, 'utf8');
|
|
162
|
+
if (buffer.byteLength <= maxBytes) {
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
return buffer.subarray(0, maxBytes).toString('utf8').replace(/\uFFFD$/u, '');
|
|
166
|
+
}
|
|
167
|
+
function collectDocuments(projectRoot) {
|
|
168
|
+
return getExistingIndexablePaths(projectRoot).map((relativePath) => {
|
|
169
|
+
const content = readText(projectRoot, relativePath);
|
|
170
|
+
const frontmatter = parseFrontmatter(content);
|
|
171
|
+
const revision = Number.parseInt(frontmatter.revision ?? '', 10);
|
|
172
|
+
return {
|
|
173
|
+
path: relativePath,
|
|
174
|
+
type: getDocumentType(relativePath),
|
|
175
|
+
title: getTitle(relativePath, content),
|
|
176
|
+
locale: frontmatter.locale ?? null,
|
|
177
|
+
revision: Number.isInteger(revision) ? revision : null,
|
|
178
|
+
contentHash: sha256Text(content),
|
|
179
|
+
contentSnippet: truncateUtf8(content, MAX_SNIPPET_BYTES_PER_DOCUMENT),
|
|
180
|
+
sections: getSections(content),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function collectSkills(documents) {
|
|
185
|
+
return documents
|
|
186
|
+
.filter((document) => document.type === 'skill')
|
|
187
|
+
.map((document) => ({
|
|
188
|
+
name: document.path.split('/').at(-2) ?? document.title,
|
|
189
|
+
path: document.path,
|
|
190
|
+
title: document.title,
|
|
191
|
+
}))
|
|
192
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
193
|
+
}
|
|
194
|
+
function normalizeMarkdownCell(value) {
|
|
195
|
+
return value
|
|
196
|
+
.replace(/<br\s*\/?>/giu, ' ')
|
|
197
|
+
.replace(/`([^`]+)`/gu, '$1')
|
|
198
|
+
.replace(/\s+/gu, ' ')
|
|
199
|
+
.trim();
|
|
200
|
+
}
|
|
201
|
+
function parseMarkdownTableRow(line) {
|
|
202
|
+
return line
|
|
203
|
+
.trim()
|
|
204
|
+
.replace(/^\|/u, '')
|
|
205
|
+
.replace(/\|$/u, '')
|
|
206
|
+
.split('|')
|
|
207
|
+
.map((cell) => normalizeMarkdownCell(cell));
|
|
208
|
+
}
|
|
209
|
+
function isMarkdownSeparatorRow(cells) {
|
|
210
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/u.test(cell));
|
|
211
|
+
}
|
|
212
|
+
function skillNameFromPath(skillPath) {
|
|
213
|
+
return skillPath.split('/').at(-2) ?? path.posix.basename(skillPath, '.md');
|
|
214
|
+
}
|
|
215
|
+
function splitVerificationIntents(value) {
|
|
216
|
+
return value
|
|
217
|
+
.split(',')
|
|
218
|
+
.map((item) => item.trim())
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
.sort((left, right) => left.localeCompare(right));
|
|
221
|
+
}
|
|
222
|
+
function collectSkillRoutes(projectRoot) {
|
|
223
|
+
const indexPath = path.join(projectRoot, '.mustflow', 'skills', 'INDEX.md');
|
|
224
|
+
if (!existsSync(indexPath)) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const content = readFileSync(indexPath, 'utf8');
|
|
228
|
+
const routes = [];
|
|
229
|
+
let inRouteTable = false;
|
|
230
|
+
for (const line of content.split(/\r?\n/u)) {
|
|
231
|
+
if (!line.trim().startsWith('|')) {
|
|
232
|
+
if (inRouteTable && line.trim() === '') {
|
|
233
|
+
inRouteTable = false;
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const cells = parseMarkdownTableRow(line);
|
|
238
|
+
if (cells.includes('Skill Document') && cells.includes('Trigger')) {
|
|
239
|
+
inRouteTable = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!inRouteTable || isMarkdownSeparatorRow(cells) || cells.length < 7) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const [trigger, skillPath, requiredInput, editScope, risk, verificationIntents, expectedOutput] = cells;
|
|
246
|
+
if (!skillPath?.startsWith('.mustflow/skills/') || !skillPath.endsWith('/SKILL.md')) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
routes.push({
|
|
250
|
+
skillName: skillNameFromPath(skillPath),
|
|
251
|
+
skillPath,
|
|
252
|
+
trigger: trigger ?? '',
|
|
253
|
+
requiredInput: requiredInput ?? '',
|
|
254
|
+
editScope: editScope ?? '',
|
|
255
|
+
risk: risk ?? '',
|
|
256
|
+
verificationIntents: splitVerificationIntents(verificationIntents ?? ''),
|
|
257
|
+
expectedOutput: expectedOutput ?? '',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return routes.sort((left, right) => {
|
|
261
|
+
const skillOrder = left.skillName.localeCompare(right.skillName);
|
|
262
|
+
return skillOrder === 0 ? left.trigger.localeCompare(right.trigger) : skillOrder;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function collectCommandIntents(projectRoot) {
|
|
266
|
+
if (!existsSync(path.join(projectRoot, '.mustflow', 'config', 'commands.toml'))) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
const contract = readCommandContract(projectRoot);
|
|
270
|
+
const intents = [];
|
|
271
|
+
for (const [name, intent] of Object.entries(contract.intents).sort(([left], [right]) => left.localeCompare(right))) {
|
|
272
|
+
if (!isRecord(intent)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
intents.push({
|
|
276
|
+
name,
|
|
277
|
+
status: readString(intent, 'status') ?? 'unknown',
|
|
278
|
+
lifecycle: readString(intent, 'lifecycle') ?? null,
|
|
279
|
+
runPolicy: readString(intent, 'run_policy') ?? null,
|
|
280
|
+
description: readString(intent, 'description') ?? null,
|
|
281
|
+
effects: normalizeCommandEffects(projectRoot, contract, name),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return intents;
|
|
285
|
+
}
|
|
286
|
+
function readIndexedFileRecord(projectRoot, relativePath, sourceScope, contentHash = null) {
|
|
287
|
+
const fullPath = path.join(projectRoot, ...relativePath.split('/'));
|
|
288
|
+
const stats = statSync(fullPath);
|
|
289
|
+
return {
|
|
290
|
+
path: relativePath,
|
|
291
|
+
sourceScope,
|
|
292
|
+
sizeBytes: stats.size,
|
|
293
|
+
mtimeMs: Math.round(stats.mtimeMs),
|
|
294
|
+
contentHash: contentHash ?? sha256Bytes(readFileSync(fullPath)),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function collectIndexedFileRecords(projectRoot, documents, sourceAnchors) {
|
|
298
|
+
const records = new Map();
|
|
299
|
+
for (const document of documents) {
|
|
300
|
+
records.set(document.path, readIndexedFileRecord(projectRoot, document.path, 'workflow', document.contentHash));
|
|
301
|
+
}
|
|
302
|
+
for (const anchorPath of [...new Set(sourceAnchors.map((anchor) => anchor.path))].sort((left, right) => left.localeCompare(right))) {
|
|
303
|
+
if (!records.has(anchorPath)) {
|
|
304
|
+
records.set(anchorPath, readIndexedFileRecord(projectRoot, anchorPath, 'source_anchor'));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
308
|
+
}
|
|
309
|
+
function normalizeSearchText(value) {
|
|
310
|
+
return value.trim().replace(/\s+/g, ' ');
|
|
311
|
+
}
|
|
312
|
+
function normalizeSearchTokenText(value) {
|
|
313
|
+
return normalizeSearchText(value).normalize('NFKC').toLowerCase();
|
|
314
|
+
}
|
|
315
|
+
function extractSearchTokens(value) {
|
|
316
|
+
return [...normalizeSearchTokenText(value).matchAll(/[\p{L}\p{N}]+/gu)]
|
|
317
|
+
.map((match) => match[0])
|
|
318
|
+
.filter((token) => Boolean(token));
|
|
319
|
+
}
|
|
320
|
+
function buildSearchNgrams(values) {
|
|
321
|
+
const grams = new Set();
|
|
322
|
+
for (const value of values) {
|
|
323
|
+
for (const token of extractSearchTokens(value)) {
|
|
324
|
+
const maxLength = Math.min(SEARCH_NGRAM_MAX_LENGTH, token.length);
|
|
325
|
+
for (let length = SEARCH_NGRAM_MIN_LENGTH; length <= maxLength; length += 1) {
|
|
326
|
+
for (let index = 0; index <= token.length - length; index += 1) {
|
|
327
|
+
grams.add(token.slice(index, index + length));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return [...grams].sort((left, right) => left.localeCompare(right));
|
|
333
|
+
}
|
|
334
|
+
function toSearchString(value) {
|
|
335
|
+
if (value === null || value === undefined) {
|
|
336
|
+
return '';
|
|
337
|
+
}
|
|
338
|
+
if (value instanceof Uint8Array) {
|
|
339
|
+
return '';
|
|
340
|
+
}
|
|
341
|
+
return String(value);
|
|
342
|
+
}
|
|
343
|
+
function queryRows(database, sql, params = []) {
|
|
344
|
+
const [result] = database.exec(sql, params);
|
|
345
|
+
if (!result) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
return result.values.map((values) => {
|
|
349
|
+
const row = {};
|
|
350
|
+
result.columns.forEach((column, index) => {
|
|
351
|
+
row[column] = values[index] ?? null;
|
|
352
|
+
});
|
|
353
|
+
return row;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const VALIDATION_RATCHET_RISK_CODES = new Set([
|
|
357
|
+
'related_test_deleted',
|
|
358
|
+
'skip_or_only_marker_present',
|
|
359
|
+
'todo_or_pending_marker_added',
|
|
360
|
+
'assertion_count_decreased',
|
|
361
|
+
'assertion_matcher_weakened',
|
|
362
|
+
'negative_assertion_removed',
|
|
363
|
+
'exception_assertion_removed',
|
|
364
|
+
'snapshot_mass_updated',
|
|
365
|
+
'golden_output_replaced',
|
|
366
|
+
'verification_intent_disabled',
|
|
367
|
+
'verification_required_after_removed',
|
|
368
|
+
'success_exit_codes_widened',
|
|
369
|
+
'command_allows_no_tests',
|
|
370
|
+
'command_forces_snapshot_update',
|
|
371
|
+
'command_hides_failure',
|
|
372
|
+
'coverage_threshold_lowered',
|
|
373
|
+
'test_selection_narrowed',
|
|
374
|
+
]);
|
|
375
|
+
function searchCapabilities(fts5Available) {
|
|
376
|
+
return {
|
|
377
|
+
backend: fts5Available ? SEARCH_BACKEND_FTS5 : SEARCH_BACKEND_TABLE_SCAN,
|
|
378
|
+
fts5Available,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function detectLocalSearchCapabilities(database) {
|
|
382
|
+
if (process.env[TEST_DISABLE_FTS5_ENV] === '1') {
|
|
383
|
+
return searchCapabilities(false);
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
database.run('CREATE VIRTUAL TABLE temp.mustflow_fts5_probe USING fts5(value)');
|
|
387
|
+
database.run('DROP TABLE temp.mustflow_fts5_probe');
|
|
388
|
+
return searchCapabilities(true);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return searchCapabilities(false);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function isJsonRecord(value) {
|
|
395
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
396
|
+
}
|
|
397
|
+
function readJsonRecord(filePath) {
|
|
398
|
+
try {
|
|
399
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
400
|
+
return isJsonRecord(parsed) ? parsed : null;
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function stringField(record, key) {
|
|
407
|
+
const value = record?.[key];
|
|
408
|
+
return typeof value === 'string' ? value : null;
|
|
409
|
+
}
|
|
410
|
+
function booleanField(record, key) {
|
|
411
|
+
return record?.[key] === true;
|
|
412
|
+
}
|
|
413
|
+
function numberField(record, key) {
|
|
414
|
+
const value = record?.[key];
|
|
415
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
416
|
+
}
|
|
417
|
+
function recordField(record, key) {
|
|
418
|
+
const value = record?.[key];
|
|
419
|
+
return isJsonRecord(value) ? value : null;
|
|
420
|
+
}
|
|
421
|
+
function recordArrayField(record, key) {
|
|
422
|
+
const value = record?.[key];
|
|
423
|
+
return Array.isArray(value) ? value.filter(isJsonRecord) : [];
|
|
424
|
+
}
|
|
425
|
+
function stringArrayField(record, key) {
|
|
426
|
+
const value = record?.[key];
|
|
427
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
428
|
+
}
|
|
429
|
+
function joinedList(values) {
|
|
430
|
+
return [...values].sort((left, right) => left.localeCompare(right)).join(', ');
|
|
431
|
+
}
|
|
432
|
+
function hashJson(value) {
|
|
433
|
+
return sha256Text(JSON.stringify(value));
|
|
434
|
+
}
|
|
435
|
+
function stringListHash(values) {
|
|
436
|
+
const normalized = values.filter((value) => typeof value === 'string' && value.length > 0);
|
|
437
|
+
return normalized.length > 0 ? hashJson([...normalized].sort((left, right) => left.localeCompare(right))) : null;
|
|
438
|
+
}
|
|
439
|
+
function reproObservation(routeId, phase, evidence) {
|
|
440
|
+
const status = stringField(evidence, 'status');
|
|
441
|
+
const outcome = stringField(evidence, 'outcome') ?? status;
|
|
442
|
+
const receiptHash = stringField(evidence, 'receipt_sha256');
|
|
443
|
+
const diagnosticFingerprint = stringField(evidence, 'diagnostic_fingerprint') ??
|
|
444
|
+
stringField(evidence, 'diagnostic_hash') ??
|
|
445
|
+
hashJson({
|
|
446
|
+
phase,
|
|
447
|
+
status,
|
|
448
|
+
outcome,
|
|
449
|
+
summary: stringField(evidence, 'summary'),
|
|
450
|
+
reason: stringField(evidence, 'reason'),
|
|
451
|
+
});
|
|
452
|
+
return {
|
|
453
|
+
routeId,
|
|
454
|
+
phase,
|
|
455
|
+
outcome,
|
|
456
|
+
receiptHash,
|
|
457
|
+
diagnosticFingerprint,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function evidenceStatusForRunReceipt(latest) {
|
|
461
|
+
return stringField(latest, 'status') ?? (booleanField(latest, 'timed_out') ? 'timed_out' : 'unknown');
|
|
462
|
+
}
|
|
463
|
+
function failedIntentsFromReceipts(receipts) {
|
|
464
|
+
return receipts
|
|
465
|
+
.filter((receipt) => ['failed', 'timed_out', 'start_failed'].includes(receipt.status))
|
|
466
|
+
.map((receipt) => receipt.intent)
|
|
467
|
+
.filter((intent) => typeof intent === 'string' && intent.length > 0)
|
|
468
|
+
.sort((left, right) => left.localeCompare(right));
|
|
469
|
+
}
|
|
470
|
+
function createFailureFingerprint(input) {
|
|
471
|
+
if (input.status === 'passed' ||
|
|
472
|
+
input.status === 'verified' ||
|
|
473
|
+
(input.failedIntents.length === 0 && input.riskCodes.length === 0 && input.timedOut !== true && !input.errorKind)) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
return sha256Text(JSON.stringify({
|
|
477
|
+
command: input.command,
|
|
478
|
+
status: input.status,
|
|
479
|
+
verificationPlanId: input.verificationPlanId,
|
|
480
|
+
primaryReason: input.primaryReason,
|
|
481
|
+
failedIntents: [...input.failedIntents].sort((left, right) => left.localeCompare(right)),
|
|
482
|
+
riskCodes: [...input.riskCodes].sort((left, right) => left.localeCompare(right)),
|
|
483
|
+
runIntent: input.runIntent ?? null,
|
|
484
|
+
timedOut: input.timedOut === true,
|
|
485
|
+
exitCodeClass: input.exitCodeClass ?? null,
|
|
486
|
+
errorKind: input.errorKind ?? null,
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
function createVerificationEvidenceIndex(projectRoot) {
|
|
490
|
+
const latestPath = path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/'));
|
|
491
|
+
if (!existsSync(latestPath)) {
|
|
492
|
+
return {
|
|
493
|
+
summaries: [],
|
|
494
|
+
verificationPlans: [],
|
|
495
|
+
acceptanceCriteria: [],
|
|
496
|
+
criterionCoverage: [],
|
|
497
|
+
receipts: [],
|
|
498
|
+
commandReceiptSummaries: [],
|
|
499
|
+
coverageStates: [],
|
|
500
|
+
riskSignals: [],
|
|
501
|
+
validationRatchetSignals: [],
|
|
502
|
+
completionVerdictSummaries: [],
|
|
503
|
+
failureFingerprints: [],
|
|
504
|
+
reproRoutes: [],
|
|
505
|
+
reproObservations: [],
|
|
506
|
+
failureFingerprintReadModels: [],
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const latest = readJsonRecord(latestPath);
|
|
510
|
+
if (!latest) {
|
|
511
|
+
return {
|
|
512
|
+
summaries: [],
|
|
513
|
+
verificationPlans: [],
|
|
514
|
+
acceptanceCriteria: [],
|
|
515
|
+
criterionCoverage: [],
|
|
516
|
+
receipts: [],
|
|
517
|
+
commandReceiptSummaries: [],
|
|
518
|
+
coverageStates: [],
|
|
519
|
+
riskSignals: [],
|
|
520
|
+
validationRatchetSignals: [],
|
|
521
|
+
completionVerdictSummaries: [],
|
|
522
|
+
failureFingerprints: [],
|
|
523
|
+
reproRoutes: [],
|
|
524
|
+
reproObservations: [],
|
|
525
|
+
failureFingerprintReadModels: [],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const sourceHash = sha256Bytes(readFileSync(latestPath));
|
|
529
|
+
const command = stringField(latest, 'command') ?? 'unknown';
|
|
530
|
+
const kind = stringField(latest, 'kind') ?? (command === 'verify' ? 'verify_run_summary' : 'run_receipt');
|
|
531
|
+
const evidenceModel = recordField(latest, 'evidence_model');
|
|
532
|
+
const completionVerdict = recordField(latest, 'completion_verdict');
|
|
533
|
+
const completionEvidence = recordField(completionVerdict, 'evidence');
|
|
534
|
+
const verificationPlanId = stringField(latest, 'verification_plan_id') ?? stringField(evidenceModel, 'verification_plan_id');
|
|
535
|
+
const primaryReason = stringField(completionVerdict, 'primary_reason');
|
|
536
|
+
const status = stringField(latest, 'status') ?? stringField(completionVerdict, 'status') ?? 'unknown';
|
|
537
|
+
const completionStatus = stringField(completionVerdict, 'status');
|
|
538
|
+
const rawReceipts = recordArrayField(evidenceModel, 'receipts');
|
|
539
|
+
const rawCoverage = recordArrayField(evidenceModel, 'coverage_matrix');
|
|
540
|
+
const rawRequirements = recordArrayField(evidenceModel, 'requirements');
|
|
541
|
+
const rawRisks = recordArrayField(evidenceModel, 'remaining_risks');
|
|
542
|
+
const recordedFailureFingerprintRecord = recordField(latest, 'failure_fingerprint');
|
|
543
|
+
const repeatedFailureSummary = recordField(latest, 'repeated_failure_summary');
|
|
544
|
+
const reproEvidence = recordField(latest, 'repro_evidence') ?? recordField(evidenceModel, 'repro_evidence');
|
|
545
|
+
const reproductionRoute = recordField(reproEvidence, 'reproduction_route');
|
|
546
|
+
const recordedFailureFingerprint = stringField(recordedFailureFingerprintRecord, 'fingerprint');
|
|
547
|
+
const receipts = rawReceipts.length > 0
|
|
548
|
+
? rawReceipts.map((receipt, index) => ({
|
|
549
|
+
sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
|
|
550
|
+
ordinal: index + 1,
|
|
551
|
+
intent: stringField(receipt, 'intent'),
|
|
552
|
+
status: stringField(receipt, 'status') ?? 'unknown',
|
|
553
|
+
skipped: booleanField(receipt, 'skipped'),
|
|
554
|
+
verificationPlanId: stringField(receipt, 'verification_plan_id'),
|
|
555
|
+
receiptPath: stringField(receipt, 'receipt_path'),
|
|
556
|
+
receiptSha256: stringField(receipt, 'receipt_sha256'),
|
|
557
|
+
commandFingerprint: stringField(receipt, 'command_fingerprint'),
|
|
558
|
+
contractFingerprint: stringField(receipt, 'contract_fingerprint'),
|
|
559
|
+
currentStateHash: stringField(receipt, 'head_tree_hash') ??
|
|
560
|
+
stringField(receipt, 'changed_files_hash') ??
|
|
561
|
+
stringField(receipt, 'changed_file_hash'),
|
|
562
|
+
writeDriftStatus: stringField(receipt, 'write_drift_status'),
|
|
563
|
+
}))
|
|
564
|
+
: [
|
|
565
|
+
{
|
|
566
|
+
sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
|
|
567
|
+
ordinal: 1,
|
|
568
|
+
intent: stringField(latest, 'intent'),
|
|
569
|
+
status: evidenceStatusForRunReceipt(latest),
|
|
570
|
+
skipped: false,
|
|
571
|
+
verificationPlanId: null,
|
|
572
|
+
receiptPath: stringField(latest, 'receipt_path') ?? LATEST_RUN_STATE_RELATIVE_PATH,
|
|
573
|
+
receiptSha256: sourceHash,
|
|
574
|
+
commandFingerprint: stringField(recordField(latest, 'performance'), 'command_fingerprint'),
|
|
575
|
+
contractFingerprint: stringField(recordField(latest, 'performance'), 'contract_fingerprint'),
|
|
576
|
+
currentStateHash: stringField(latest, 'head_tree_hash') ?? stringField(latest, 'changed_files_hash'),
|
|
577
|
+
writeDriftStatus: stringField(recordField(latest, 'write_drift'), 'status'),
|
|
578
|
+
},
|
|
579
|
+
];
|
|
580
|
+
const coverageStates = rawCoverage.map((coverage) => {
|
|
581
|
+
const evidence = recordField(coverage, 'evidence');
|
|
582
|
+
return {
|
|
583
|
+
sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
|
|
584
|
+
criterionId: stringField(coverage, 'criterion_id') ?? 'unknown',
|
|
585
|
+
source: stringField(coverage, 'source') ?? 'unknown',
|
|
586
|
+
status: stringField(coverage, 'status') ?? 'unknown',
|
|
587
|
+
requirementReason: stringField(coverage, 'requirement_reason'),
|
|
588
|
+
intents: stringArrayField(evidence, 'intents'),
|
|
589
|
+
receiptCount: stringArrayField(evidence, 'receipt_paths').length,
|
|
590
|
+
gapCount: stringArrayField(evidence, 'gap_reasons').length,
|
|
591
|
+
sourceAnchorCount: stringArrayField(evidence, 'source_anchor_ids').length,
|
|
592
|
+
};
|
|
593
|
+
});
|
|
594
|
+
const riskSignals = rawRisks.map((risk, index) => ({
|
|
595
|
+
sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
|
|
596
|
+
ordinal: index + 1,
|
|
597
|
+
code: stringField(risk, 'code') ?? 'unknown',
|
|
598
|
+
severity: stringField(risk, 'severity') ?? 'unknown',
|
|
599
|
+
detailHash: sha256Text(stringField(risk, 'detail') ?? ''),
|
|
600
|
+
}));
|
|
601
|
+
const validationRatchetSignals = rawRisks
|
|
602
|
+
.map((risk, index) => {
|
|
603
|
+
const code = stringField(risk, 'code') ?? 'unknown';
|
|
604
|
+
if (!VALIDATION_RATCHET_RISK_CODES.has(code)) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const severity = stringField(risk, 'severity') ?? 'unknown';
|
|
608
|
+
const pathValue = stringField(risk, 'path');
|
|
609
|
+
const detailHash = sha256Text(stringField(risk, 'detail') ?? '');
|
|
610
|
+
const pathHash = pathValue === null ? hashJson({ code, detailHash }) : sha256Text(pathValue);
|
|
611
|
+
const beforeHash = stringField(risk, 'before_hash') ?? stringField(risk, 'before_digest');
|
|
612
|
+
const afterHash = stringField(risk, 'after_hash') ?? stringField(risk, 'after_digest');
|
|
613
|
+
return {
|
|
614
|
+
signalId: hashJson({
|
|
615
|
+
sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
|
|
616
|
+
ordinal: index + 1,
|
|
617
|
+
planId: verificationPlanId,
|
|
618
|
+
code,
|
|
619
|
+
pathHash,
|
|
620
|
+
beforeHash,
|
|
621
|
+
afterHash,
|
|
622
|
+
}),
|
|
623
|
+
planId: verificationPlanId,
|
|
624
|
+
code,
|
|
625
|
+
severity,
|
|
626
|
+
pathHash,
|
|
627
|
+
beforeHash,
|
|
628
|
+
afterHash,
|
|
629
|
+
};
|
|
630
|
+
})
|
|
631
|
+
.filter((signal) => signal !== null);
|
|
632
|
+
const verificationPlans = verificationPlanId === null
|
|
633
|
+
? []
|
|
634
|
+
: [
|
|
635
|
+
{
|
|
636
|
+
planId: verificationPlanId,
|
|
637
|
+
sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
|
|
638
|
+
classificationHash: rawRequirements.length > 0 || rawCoverage.length > 0
|
|
639
|
+
? hashJson({
|
|
640
|
+
requirements: rawRequirements.map((requirement) => ({
|
|
641
|
+
id: stringField(requirement, 'requirement_id') ?? stringField(requirement, 'id'),
|
|
642
|
+
reason: stringField(requirement, 'reason'),
|
|
643
|
+
source: stringField(requirement, 'source'),
|
|
644
|
+
})),
|
|
645
|
+
coverage: rawCoverage.map((coverage) => ({
|
|
646
|
+
id: stringField(coverage, 'criterion_id'),
|
|
647
|
+
reason: stringField(coverage, 'requirement_reason'),
|
|
648
|
+
source: stringField(coverage, 'source'),
|
|
649
|
+
status: stringField(coverage, 'status'),
|
|
650
|
+
})),
|
|
651
|
+
})
|
|
652
|
+
: null,
|
|
653
|
+
commandContractHash: stringListHash(receipts.map((receipt) => receipt.contractFingerprint)),
|
|
654
|
+
selectedIntentsHash: stringListHash(receipts.map((receipt) => receipt.intent)),
|
|
655
|
+
createdAt: stringField(latest, 'started_at') ?? stringField(latest, 'created_at'),
|
|
656
|
+
sourceHash,
|
|
657
|
+
},
|
|
658
|
+
];
|
|
659
|
+
const acceptanceCriteria = verificationPlanId === null
|
|
660
|
+
? []
|
|
661
|
+
: rawCoverage.map((coverage) => {
|
|
662
|
+
const evidence = recordField(coverage, 'evidence');
|
|
663
|
+
const pathRefs = [
|
|
664
|
+
...stringArrayField(evidence, 'paths'),
|
|
665
|
+
...stringArrayField(evidence, 'changed_paths'),
|
|
666
|
+
...stringArrayField(evidence, 'source_anchor_ids'),
|
|
667
|
+
];
|
|
668
|
+
return {
|
|
669
|
+
criterionId: stringField(coverage, 'criterion_id') ?? 'unknown',
|
|
670
|
+
planId: verificationPlanId,
|
|
671
|
+
source: stringField(coverage, 'source') ?? 'unknown',
|
|
672
|
+
statementHash: stringField(coverage, 'statement') ? sha256Text(stringField(coverage, 'statement') ?? '') : null,
|
|
673
|
+
reason: stringField(coverage, 'requirement_reason'),
|
|
674
|
+
surface: stringField(coverage, 'surface'),
|
|
675
|
+
pathHash: pathRefs.length > 0 ? stringListHash(pathRefs) : null,
|
|
676
|
+
};
|
|
677
|
+
});
|
|
678
|
+
const criterionCoverage = verificationPlanId === null
|
|
679
|
+
? []
|
|
680
|
+
: coverageStates.map((coverage) => ({
|
|
681
|
+
criterionId: coverage.criterionId,
|
|
682
|
+
planId: verificationPlanId,
|
|
683
|
+
status: coverage.status,
|
|
684
|
+
receiptCount: coverage.receiptCount,
|
|
685
|
+
gapCount: coverage.gapCount,
|
|
686
|
+
riskCount: coverage.sourceAnchorCount,
|
|
687
|
+
}));
|
|
688
|
+
const commandReceiptSummaries = verificationPlanId === null
|
|
689
|
+
? []
|
|
690
|
+
: receipts
|
|
691
|
+
.filter((receipt) => receipt.verificationPlanId === verificationPlanId || receipt.verificationPlanId === null)
|
|
692
|
+
.map((receipt) => ({
|
|
693
|
+
receiptHash: receipt.receiptSha256 ??
|
|
694
|
+
hashJson({
|
|
695
|
+
sourcePath: receipt.sourcePath,
|
|
696
|
+
ordinal: receipt.ordinal,
|
|
697
|
+
intent: receipt.intent,
|
|
698
|
+
status: receipt.status,
|
|
699
|
+
verificationPlanId,
|
|
700
|
+
}),
|
|
701
|
+
planId: verificationPlanId,
|
|
702
|
+
intent: receipt.intent,
|
|
703
|
+
status: receipt.status,
|
|
704
|
+
commandFingerprint: receipt.commandFingerprint,
|
|
705
|
+
contractFingerprint: receipt.contractFingerprint,
|
|
706
|
+
currentStateHash: receipt.currentStateHash,
|
|
707
|
+
writeDriftStatus: receipt.writeDriftStatus,
|
|
708
|
+
}));
|
|
709
|
+
const completionVerdictSummaries = verificationPlanId === null || completionStatus === null
|
|
710
|
+
? []
|
|
711
|
+
: [
|
|
712
|
+
{
|
|
713
|
+
claimId: hashJson({
|
|
714
|
+
sourceHash,
|
|
715
|
+
verificationPlanId,
|
|
716
|
+
completionStatus,
|
|
717
|
+
primaryReason,
|
|
718
|
+
}),
|
|
719
|
+
planId: verificationPlanId,
|
|
720
|
+
status: completionStatus,
|
|
721
|
+
primaryReason,
|
|
722
|
+
riskCount: riskSignals.length,
|
|
723
|
+
contradictionCount: stringArrayField(completionVerdict, 'contradictions').length,
|
|
724
|
+
blockerCount: stringArrayField(completionVerdict, 'blockers').length,
|
|
725
|
+
},
|
|
726
|
+
];
|
|
727
|
+
const failedIntents = failedIntentsFromReceipts(receipts);
|
|
728
|
+
const failureFingerprint = recordedFailureFingerprint ??
|
|
729
|
+
createFailureFingerprint({
|
|
730
|
+
command,
|
|
731
|
+
status: completionStatus ?? status,
|
|
732
|
+
verificationPlanId,
|
|
733
|
+
primaryReason,
|
|
734
|
+
failedIntents,
|
|
735
|
+
riskCodes: riskSignals.map((risk) => risk.code),
|
|
736
|
+
runIntent: stringField(latest, 'intent'),
|
|
737
|
+
timedOut: booleanField(latest, 'timed_out'),
|
|
738
|
+
exitCodeClass: stringField(recordField(recordField(latest, 'performance'), 'result_summary'), 'exit_code_class'),
|
|
739
|
+
errorKind: stringField(recordField(recordField(latest, 'performance'), 'result_summary'), 'error_kind'),
|
|
740
|
+
});
|
|
741
|
+
const failureFingerprints = failureFingerprint === null
|
|
742
|
+
? []
|
|
743
|
+
: [
|
|
744
|
+
{
|
|
745
|
+
sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
|
|
746
|
+
fingerprint: failureFingerprint,
|
|
747
|
+
verificationPlanId,
|
|
748
|
+
status: completionStatus ?? status,
|
|
749
|
+
failedIntents,
|
|
750
|
+
primaryReason,
|
|
751
|
+
failedIntentsHash: stringField(recordedFailureFingerprintRecord, 'failed_intents_hash') ??
|
|
752
|
+
stringField(repeatedFailureSummary, 'failed_intents_hash'),
|
|
753
|
+
riskCodesHash: stringField(recordedFailureFingerprintRecord, 'risk_codes_hash') ??
|
|
754
|
+
stringField(repeatedFailureSummary, 'risk_codes_hash'),
|
|
755
|
+
affectedSurfacesHash: stringField(recordedFailureFingerprintRecord, 'affected_surfaces_hash') ??
|
|
756
|
+
stringField(repeatedFailureSummary, 'affected_surfaces_hash'),
|
|
757
|
+
firstSeenAt: stringField(repeatedFailureSummary, 'first_seen_at'),
|
|
758
|
+
lastSeenAt: stringField(repeatedFailureSummary, 'last_seen_at'),
|
|
759
|
+
seenCount: Math.max(1, numberField(repeatedFailureSummary, 'seen_count')),
|
|
760
|
+
requiresNewEvidence: booleanField(repeatedFailureSummary, 'requires_new_evidence'),
|
|
761
|
+
},
|
|
762
|
+
];
|
|
763
|
+
const routeId = stringField(reproductionRoute, 'route_id');
|
|
764
|
+
const reproRoutes = routeId === null || reproEvidence === null
|
|
765
|
+
? []
|
|
766
|
+
: [
|
|
767
|
+
{
|
|
768
|
+
routeId,
|
|
769
|
+
taskHash: hashJson({
|
|
770
|
+
reported_symptom: stringField(reproEvidence, 'reported_symptom'),
|
|
771
|
+
expected_behavior: stringField(reproEvidence, 'expected_behavior'),
|
|
772
|
+
observed_behavior: stringField(reproEvidence, 'observed_behavior'),
|
|
773
|
+
}),
|
|
774
|
+
routeDigest: stringField(reproductionRoute, 'route_digest'),
|
|
775
|
+
routeKind: stringField(reproductionRoute, 'route_kind'),
|
|
776
|
+
failureOracleHash: stringField(reproductionRoute, 'failure_oracle_hash'),
|
|
777
|
+
},
|
|
778
|
+
];
|
|
779
|
+
const reproObservations = routeId === null || reproEvidence === null
|
|
780
|
+
? []
|
|
781
|
+
: [
|
|
782
|
+
reproObservation(routeId, 'before_fix', recordField(reproEvidence, 'before_fix')),
|
|
783
|
+
reproObservation(routeId, 'after_fix', recordField(reproEvidence, 'after_fix')),
|
|
784
|
+
reproObservation(routeId, 'regression_guard', recordField(reproEvidence, 'regression_guard')),
|
|
785
|
+
];
|
|
786
|
+
const failureFingerprintReadModels = failureFingerprints.map((fingerprint) => ({
|
|
787
|
+
fingerprint: fingerprint.fingerprint,
|
|
788
|
+
planId: fingerprint.verificationPlanId,
|
|
789
|
+
failedIntentsHash: fingerprint.failedIntentsHash ?? stringListHash(fingerprint.failedIntents),
|
|
790
|
+
riskCodesHash: fingerprint.riskCodesHash,
|
|
791
|
+
seenCount: fingerprint.seenCount,
|
|
792
|
+
firstSeenAt: fingerprint.firstSeenAt,
|
|
793
|
+
lastSeenAt: fingerprint.lastSeenAt,
|
|
794
|
+
}));
|
|
795
|
+
return {
|
|
796
|
+
summaries: [
|
|
797
|
+
{
|
|
798
|
+
sourcePath: LATEST_RUN_STATE_RELATIVE_PATH,
|
|
799
|
+
sourceHash,
|
|
800
|
+
command,
|
|
801
|
+
kind,
|
|
802
|
+
status,
|
|
803
|
+
runDir: stringField(latest, 'run_dir'),
|
|
804
|
+
manifestPath: stringField(latest, 'manifest_path'),
|
|
805
|
+
verificationPlanId,
|
|
806
|
+
completionStatus,
|
|
807
|
+
primaryReason,
|
|
808
|
+
matchedIntents: numberField(completionEvidence, 'matched_intents'),
|
|
809
|
+
ranIntents: numberField(completionEvidence, 'ran_intents'),
|
|
810
|
+
passedIntents: numberField(completionEvidence, 'passed_intents'),
|
|
811
|
+
failedIntents: numberField(completionEvidence, 'failed_intents'),
|
|
812
|
+
skippedIntents: numberField(completionEvidence, 'skipped_intents'),
|
|
813
|
+
receiptCount: receipts.length,
|
|
814
|
+
coverageCount: coverageStates.length,
|
|
815
|
+
remainingRiskCount: riskSignals.length,
|
|
816
|
+
failureFingerprint,
|
|
817
|
+
},
|
|
818
|
+
],
|
|
819
|
+
verificationPlans,
|
|
820
|
+
acceptanceCriteria,
|
|
821
|
+
criterionCoverage,
|
|
822
|
+
receipts,
|
|
823
|
+
commandReceiptSummaries,
|
|
824
|
+
coverageStates,
|
|
825
|
+
riskSignals,
|
|
826
|
+
validationRatchetSignals,
|
|
827
|
+
completionVerdictSummaries,
|
|
828
|
+
failureFingerprints,
|
|
829
|
+
reproRoutes,
|
|
830
|
+
reproObservations,
|
|
831
|
+
failureFingerprintReadModels,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function readMetadataValue(database, key) {
|
|
835
|
+
return toSearchString(queryRows(database, 'SELECT value FROM metadata WHERE key = ?', [key])[0]?.value) || undefined;
|
|
836
|
+
}
|
|
837
|
+
function hasTable(database, tableName) {
|
|
838
|
+
return queryRows(database, 'SELECT name FROM sqlite_master WHERE type = "table" AND name = ?', [tableName]).length > 0;
|
|
839
|
+
}
|
|
840
|
+
function readStoredSearchCapabilities(database) {
|
|
841
|
+
const fts5Available = readMetadataValue(database, 'search_fts5_available') === 'true';
|
|
842
|
+
const backend = readMetadataValue(database, 'search_backend');
|
|
843
|
+
if (backend === SEARCH_BACKEND_FTS5 && hasTable(database, 'search_documents_fts')) {
|
|
844
|
+
return { backend: SEARCH_BACKEND_FTS5, fts5Available };
|
|
845
|
+
}
|
|
846
|
+
return { backend: SEARCH_BACKEND_TABLE_SCAN, fts5Available };
|
|
847
|
+
}
|
|
848
|
+
function toNullableNumber(value) {
|
|
849
|
+
if (typeof value !== 'number') {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
return Number.isFinite(value) ? value : null;
|
|
853
|
+
}
|
|
854
|
+
function splitIndexedList(value) {
|
|
855
|
+
return toSearchString(value)
|
|
856
|
+
.split(',')
|
|
857
|
+
.map((item) => item.trim())
|
|
858
|
+
.filter(Boolean)
|
|
859
|
+
.sort((left, right) => left.localeCompare(right));
|
|
860
|
+
}
|
|
861
|
+
function createCommandEffectGraphStatus(databasePath, status, stalePaths = []) {
|
|
862
|
+
return {
|
|
863
|
+
source: 'local_index',
|
|
864
|
+
authority: 'explanation_only',
|
|
865
|
+
commandAuthority: '.mustflow/config/commands.toml',
|
|
866
|
+
grantsCommandAuthority: false,
|
|
867
|
+
status,
|
|
868
|
+
databasePath,
|
|
869
|
+
indexFresh: status === 'fresh',
|
|
870
|
+
stalePaths,
|
|
871
|
+
writeLocks: [],
|
|
872
|
+
lockConflicts: [],
|
|
873
|
+
refreshHint: status === 'fresh' ? null : 'Run `mf index` to refresh command-effect graph explanations.',
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
async function readPreviousSourceAnchorSnapshots(databasePath) {
|
|
877
|
+
if (!existsSync(databasePath)) {
|
|
878
|
+
return [];
|
|
879
|
+
}
|
|
880
|
+
const SQL = await loadSqlJs();
|
|
881
|
+
const database = new SQL.Database(readFileSync(databasePath));
|
|
882
|
+
try {
|
|
883
|
+
const rows = queryRows(database, `
|
|
884
|
+
SELECT
|
|
885
|
+
source_anchors.id,
|
|
886
|
+
source_anchors.path,
|
|
887
|
+
source_anchors.line_start,
|
|
888
|
+
source_anchors.purpose,
|
|
889
|
+
source_anchors.search_terms,
|
|
890
|
+
source_anchors.invariant,
|
|
891
|
+
source_anchors.risk,
|
|
892
|
+
source_anchor_fingerprints.anchor_metadata_hash,
|
|
893
|
+
source_anchor_fingerprints.anchor_text_hash,
|
|
894
|
+
source_anchor_fingerprints.context_hash,
|
|
895
|
+
source_anchor_fingerprints.search_terms_hash,
|
|
896
|
+
source_anchor_fingerprints.invariant_hash,
|
|
897
|
+
source_anchor_fingerprints.risk_hash,
|
|
898
|
+
source_anchor_fingerprints.symbol_kind,
|
|
899
|
+
source_anchor_fingerprints.symbol_name,
|
|
900
|
+
source_anchor_fingerprints.symbol_exported,
|
|
901
|
+
source_anchor_fingerprints.signature_hash,
|
|
902
|
+
source_anchor_fingerprints.body_hash,
|
|
903
|
+
source_anchor_fingerprints.symbol_start_line,
|
|
904
|
+
source_anchor_fingerprints.symbol_end_line
|
|
905
|
+
FROM source_anchors
|
|
906
|
+
JOIN source_anchor_fingerprints ON source_anchor_fingerprints.anchor_id = source_anchors.id
|
|
907
|
+
`);
|
|
908
|
+
return rows.map((row) => {
|
|
909
|
+
const symbol = {
|
|
910
|
+
kind: toSearchString(row.symbol_kind),
|
|
911
|
+
name: toSearchString(row.symbol_name) || null,
|
|
912
|
+
exported: Number(row.symbol_exported) === 1,
|
|
913
|
+
signatureHash: toSearchString(row.signature_hash) || null,
|
|
914
|
+
bodyHash: toSearchString(row.body_hash) || null,
|
|
915
|
+
startLine: toNullableNumber(row.symbol_start_line),
|
|
916
|
+
endLine: toNullableNumber(row.symbol_end_line),
|
|
917
|
+
};
|
|
918
|
+
return {
|
|
919
|
+
id: toSearchString(row.id),
|
|
920
|
+
path: toSearchString(row.path),
|
|
921
|
+
lineStart: Number(row.line_start),
|
|
922
|
+
purpose: toSearchString(row.purpose) || null,
|
|
923
|
+
search: toSearchString(row.search_terms)
|
|
924
|
+
.split(/[,;]/u)
|
|
925
|
+
.map((value) => value.trim())
|
|
926
|
+
.filter((value) => value.length > 0),
|
|
927
|
+
invariant: toSearchString(row.invariant) || null,
|
|
928
|
+
risk: toSearchString(row.risk)
|
|
929
|
+
.split(/[,;]/u)
|
|
930
|
+
.map((value) => value.trim())
|
|
931
|
+
.filter((value) => value.length > 0),
|
|
932
|
+
navigationOnly: true,
|
|
933
|
+
canInstructAgent: false,
|
|
934
|
+
fingerprint: {
|
|
935
|
+
anchorMetadataHash: toSearchString(row.anchor_metadata_hash),
|
|
936
|
+
anchorTextHash: toSearchString(row.anchor_text_hash),
|
|
937
|
+
contextHash: toSearchString(row.context_hash),
|
|
938
|
+
searchTermsHash: toSearchString(row.search_terms_hash) || null,
|
|
939
|
+
invariantHash: toSearchString(row.invariant_hash) || null,
|
|
940
|
+
riskHash: toSearchString(row.risk_hash),
|
|
941
|
+
symbol,
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
return [];
|
|
948
|
+
}
|
|
949
|
+
finally {
|
|
950
|
+
database.close();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function readCacheLayerSets(projectRoot) {
|
|
954
|
+
const mustflow = readMustflowToml(projectRoot);
|
|
955
|
+
const promptCache = readNestedTable(mustflow, 'prompt_cache');
|
|
956
|
+
const layers = readNestedTable(promptCache, 'layers');
|
|
957
|
+
const stable = readNestedTable(layers, 'stable');
|
|
958
|
+
const task = readNestedTable(layers, 'task');
|
|
959
|
+
const volatile = readNestedTable(layers, 'volatile');
|
|
960
|
+
const normalize = (values) => new Set(values.map((value) => toPosixPath(value)));
|
|
961
|
+
return {
|
|
962
|
+
stable: normalize(readOptionalStringArray(stable, 'read') ?? [...DEFAULT_PROMPT_CACHE_STABLE_READ]),
|
|
963
|
+
task: normalize(readOptionalStringArray(task, 'sources') ?? [...DEFAULT_PROMPT_CACHE_TASK_SOURCES]),
|
|
964
|
+
volatile: normalize(readOptionalStringArray(volatile, 'sources') ?? [...DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES]),
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function inferCacheLayer(relativePath, kind, cacheLayers) {
|
|
968
|
+
const normalized = relativePath ? toPosixPath(relativePath) : null;
|
|
969
|
+
if (normalized && cacheLayers.volatile.has(normalized)) {
|
|
970
|
+
return 'volatile';
|
|
971
|
+
}
|
|
972
|
+
if (normalized && cacheLayers.stable.has(normalized)) {
|
|
973
|
+
return 'stable';
|
|
974
|
+
}
|
|
975
|
+
if (kind === 'command_intent' && cacheLayers.stable.has('.mustflow/config/commands.toml')) {
|
|
976
|
+
return 'stable';
|
|
977
|
+
}
|
|
978
|
+
if (normalized &&
|
|
979
|
+
(cacheLayers.task.has(normalized) || normalized.startsWith('.mustflow/context/') || normalized.endsWith('/SKILL.md'))) {
|
|
980
|
+
return 'task';
|
|
981
|
+
}
|
|
982
|
+
if (normalized?.startsWith('.mustflow/state/') || normalized?.startsWith('.mustflow/cache/')) {
|
|
983
|
+
return 'volatile';
|
|
984
|
+
}
|
|
985
|
+
return 'task';
|
|
986
|
+
}
|
|
987
|
+
function withCacheHint(item, cacheLayers) {
|
|
988
|
+
const layer = inferCacheLayer(item.path ?? null, item.kind, cacheLayers);
|
|
989
|
+
return {
|
|
990
|
+
...item,
|
|
991
|
+
cache_layer: layer,
|
|
992
|
+
volatile: layer === 'volatile',
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
function workflowAuthorityForDocument(documentType) {
|
|
996
|
+
if (documentType === 'agent_rules' || documentType === 'config' || documentType === 'workflow_doc') {
|
|
997
|
+
return {
|
|
998
|
+
authority_rank: 2,
|
|
999
|
+
authority_label: 'workflow_authority',
|
|
1000
|
+
source_scope: 'workflow',
|
|
1001
|
+
navigation_only: false,
|
|
1002
|
+
can_instruct_agent: true,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
authority_rank: 4,
|
|
1007
|
+
authority_label: 'workflow_context',
|
|
1008
|
+
source_scope: 'workflow',
|
|
1009
|
+
navigation_only: false,
|
|
1010
|
+
can_instruct_agent: false,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function skillAuthority() {
|
|
1014
|
+
return {
|
|
1015
|
+
authority_rank: 3,
|
|
1016
|
+
authority_label: 'skill_procedure',
|
|
1017
|
+
source_scope: 'workflow',
|
|
1018
|
+
navigation_only: false,
|
|
1019
|
+
can_instruct_agent: true,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function commandIntentAuthority() {
|
|
1023
|
+
return {
|
|
1024
|
+
authority_rank: 1,
|
|
1025
|
+
authority_label: 'command_contract',
|
|
1026
|
+
source_scope: 'workflow',
|
|
1027
|
+
navigation_only: false,
|
|
1028
|
+
can_instruct_agent: true,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function sourceAnchorAuthority() {
|
|
1032
|
+
return {
|
|
1033
|
+
authority_rank: 5,
|
|
1034
|
+
authority_label: 'source_navigation_hint',
|
|
1035
|
+
source_scope: 'source',
|
|
1036
|
+
navigation_only: true,
|
|
1037
|
+
can_instruct_agent: false,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function getMatchSnippet(fields, query) {
|
|
1041
|
+
const normalized = normalizeSearchText(fields.join(' '));
|
|
1042
|
+
const lower = normalized.toLowerCase();
|
|
1043
|
+
let start = lower.indexOf(query.toLowerCase());
|
|
1044
|
+
let matchLength = query.length;
|
|
1045
|
+
if (start === -1) {
|
|
1046
|
+
const [firstGram] = buildSearchNgrams([query]).filter((gram) => lower.includes(gram));
|
|
1047
|
+
if (!firstGram) {
|
|
1048
|
+
return truncateSearchMatchSnippet(normalized);
|
|
1049
|
+
}
|
|
1050
|
+
start = lower.indexOf(firstGram);
|
|
1051
|
+
matchLength = firstGram.length;
|
|
1052
|
+
}
|
|
1053
|
+
const from = Math.max(0, start - SEARCH_MATCH_CONTEXT_BEFORE_CHARS);
|
|
1054
|
+
const to = Math.min(normalized.length, start + matchLength + SEARCH_MATCH_CONTEXT_AFTER_CHARS);
|
|
1055
|
+
const prefix = from > 0 ? SEARCH_MATCH_TRUNCATION_MARKER : '';
|
|
1056
|
+
const suffix = to < normalized.length ? SEARCH_MATCH_TRUNCATION_MARKER : '';
|
|
1057
|
+
return truncateSearchMatchSnippet(`${prefix}${normalized.slice(from, to)}${suffix}`);
|
|
1058
|
+
}
|
|
1059
|
+
function truncateSearchMatchSnippet(value) {
|
|
1060
|
+
if (value.length <= MAX_SEARCH_MATCH_SNIPPET_CHARS) {
|
|
1061
|
+
return value;
|
|
1062
|
+
}
|
|
1063
|
+
return `${value.slice(0, MAX_SEARCH_MATCH_SNIPPET_CHARS - SEARCH_MATCH_TRUNCATION_MARKER.length)}${SEARCH_MATCH_TRUNCATION_MARKER}`;
|
|
1064
|
+
}
|
|
1065
|
+
function scoreMatch(primaryFields, secondaryFields, query) {
|
|
1066
|
+
const lowerQuery = query.toLowerCase();
|
|
1067
|
+
if (primaryFields.some((field) => field.toLowerCase() === lowerQuery)) {
|
|
1068
|
+
return 100;
|
|
1069
|
+
}
|
|
1070
|
+
if (primaryFields.some((field) => field.toLowerCase().includes(lowerQuery))) {
|
|
1071
|
+
return 80;
|
|
1072
|
+
}
|
|
1073
|
+
if (secondaryFields.some((field) => field.toLowerCase().includes(lowerQuery))) {
|
|
1074
|
+
return 40;
|
|
1075
|
+
}
|
|
1076
|
+
return 0;
|
|
1077
|
+
}
|
|
1078
|
+
function isMatched(fields, query) {
|
|
1079
|
+
const lowerQuery = query.toLowerCase();
|
|
1080
|
+
return fields.some((field) => field.toLowerCase().includes(lowerQuery));
|
|
1081
|
+
}
|
|
1082
|
+
function createSchema(database, capabilities) {
|
|
1083
|
+
database.run(`
|
|
1084
|
+
CREATE TABLE metadata (
|
|
1085
|
+
key TEXT PRIMARY KEY,
|
|
1086
|
+
value TEXT NOT NULL
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
CREATE TABLE indexed_files (
|
|
1090
|
+
path TEXT PRIMARY KEY,
|
|
1091
|
+
source_scope TEXT NOT NULL,
|
|
1092
|
+
size_bytes INTEGER NOT NULL,
|
|
1093
|
+
mtime_ms INTEGER NOT NULL,
|
|
1094
|
+
content_hash TEXT NOT NULL,
|
|
1095
|
+
indexed_at TEXT NOT NULL,
|
|
1096
|
+
index_mode TEXT NOT NULL,
|
|
1097
|
+
parser_version TEXT NOT NULL
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
CREATE TABLE documents (
|
|
1101
|
+
path TEXT PRIMARY KEY,
|
|
1102
|
+
type TEXT NOT NULL,
|
|
1103
|
+
title TEXT NOT NULL,
|
|
1104
|
+
locale TEXT,
|
|
1105
|
+
revision INTEGER,
|
|
1106
|
+
content_hash TEXT NOT NULL,
|
|
1107
|
+
content_snippet TEXT NOT NULL
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
CREATE TABLE sections (
|
|
1111
|
+
document_path TEXT NOT NULL,
|
|
1112
|
+
ordinal INTEGER NOT NULL,
|
|
1113
|
+
heading TEXT NOT NULL,
|
|
1114
|
+
PRIMARY KEY (document_path, ordinal)
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
CREATE TABLE document_terms (
|
|
1118
|
+
document_path TEXT NOT NULL,
|
|
1119
|
+
term TEXT NOT NULL,
|
|
1120
|
+
source TEXT NOT NULL,
|
|
1121
|
+
PRIMARY KEY (document_path, term, source)
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
CREATE TABLE search_ngrams (
|
|
1125
|
+
target_kind TEXT NOT NULL,
|
|
1126
|
+
target_key TEXT NOT NULL,
|
|
1127
|
+
gram TEXT NOT NULL,
|
|
1128
|
+
source TEXT NOT NULL,
|
|
1129
|
+
PRIMARY KEY (target_kind, target_key, gram, source)
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
CREATE INDEX search_ngrams_lookup ON search_ngrams(target_kind, gram, target_key);
|
|
1133
|
+
|
|
1134
|
+
CREATE TABLE skills (
|
|
1135
|
+
name TEXT PRIMARY KEY,
|
|
1136
|
+
path TEXT NOT NULL,
|
|
1137
|
+
title TEXT NOT NULL
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
CREATE TABLE skill_routes (
|
|
1141
|
+
skill_name TEXT NOT NULL,
|
|
1142
|
+
skill_path TEXT NOT NULL,
|
|
1143
|
+
trigger TEXT NOT NULL,
|
|
1144
|
+
required_input TEXT NOT NULL,
|
|
1145
|
+
edit_scope TEXT NOT NULL,
|
|
1146
|
+
risk TEXT NOT NULL,
|
|
1147
|
+
verification_intents TEXT NOT NULL,
|
|
1148
|
+
expected_output TEXT NOT NULL,
|
|
1149
|
+
PRIMARY KEY (skill_name, trigger)
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
CREATE TABLE command_intents (
|
|
1153
|
+
name TEXT PRIMARY KEY,
|
|
1154
|
+
status TEXT NOT NULL,
|
|
1155
|
+
lifecycle TEXT,
|
|
1156
|
+
run_policy TEXT,
|
|
1157
|
+
description TEXT
|
|
1158
|
+
);
|
|
1159
|
+
|
|
1160
|
+
CREATE TABLE command_effects (
|
|
1161
|
+
intent TEXT NOT NULL,
|
|
1162
|
+
source TEXT NOT NULL,
|
|
1163
|
+
access TEXT NOT NULL,
|
|
1164
|
+
mode TEXT NOT NULL,
|
|
1165
|
+
path TEXT,
|
|
1166
|
+
lock TEXT NOT NULL,
|
|
1167
|
+
concurrency TEXT NOT NULL
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
CREATE VIEW command_write_locks AS
|
|
1171
|
+
SELECT
|
|
1172
|
+
intent,
|
|
1173
|
+
lock,
|
|
1174
|
+
group_concat(DISTINCT path) AS paths,
|
|
1175
|
+
group_concat(DISTINCT mode) AS modes,
|
|
1176
|
+
group_concat(DISTINCT source) AS sources,
|
|
1177
|
+
group_concat(DISTINCT concurrency) AS concurrencies,
|
|
1178
|
+
count(*) AS effect_count
|
|
1179
|
+
FROM command_effects
|
|
1180
|
+
WHERE access = 'write'
|
|
1181
|
+
GROUP BY intent, lock;
|
|
1182
|
+
|
|
1183
|
+
CREATE VIEW command_lock_conflicts AS
|
|
1184
|
+
SELECT
|
|
1185
|
+
a.intent AS left_intent,
|
|
1186
|
+
b.intent AS right_intent,
|
|
1187
|
+
a.lock AS lock,
|
|
1188
|
+
group_concat(DISTINCT a.path) AS left_paths,
|
|
1189
|
+
group_concat(DISTINCT b.path) AS right_paths,
|
|
1190
|
+
group_concat(DISTINCT a.mode) AS left_modes,
|
|
1191
|
+
group_concat(DISTINCT b.mode) AS right_modes,
|
|
1192
|
+
group_concat(DISTINCT a.concurrency) AS left_concurrencies,
|
|
1193
|
+
group_concat(DISTINCT b.concurrency) AS right_concurrencies
|
|
1194
|
+
FROM command_effects a
|
|
1195
|
+
JOIN command_effects b
|
|
1196
|
+
ON a.lock = b.lock
|
|
1197
|
+
AND a.intent < b.intent
|
|
1198
|
+
WHERE
|
|
1199
|
+
a.access = 'write'
|
|
1200
|
+
OR b.access = 'write'
|
|
1201
|
+
OR a.concurrency = 'exclusive'
|
|
1202
|
+
OR b.concurrency = 'exclusive'
|
|
1203
|
+
OR a.mode = 'delete_recreate'
|
|
1204
|
+
OR b.mode = 'delete_recreate'
|
|
1205
|
+
GROUP BY a.intent, b.intent, a.lock;
|
|
1206
|
+
|
|
1207
|
+
CREATE TABLE path_surfaces (
|
|
1208
|
+
rule_id TEXT PRIMARY KEY,
|
|
1209
|
+
pattern_kind TEXT NOT NULL,
|
|
1210
|
+
pattern TEXT NOT NULL,
|
|
1211
|
+
pattern_flags TEXT NOT NULL,
|
|
1212
|
+
surface_kind TEXT NOT NULL,
|
|
1213
|
+
category TEXT NOT NULL,
|
|
1214
|
+
is_public_surface INTEGER NOT NULL,
|
|
1215
|
+
update_policy TEXT NOT NULL
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
CREATE TABLE path_surface_reasons (
|
|
1219
|
+
rule_id TEXT NOT NULL,
|
|
1220
|
+
reason_kind TEXT NOT NULL,
|
|
1221
|
+
reason TEXT NOT NULL,
|
|
1222
|
+
ordinal INTEGER NOT NULL,
|
|
1223
|
+
PRIMARY KEY (rule_id, reason_kind, reason)
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
CREATE TABLE source_anchors (
|
|
1227
|
+
id TEXT PRIMARY KEY,
|
|
1228
|
+
path TEXT NOT NULL,
|
|
1229
|
+
line_start INTEGER NOT NULL,
|
|
1230
|
+
purpose TEXT,
|
|
1231
|
+
search_terms TEXT NOT NULL,
|
|
1232
|
+
invariant TEXT,
|
|
1233
|
+
risk TEXT NOT NULL,
|
|
1234
|
+
navigation_only INTEGER NOT NULL,
|
|
1235
|
+
can_instruct_agent INTEGER NOT NULL
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
CREATE TABLE source_anchor_fingerprints (
|
|
1239
|
+
anchor_id TEXT PRIMARY KEY,
|
|
1240
|
+
path TEXT NOT NULL,
|
|
1241
|
+
line_start INTEGER NOT NULL,
|
|
1242
|
+
anchor_metadata_hash TEXT NOT NULL,
|
|
1243
|
+
anchor_text_hash TEXT NOT NULL,
|
|
1244
|
+
context_hash TEXT NOT NULL,
|
|
1245
|
+
search_terms_hash TEXT,
|
|
1246
|
+
invariant_hash TEXT,
|
|
1247
|
+
risk_hash TEXT NOT NULL,
|
|
1248
|
+
symbol_kind TEXT NOT NULL,
|
|
1249
|
+
symbol_name TEXT,
|
|
1250
|
+
symbol_exported INTEGER NOT NULL,
|
|
1251
|
+
signature_hash TEXT,
|
|
1252
|
+
body_hash TEXT,
|
|
1253
|
+
symbol_start_line INTEGER,
|
|
1254
|
+
symbol_end_line INTEGER
|
|
1255
|
+
);
|
|
1256
|
+
|
|
1257
|
+
CREATE TABLE source_anchor_status (
|
|
1258
|
+
anchor_id TEXT PRIMARY KEY,
|
|
1259
|
+
status TEXT NOT NULL,
|
|
1260
|
+
confidence REAL NOT NULL,
|
|
1261
|
+
identity_signal TEXT NOT NULL,
|
|
1262
|
+
location_signal TEXT NOT NULL,
|
|
1263
|
+
symbol_signal TEXT NOT NULL,
|
|
1264
|
+
body_signal TEXT NOT NULL,
|
|
1265
|
+
metadata_signal TEXT NOT NULL,
|
|
1266
|
+
semantic_signal TEXT NOT NULL,
|
|
1267
|
+
risk_signal TEXT NOT NULL,
|
|
1268
|
+
navigation_only INTEGER NOT NULL,
|
|
1269
|
+
can_instruct_agent INTEGER NOT NULL
|
|
1270
|
+
);
|
|
1271
|
+
|
|
1272
|
+
CREATE TABLE verification_evidence_summaries (
|
|
1273
|
+
source_path TEXT PRIMARY KEY,
|
|
1274
|
+
source_hash TEXT NOT NULL,
|
|
1275
|
+
command TEXT NOT NULL,
|
|
1276
|
+
kind TEXT NOT NULL,
|
|
1277
|
+
status TEXT NOT NULL,
|
|
1278
|
+
run_dir TEXT,
|
|
1279
|
+
manifest_path TEXT,
|
|
1280
|
+
verification_plan_id TEXT,
|
|
1281
|
+
completion_status TEXT,
|
|
1282
|
+
primary_reason TEXT,
|
|
1283
|
+
matched_intents INTEGER NOT NULL,
|
|
1284
|
+
ran_intents INTEGER NOT NULL,
|
|
1285
|
+
passed_intents INTEGER NOT NULL,
|
|
1286
|
+
failed_intents INTEGER NOT NULL,
|
|
1287
|
+
skipped_intents INTEGER NOT NULL,
|
|
1288
|
+
receipt_count INTEGER NOT NULL,
|
|
1289
|
+
coverage_count INTEGER NOT NULL,
|
|
1290
|
+
remaining_risk_count INTEGER NOT NULL,
|
|
1291
|
+
failure_fingerprint TEXT
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
CREATE TABLE verification_plans (
|
|
1295
|
+
plan_id TEXT PRIMARY KEY,
|
|
1296
|
+
source_path TEXT NOT NULL,
|
|
1297
|
+
classification_hash TEXT,
|
|
1298
|
+
command_contract_hash TEXT,
|
|
1299
|
+
selected_intents_hash TEXT,
|
|
1300
|
+
created_at TEXT,
|
|
1301
|
+
source_hash TEXT NOT NULL
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
CREATE TABLE acceptance_criteria (
|
|
1305
|
+
criterion_id TEXT NOT NULL,
|
|
1306
|
+
plan_id TEXT NOT NULL,
|
|
1307
|
+
source TEXT NOT NULL,
|
|
1308
|
+
statement_hash TEXT,
|
|
1309
|
+
reason TEXT,
|
|
1310
|
+
surface TEXT,
|
|
1311
|
+
path_hash TEXT,
|
|
1312
|
+
PRIMARY KEY (plan_id, criterion_id)
|
|
1313
|
+
);
|
|
1314
|
+
|
|
1315
|
+
CREATE TABLE criterion_coverage (
|
|
1316
|
+
criterion_id TEXT NOT NULL,
|
|
1317
|
+
plan_id TEXT NOT NULL,
|
|
1318
|
+
status TEXT NOT NULL,
|
|
1319
|
+
receipt_count INTEGER NOT NULL,
|
|
1320
|
+
gap_count INTEGER NOT NULL,
|
|
1321
|
+
risk_count INTEGER NOT NULL,
|
|
1322
|
+
PRIMARY KEY (plan_id, criterion_id)
|
|
1323
|
+
);
|
|
1324
|
+
|
|
1325
|
+
CREATE TABLE verification_receipt_summaries (
|
|
1326
|
+
source_path TEXT NOT NULL,
|
|
1327
|
+
ordinal INTEGER NOT NULL,
|
|
1328
|
+
intent TEXT,
|
|
1329
|
+
status TEXT NOT NULL,
|
|
1330
|
+
skipped INTEGER NOT NULL,
|
|
1331
|
+
verification_plan_id TEXT,
|
|
1332
|
+
receipt_path TEXT,
|
|
1333
|
+
receipt_sha256 TEXT,
|
|
1334
|
+
PRIMARY KEY (source_path, ordinal)
|
|
1335
|
+
);
|
|
1336
|
+
|
|
1337
|
+
CREATE TABLE command_receipt_summaries (
|
|
1338
|
+
receipt_hash TEXT NOT NULL,
|
|
1339
|
+
plan_id TEXT NOT NULL,
|
|
1340
|
+
intent TEXT,
|
|
1341
|
+
status TEXT NOT NULL,
|
|
1342
|
+
command_fingerprint TEXT,
|
|
1343
|
+
contract_fingerprint TEXT,
|
|
1344
|
+
current_state_hash TEXT,
|
|
1345
|
+
write_drift_status TEXT,
|
|
1346
|
+
PRIMARY KEY (plan_id, receipt_hash)
|
|
1347
|
+
);
|
|
1348
|
+
|
|
1349
|
+
CREATE TABLE verification_coverage_states (
|
|
1350
|
+
source_path TEXT NOT NULL,
|
|
1351
|
+
criterion_id TEXT NOT NULL,
|
|
1352
|
+
source TEXT NOT NULL,
|
|
1353
|
+
status TEXT NOT NULL,
|
|
1354
|
+
requirement_reason TEXT,
|
|
1355
|
+
intents TEXT NOT NULL,
|
|
1356
|
+
receipt_count INTEGER NOT NULL,
|
|
1357
|
+
gap_count INTEGER NOT NULL,
|
|
1358
|
+
source_anchor_count INTEGER NOT NULL,
|
|
1359
|
+
PRIMARY KEY (source_path, criterion_id)
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
CREATE TABLE verification_risk_signals (
|
|
1363
|
+
source_path TEXT NOT NULL,
|
|
1364
|
+
ordinal INTEGER NOT NULL,
|
|
1365
|
+
code TEXT NOT NULL,
|
|
1366
|
+
severity TEXT NOT NULL,
|
|
1367
|
+
detail_hash TEXT NOT NULL,
|
|
1368
|
+
PRIMARY KEY (source_path, ordinal)
|
|
1369
|
+
);
|
|
1370
|
+
|
|
1371
|
+
CREATE TABLE validation_ratchet_signals (
|
|
1372
|
+
signal_id TEXT PRIMARY KEY,
|
|
1373
|
+
plan_id TEXT,
|
|
1374
|
+
code TEXT NOT NULL,
|
|
1375
|
+
severity TEXT NOT NULL,
|
|
1376
|
+
path_hash TEXT NOT NULL,
|
|
1377
|
+
before_hash TEXT,
|
|
1378
|
+
after_hash TEXT
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
CREATE TABLE completion_verdict_summaries (
|
|
1382
|
+
claim_id TEXT PRIMARY KEY,
|
|
1383
|
+
plan_id TEXT NOT NULL,
|
|
1384
|
+
status TEXT NOT NULL,
|
|
1385
|
+
primary_reason TEXT,
|
|
1386
|
+
risk_count INTEGER NOT NULL,
|
|
1387
|
+
contradiction_count INTEGER NOT NULL,
|
|
1388
|
+
blocker_count INTEGER NOT NULL
|
|
1389
|
+
);
|
|
1390
|
+
|
|
1391
|
+
CREATE TABLE repro_routes (
|
|
1392
|
+
route_id TEXT PRIMARY KEY,
|
|
1393
|
+
task_hash TEXT NOT NULL,
|
|
1394
|
+
route_digest TEXT,
|
|
1395
|
+
route_kind TEXT,
|
|
1396
|
+
failure_oracle_hash TEXT
|
|
1397
|
+
);
|
|
1398
|
+
|
|
1399
|
+
CREATE TABLE repro_observations (
|
|
1400
|
+
route_id TEXT NOT NULL,
|
|
1401
|
+
phase TEXT NOT NULL,
|
|
1402
|
+
outcome TEXT,
|
|
1403
|
+
receipt_hash TEXT,
|
|
1404
|
+
diagnostic_fingerprint TEXT NOT NULL,
|
|
1405
|
+
PRIMARY KEY (route_id, phase)
|
|
1406
|
+
);
|
|
1407
|
+
|
|
1408
|
+
CREATE TABLE failure_fingerprints (
|
|
1409
|
+
fingerprint TEXT PRIMARY KEY,
|
|
1410
|
+
plan_id TEXT,
|
|
1411
|
+
failed_intents_hash TEXT,
|
|
1412
|
+
risk_codes_hash TEXT,
|
|
1413
|
+
seen_count INTEGER NOT NULL,
|
|
1414
|
+
first_seen_at TEXT,
|
|
1415
|
+
last_seen_at TEXT
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
CREATE TABLE verification_failure_fingerprints (
|
|
1419
|
+
source_path TEXT NOT NULL,
|
|
1420
|
+
fingerprint TEXT NOT NULL,
|
|
1421
|
+
verification_plan_id TEXT,
|
|
1422
|
+
status TEXT NOT NULL,
|
|
1423
|
+
failed_intents TEXT NOT NULL,
|
|
1424
|
+
primary_reason TEXT,
|
|
1425
|
+
failed_intents_hash TEXT,
|
|
1426
|
+
risk_codes_hash TEXT,
|
|
1427
|
+
affected_surfaces_hash TEXT,
|
|
1428
|
+
first_seen_at TEXT,
|
|
1429
|
+
last_seen_at TEXT,
|
|
1430
|
+
seen_count INTEGER NOT NULL,
|
|
1431
|
+
requires_new_evidence INTEGER NOT NULL,
|
|
1432
|
+
PRIMARY KEY (source_path, fingerprint)
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
CREATE TABLE source_anchor_risk_signals (
|
|
1436
|
+
anchor_id TEXT PRIMARY KEY,
|
|
1437
|
+
path_hash TEXT NOT NULL,
|
|
1438
|
+
status TEXT NOT NULL,
|
|
1439
|
+
risk_signal TEXT NOT NULL,
|
|
1440
|
+
confidence REAL NOT NULL,
|
|
1441
|
+
navigation_only INTEGER NOT NULL,
|
|
1442
|
+
can_instruct_agent INTEGER NOT NULL
|
|
1443
|
+
);
|
|
1444
|
+
`);
|
|
1445
|
+
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
1446
|
+
database.run(`
|
|
1447
|
+
CREATE VIRTUAL TABLE search_documents_fts USING fts5(
|
|
1448
|
+
path UNINDEXED,
|
|
1449
|
+
type UNINDEXED,
|
|
1450
|
+
title,
|
|
1451
|
+
sections,
|
|
1452
|
+
terms,
|
|
1453
|
+
snippet
|
|
1454
|
+
);
|
|
1455
|
+
|
|
1456
|
+
CREATE VIRTUAL TABLE search_skills_fts USING fts5(
|
|
1457
|
+
name UNINDEXED,
|
|
1458
|
+
path UNINDEXED,
|
|
1459
|
+
title
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
CREATE VIRTUAL TABLE search_skill_routes_fts USING fts5(
|
|
1463
|
+
route_key UNINDEXED,
|
|
1464
|
+
skill_name UNINDEXED,
|
|
1465
|
+
skill_path UNINDEXED,
|
|
1466
|
+
trigger,
|
|
1467
|
+
required_input,
|
|
1468
|
+
edit_scope,
|
|
1469
|
+
risk,
|
|
1470
|
+
verification_intents,
|
|
1471
|
+
expected_output
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
CREATE VIRTUAL TABLE search_command_intents_fts USING fts5(
|
|
1475
|
+
name UNINDEXED,
|
|
1476
|
+
status UNINDEXED,
|
|
1477
|
+
lifecycle UNINDEXED,
|
|
1478
|
+
run_policy UNINDEXED,
|
|
1479
|
+
description,
|
|
1480
|
+
effects
|
|
1481
|
+
);
|
|
1482
|
+
|
|
1483
|
+
CREATE VIRTUAL TABLE search_source_anchors_fts USING fts5(
|
|
1484
|
+
id UNINDEXED,
|
|
1485
|
+
path UNINDEXED,
|
|
1486
|
+
purpose,
|
|
1487
|
+
search_terms,
|
|
1488
|
+
invariant,
|
|
1489
|
+
risk
|
|
1490
|
+
);
|
|
1491
|
+
`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
function insertDocumentTerm(database, documentPath, term, source) {
|
|
1495
|
+
const normalized = normalizeSearchText(term ?? '');
|
|
1496
|
+
if (normalized.length === 0) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
database.run('INSERT OR IGNORE INTO document_terms (document_path, term, source) VALUES (?, ?, ?)', [
|
|
1500
|
+
documentPath,
|
|
1501
|
+
normalized,
|
|
1502
|
+
source,
|
|
1503
|
+
]);
|
|
1504
|
+
}
|
|
1505
|
+
function insertSearchNgrams(database, targetKind, targetKey, values, source) {
|
|
1506
|
+
for (const gram of buildSearchNgrams(values)) {
|
|
1507
|
+
database.run('INSERT OR IGNORE INTO search_ngrams (target_kind, target_key, gram, source) VALUES (?, ?, ?, ?)', [targetKind, targetKey, gram, source]);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function insertPathSurfaceReasons(database, ruleId, reasonKind, values) {
|
|
1511
|
+
values.forEach((value, index) => {
|
|
1512
|
+
database.run('INSERT INTO path_surface_reasons (rule_id, reason_kind, reason, ordinal) VALUES (?, ?, ?, ?)', [ruleId, reasonKind, value, index + 1]);
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
function populatePathSurfaceReadModel(database) {
|
|
1516
|
+
for (const rule of listChangeClassificationRuleDescriptors()) {
|
|
1517
|
+
database.run('INSERT INTO path_surfaces (rule_id, pattern_kind, pattern, pattern_flags, surface_kind, category, is_public_surface, update_policy) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1518
|
+
rule.id,
|
|
1519
|
+
rule.patternKind,
|
|
1520
|
+
rule.pattern,
|
|
1521
|
+
rule.patternFlags,
|
|
1522
|
+
rule.surface.kind,
|
|
1523
|
+
rule.surface.category,
|
|
1524
|
+
rule.surface.isPublicSurface ? 1 : 0,
|
|
1525
|
+
rule.surface.updatePolicy,
|
|
1526
|
+
]);
|
|
1527
|
+
insertPathSurfaceReasons(database, rule.id, 'change_kind', rule.changeKinds);
|
|
1528
|
+
insertPathSurfaceReasons(database, rule.id, 'validation_reason', rule.surface.validationReasons);
|
|
1529
|
+
insertPathSurfaceReasons(database, rule.id, 'affected_contract', rule.surface.affectedContracts);
|
|
1530
|
+
insertPathSurfaceReasons(database, rule.id, 'drift_check', rule.surface.driftChecks);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
function skillRouteKey(route) {
|
|
1534
|
+
return `${route.skillName}\u0000${route.trigger}`;
|
|
1535
|
+
}
|
|
1536
|
+
function populateSearchTables(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors) {
|
|
1537
|
+
for (const document of documents) {
|
|
1538
|
+
const documentTerms = queryRows(database, 'SELECT term FROM document_terms WHERE document_path = ? ORDER BY term', [
|
|
1539
|
+
document.path,
|
|
1540
|
+
]).map((row) => toSearchString(row.term));
|
|
1541
|
+
insertSearchNgrams(database, 'document', document.path, [
|
|
1542
|
+
document.path,
|
|
1543
|
+
document.type,
|
|
1544
|
+
document.title,
|
|
1545
|
+
document.sections.join(' '),
|
|
1546
|
+
documentTerms.join(' '),
|
|
1547
|
+
document.contentSnippet,
|
|
1548
|
+
], 'workflow_document');
|
|
1549
|
+
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
1550
|
+
database.run('INSERT INTO search_documents_fts (path, type, title, sections, terms, snippet) VALUES (?, ?, ?, ?, ?, ?)', [
|
|
1551
|
+
document.path,
|
|
1552
|
+
document.type,
|
|
1553
|
+
document.title,
|
|
1554
|
+
document.sections.join(' '),
|
|
1555
|
+
documentTerms.join(' '),
|
|
1556
|
+
document.contentSnippet,
|
|
1557
|
+
]);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
for (const skill of skills) {
|
|
1561
|
+
insertSearchNgrams(database, 'skill', skill.name, [skill.name, skill.path, skill.title], 'skill');
|
|
1562
|
+
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
1563
|
+
database.run('INSERT INTO search_skills_fts (name, path, title) VALUES (?, ?, ?)', [
|
|
1564
|
+
skill.name,
|
|
1565
|
+
skill.path,
|
|
1566
|
+
skill.title,
|
|
1567
|
+
]);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
for (const route of skillRoutes) {
|
|
1571
|
+
const verificationIntents = route.verificationIntents.join(' ');
|
|
1572
|
+
insertSearchNgrams(database, 'skill_route', skillRouteKey(route), [
|
|
1573
|
+
skillRouteKey(route),
|
|
1574
|
+
route.skillName,
|
|
1575
|
+
route.skillPath,
|
|
1576
|
+
route.trigger,
|
|
1577
|
+
route.requiredInput,
|
|
1578
|
+
route.editScope,
|
|
1579
|
+
route.risk,
|
|
1580
|
+
verificationIntents,
|
|
1581
|
+
route.expectedOutput,
|
|
1582
|
+
], 'skill_route');
|
|
1583
|
+
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
1584
|
+
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 (?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1585
|
+
skillRouteKey(route),
|
|
1586
|
+
route.skillName,
|
|
1587
|
+
route.skillPath,
|
|
1588
|
+
route.trigger,
|
|
1589
|
+
route.requiredInput,
|
|
1590
|
+
route.editScope,
|
|
1591
|
+
route.risk,
|
|
1592
|
+
verificationIntents,
|
|
1593
|
+
route.expectedOutput,
|
|
1594
|
+
]);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
for (const intent of commandIntents) {
|
|
1598
|
+
const effects = intent.effects
|
|
1599
|
+
.flatMap((effect) => [effect.lock, effect.path ?? '', effect.mode, effect.access, effect.concurrency])
|
|
1600
|
+
.join(' ');
|
|
1601
|
+
insertSearchNgrams(database, 'command_intent', intent.name, [intent.name, intent.status, intent.lifecycle ?? '', intent.runPolicy ?? '', intent.description ?? '', effects], 'command_intent');
|
|
1602
|
+
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
1603
|
+
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]);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
for (const anchor of sourceAnchors) {
|
|
1607
|
+
insertSearchNgrams(database, 'source_anchor', anchor.id, [
|
|
1608
|
+
anchor.id,
|
|
1609
|
+
anchor.path,
|
|
1610
|
+
anchor.purpose ?? '',
|
|
1611
|
+
anchor.search.join(' '),
|
|
1612
|
+
anchor.invariant ?? '',
|
|
1613
|
+
anchor.risk.join(' '),
|
|
1614
|
+
], 'source_anchor');
|
|
1615
|
+
if (capabilities.backend === SEARCH_BACKEND_FTS5) {
|
|
1616
|
+
database.run('INSERT INTO search_source_anchors_fts (id, path, purpose, search_terms, invariant, risk) VALUES (?, ?, ?, ?, ?, ?)', [
|
|
1617
|
+
anchor.id,
|
|
1618
|
+
anchor.path,
|
|
1619
|
+
anchor.purpose,
|
|
1620
|
+
anchor.search.join(' '),
|
|
1621
|
+
anchor.invariant,
|
|
1622
|
+
anchor.risk.join(' '),
|
|
1623
|
+
]);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
function createSourceAnchorRiskSignals(sourceAnchors) {
|
|
1628
|
+
return sourceAnchors
|
|
1629
|
+
.filter((anchor) => ['changed', 'review', 'stale'].includes(anchor.status))
|
|
1630
|
+
.map((anchor) => ({
|
|
1631
|
+
anchorId: anchor.id,
|
|
1632
|
+
pathHash: sha256Text(anchor.path),
|
|
1633
|
+
status: anchor.status,
|
|
1634
|
+
riskSignal: anchor.signals.risk,
|
|
1635
|
+
confidence: anchor.confidence,
|
|
1636
|
+
navigationOnly: anchor.navigationOnly,
|
|
1637
|
+
canInstructAgent: anchor.canInstructAgent,
|
|
1638
|
+
}));
|
|
1639
|
+
}
|
|
1640
|
+
function populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, verificationEvidence, indexMode, sourceScopeHash, sourceIndexEnabled, indexedAt) {
|
|
1641
|
+
const sourceAnchorRiskSignals = createSourceAnchorRiskSignals(sourceAnchors);
|
|
1642
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['schema_version', LOCAL_INDEX_SCHEMA_VERSION]);
|
|
1643
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['parser_version', LOCAL_INDEX_PARSER_VERSION]);
|
|
1644
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['content_mode', LOCAL_INDEX_CONTENT_MODE]);
|
|
1645
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1646
|
+
'store_full_content',
|
|
1647
|
+
String(LOCAL_INDEX_STORE_FULL_CONTENT),
|
|
1648
|
+
]);
|
|
1649
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1650
|
+
'max_snippet_bytes_per_document',
|
|
1651
|
+
String(MAX_SNIPPET_BYTES_PER_DOCUMENT),
|
|
1652
|
+
]);
|
|
1653
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1654
|
+
'excluded_raw_data_kinds',
|
|
1655
|
+
LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS.join(','),
|
|
1656
|
+
]);
|
|
1657
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['search_backend', capabilities.backend]);
|
|
1658
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1659
|
+
'search_fts5_available',
|
|
1660
|
+
String(capabilities.fts5Available),
|
|
1661
|
+
]);
|
|
1662
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['source_scope_hash', sourceScopeHash]);
|
|
1663
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['source_index_enabled', String(sourceIndexEnabled)]);
|
|
1664
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', ['index_mode', indexMode]);
|
|
1665
|
+
for (const indexedFile of indexedFiles) {
|
|
1666
|
+
database.run('INSERT INTO indexed_files (path, source_scope, size_bytes, mtime_ms, content_hash, indexed_at, index_mode, parser_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1667
|
+
indexedFile.path,
|
|
1668
|
+
indexedFile.sourceScope,
|
|
1669
|
+
indexedFile.sizeBytes,
|
|
1670
|
+
indexedFile.mtimeMs,
|
|
1671
|
+
indexedFile.contentHash,
|
|
1672
|
+
indexedAt,
|
|
1673
|
+
indexMode,
|
|
1674
|
+
LOCAL_INDEX_PARSER_VERSION,
|
|
1675
|
+
]);
|
|
1676
|
+
}
|
|
1677
|
+
for (const document of documents) {
|
|
1678
|
+
database.run('INSERT INTO documents (path, type, title, locale, revision, content_hash, content_snippet) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
|
1679
|
+
document.path,
|
|
1680
|
+
document.type,
|
|
1681
|
+
document.title,
|
|
1682
|
+
document.locale,
|
|
1683
|
+
document.revision,
|
|
1684
|
+
document.contentHash,
|
|
1685
|
+
document.contentSnippet,
|
|
1686
|
+
]);
|
|
1687
|
+
document.sections.forEach((heading, index) => {
|
|
1688
|
+
database.run('INSERT INTO sections (document_path, ordinal, heading) VALUES (?, ?, ?)', [
|
|
1689
|
+
document.path,
|
|
1690
|
+
index + 1,
|
|
1691
|
+
heading,
|
|
1692
|
+
]);
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
for (const skill of skills) {
|
|
1696
|
+
database.run('INSERT INTO skills (name, path, title) VALUES (?, ?, ?)', [skill.name, skill.path, skill.title]);
|
|
1697
|
+
}
|
|
1698
|
+
for (const route of skillRoutes) {
|
|
1699
|
+
const verificationIntents = route.verificationIntents.join(', ');
|
|
1700
|
+
database.run('INSERT INTO skill_routes (skill_name, skill_path, trigger, required_input, edit_scope, risk, verification_intents, expected_output) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1701
|
+
route.skillName,
|
|
1702
|
+
route.skillPath,
|
|
1703
|
+
route.trigger,
|
|
1704
|
+
route.requiredInput,
|
|
1705
|
+
route.editScope,
|
|
1706
|
+
route.risk,
|
|
1707
|
+
verificationIntents,
|
|
1708
|
+
route.expectedOutput,
|
|
1709
|
+
]);
|
|
1710
|
+
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.skillName, 'skill_route_name');
|
|
1711
|
+
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.trigger, 'skill_route_trigger');
|
|
1712
|
+
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.risk, 'skill_route_risk');
|
|
1713
|
+
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.requiredInput, 'skill_route_required_input');
|
|
1714
|
+
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', route.editScope, 'skill_route_edit_scope');
|
|
1715
|
+
insertDocumentTerm(database, '.mustflow/skills/INDEX.md', verificationIntents, 'skill_route_verification_intents');
|
|
1716
|
+
}
|
|
1717
|
+
for (const intent of commandIntents) {
|
|
1718
|
+
database.run('INSERT INTO command_intents (name, status, lifecycle, run_policy, description) VALUES (?, ?, ?, ?, ?)', [intent.name, intent.status, intent.lifecycle, intent.runPolicy, intent.description]);
|
|
1719
|
+
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.name, 'command_intent');
|
|
1720
|
+
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.status, 'command_status');
|
|
1721
|
+
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.lifecycle, 'command_lifecycle');
|
|
1722
|
+
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.runPolicy, 'command_run_policy');
|
|
1723
|
+
insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.description, 'command_description');
|
|
1724
|
+
for (const effect of intent.effects) {
|
|
1725
|
+
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]);
|
|
1726
|
+
if (effect.path !== null) {
|
|
1727
|
+
insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.path, 'command_effect_path');
|
|
1728
|
+
}
|
|
1729
|
+
insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.lock, 'command_effect_lock');
|
|
1730
|
+
insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.mode, 'command_effect_mode');
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
for (const anchor of sourceAnchors) {
|
|
1734
|
+
database.run('INSERT INTO source_anchors (id, path, line_start, purpose, search_terms, invariant, risk, navigation_only, can_instruct_agent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1735
|
+
anchor.id,
|
|
1736
|
+
anchor.path,
|
|
1737
|
+
anchor.lineStart,
|
|
1738
|
+
anchor.purpose,
|
|
1739
|
+
anchor.search.join(', '),
|
|
1740
|
+
anchor.invariant,
|
|
1741
|
+
anchor.risk.join(', '),
|
|
1742
|
+
anchor.navigationOnly ? 1 : 0,
|
|
1743
|
+
anchor.canInstructAgent ? 1 : 0,
|
|
1744
|
+
]);
|
|
1745
|
+
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1746
|
+
anchor.id,
|
|
1747
|
+
anchor.path,
|
|
1748
|
+
anchor.lineStart,
|
|
1749
|
+
anchor.fingerprint.anchorMetadataHash,
|
|
1750
|
+
anchor.fingerprint.anchorTextHash,
|
|
1751
|
+
anchor.fingerprint.contextHash,
|
|
1752
|
+
anchor.fingerprint.searchTermsHash,
|
|
1753
|
+
anchor.fingerprint.invariantHash,
|
|
1754
|
+
anchor.fingerprint.riskHash,
|
|
1755
|
+
anchor.fingerprint.symbol.kind,
|
|
1756
|
+
anchor.fingerprint.symbol.name,
|
|
1757
|
+
anchor.fingerprint.symbol.exported ? 1 : 0,
|
|
1758
|
+
anchor.fingerprint.symbol.signatureHash,
|
|
1759
|
+
anchor.fingerprint.symbol.bodyHash,
|
|
1760
|
+
anchor.fingerprint.symbol.startLine,
|
|
1761
|
+
anchor.fingerprint.symbol.endLine,
|
|
1762
|
+
]);
|
|
1763
|
+
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1764
|
+
anchor.id,
|
|
1765
|
+
anchor.status,
|
|
1766
|
+
anchor.confidence,
|
|
1767
|
+
anchor.signals.identity,
|
|
1768
|
+
anchor.signals.location,
|
|
1769
|
+
anchor.signals.symbol,
|
|
1770
|
+
anchor.signals.body,
|
|
1771
|
+
anchor.signals.metadata,
|
|
1772
|
+
anchor.signals.semantic,
|
|
1773
|
+
anchor.signals.risk,
|
|
1774
|
+
anchor.navigationOnly ? 1 : 0,
|
|
1775
|
+
anchor.canInstructAgent ? 1 : 0,
|
|
1776
|
+
]);
|
|
1777
|
+
}
|
|
1778
|
+
for (const summary of verificationEvidence.summaries) {
|
|
1779
|
+
database.run('INSERT INTO verification_evidence_summaries (source_path, source_hash, command, kind, status, run_dir, manifest_path, verification_plan_id, completion_status, primary_reason, matched_intents, ran_intents, passed_intents, failed_intents, skipped_intents, receipt_count, coverage_count, remaining_risk_count, failure_fingerprint) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1780
|
+
summary.sourcePath,
|
|
1781
|
+
summary.sourceHash,
|
|
1782
|
+
summary.command,
|
|
1783
|
+
summary.kind,
|
|
1784
|
+
summary.status,
|
|
1785
|
+
summary.runDir,
|
|
1786
|
+
summary.manifestPath,
|
|
1787
|
+
summary.verificationPlanId,
|
|
1788
|
+
summary.completionStatus,
|
|
1789
|
+
summary.primaryReason,
|
|
1790
|
+
summary.matchedIntents,
|
|
1791
|
+
summary.ranIntents,
|
|
1792
|
+
summary.passedIntents,
|
|
1793
|
+
summary.failedIntents,
|
|
1794
|
+
summary.skippedIntents,
|
|
1795
|
+
summary.receiptCount,
|
|
1796
|
+
summary.coverageCount,
|
|
1797
|
+
summary.remainingRiskCount,
|
|
1798
|
+
summary.failureFingerprint,
|
|
1799
|
+
]);
|
|
1800
|
+
}
|
|
1801
|
+
for (const plan of verificationEvidence.verificationPlans) {
|
|
1802
|
+
database.run('INSERT INTO verification_plans (plan_id, source_path, classification_hash, command_contract_hash, selected_intents_hash, created_at, source_hash) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
|
1803
|
+
plan.planId,
|
|
1804
|
+
plan.sourcePath,
|
|
1805
|
+
plan.classificationHash,
|
|
1806
|
+
plan.commandContractHash,
|
|
1807
|
+
plan.selectedIntentsHash,
|
|
1808
|
+
plan.createdAt,
|
|
1809
|
+
plan.sourceHash,
|
|
1810
|
+
]);
|
|
1811
|
+
}
|
|
1812
|
+
for (const criterion of verificationEvidence.acceptanceCriteria) {
|
|
1813
|
+
database.run('INSERT INTO acceptance_criteria (criterion_id, plan_id, source, statement_hash, reason, surface, path_hash) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
|
1814
|
+
criterion.criterionId,
|
|
1815
|
+
criterion.planId,
|
|
1816
|
+
criterion.source,
|
|
1817
|
+
criterion.statementHash,
|
|
1818
|
+
criterion.reason,
|
|
1819
|
+
criterion.surface,
|
|
1820
|
+
criterion.pathHash,
|
|
1821
|
+
]);
|
|
1822
|
+
}
|
|
1823
|
+
for (const coverage of verificationEvidence.criterionCoverage) {
|
|
1824
|
+
database.run('INSERT INTO criterion_coverage (criterion_id, plan_id, status, receipt_count, gap_count, risk_count) VALUES (?, ?, ?, ?, ?, ?)', [coverage.criterionId, coverage.planId, coverage.status, coverage.receiptCount, coverage.gapCount, coverage.riskCount]);
|
|
1825
|
+
}
|
|
1826
|
+
for (const receipt of verificationEvidence.receipts) {
|
|
1827
|
+
database.run('INSERT INTO verification_receipt_summaries (source_path, ordinal, intent, status, skipped, verification_plan_id, receipt_path, receipt_sha256) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1828
|
+
receipt.sourcePath,
|
|
1829
|
+
receipt.ordinal,
|
|
1830
|
+
receipt.intent,
|
|
1831
|
+
receipt.status,
|
|
1832
|
+
receipt.skipped ? 1 : 0,
|
|
1833
|
+
receipt.verificationPlanId,
|
|
1834
|
+
receipt.receiptPath,
|
|
1835
|
+
receipt.receiptSha256,
|
|
1836
|
+
]);
|
|
1837
|
+
}
|
|
1838
|
+
for (const receipt of verificationEvidence.commandReceiptSummaries) {
|
|
1839
|
+
database.run('INSERT INTO command_receipt_summaries (receipt_hash, plan_id, intent, status, command_fingerprint, contract_fingerprint, current_state_hash, write_drift_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1840
|
+
receipt.receiptHash,
|
|
1841
|
+
receipt.planId,
|
|
1842
|
+
receipt.intent,
|
|
1843
|
+
receipt.status,
|
|
1844
|
+
receipt.commandFingerprint,
|
|
1845
|
+
receipt.contractFingerprint,
|
|
1846
|
+
receipt.currentStateHash,
|
|
1847
|
+
receipt.writeDriftStatus,
|
|
1848
|
+
]);
|
|
1849
|
+
}
|
|
1850
|
+
for (const coverage of verificationEvidence.coverageStates) {
|
|
1851
|
+
database.run('INSERT INTO verification_coverage_states (source_path, criterion_id, source, status, requirement_reason, intents, receipt_count, gap_count, source_anchor_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1852
|
+
coverage.sourcePath,
|
|
1853
|
+
coverage.criterionId,
|
|
1854
|
+
coverage.source,
|
|
1855
|
+
coverage.status,
|
|
1856
|
+
coverage.requirementReason,
|
|
1857
|
+
joinedList(coverage.intents),
|
|
1858
|
+
coverage.receiptCount,
|
|
1859
|
+
coverage.gapCount,
|
|
1860
|
+
coverage.sourceAnchorCount,
|
|
1861
|
+
]);
|
|
1862
|
+
}
|
|
1863
|
+
for (const risk of verificationEvidence.riskSignals) {
|
|
1864
|
+
database.run('INSERT INTO verification_risk_signals (source_path, ordinal, code, severity, detail_hash) VALUES (?, ?, ?, ?, ?)', [risk.sourcePath, risk.ordinal, risk.code, risk.severity, risk.detailHash]);
|
|
1865
|
+
}
|
|
1866
|
+
for (const signal of verificationEvidence.validationRatchetSignals) {
|
|
1867
|
+
database.run('INSERT INTO validation_ratchet_signals (signal_id, plan_id, code, severity, path_hash, before_hash, after_hash) VALUES (?, ?, ?, ?, ?, ?, ?)', [signal.signalId, signal.planId, signal.code, signal.severity, signal.pathHash, signal.beforeHash, signal.afterHash]);
|
|
1868
|
+
}
|
|
1869
|
+
for (const verdict of verificationEvidence.completionVerdictSummaries) {
|
|
1870
|
+
database.run('INSERT INTO completion_verdict_summaries (claim_id, plan_id, status, primary_reason, risk_count, contradiction_count, blocker_count) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
|
1871
|
+
verdict.claimId,
|
|
1872
|
+
verdict.planId,
|
|
1873
|
+
verdict.status,
|
|
1874
|
+
verdict.primaryReason,
|
|
1875
|
+
verdict.riskCount,
|
|
1876
|
+
verdict.contradictionCount,
|
|
1877
|
+
verdict.blockerCount,
|
|
1878
|
+
]);
|
|
1879
|
+
}
|
|
1880
|
+
for (const route of verificationEvidence.reproRoutes) {
|
|
1881
|
+
database.run('INSERT INTO repro_routes (route_id, task_hash, route_digest, route_kind, failure_oracle_hash) VALUES (?, ?, ?, ?, ?)', [route.routeId, route.taskHash, route.routeDigest, route.routeKind, route.failureOracleHash]);
|
|
1882
|
+
}
|
|
1883
|
+
for (const observation of verificationEvidence.reproObservations) {
|
|
1884
|
+
database.run('INSERT INTO repro_observations (route_id, phase, outcome, receipt_hash, diagnostic_fingerprint) VALUES (?, ?, ?, ?, ?)', [
|
|
1885
|
+
observation.routeId,
|
|
1886
|
+
observation.phase,
|
|
1887
|
+
observation.outcome,
|
|
1888
|
+
observation.receiptHash,
|
|
1889
|
+
observation.diagnosticFingerprint,
|
|
1890
|
+
]);
|
|
1891
|
+
}
|
|
1892
|
+
for (const fingerprint of verificationEvidence.failureFingerprintReadModels) {
|
|
1893
|
+
database.run('INSERT INTO failure_fingerprints (fingerprint, plan_id, failed_intents_hash, risk_codes_hash, seen_count, first_seen_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
|
1894
|
+
fingerprint.fingerprint,
|
|
1895
|
+
fingerprint.planId,
|
|
1896
|
+
fingerprint.failedIntentsHash,
|
|
1897
|
+
fingerprint.riskCodesHash,
|
|
1898
|
+
fingerprint.seenCount,
|
|
1899
|
+
fingerprint.firstSeenAt,
|
|
1900
|
+
fingerprint.lastSeenAt,
|
|
1901
|
+
]);
|
|
1902
|
+
}
|
|
1903
|
+
for (const fingerprint of verificationEvidence.failureFingerprints) {
|
|
1904
|
+
database.run('INSERT INTO verification_failure_fingerprints (source_path, fingerprint, verification_plan_id, status, failed_intents, primary_reason, failed_intents_hash, risk_codes_hash, affected_surfaces_hash, first_seen_at, last_seen_at, seen_count, requires_new_evidence) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
1905
|
+
fingerprint.sourcePath,
|
|
1906
|
+
fingerprint.fingerprint,
|
|
1907
|
+
fingerprint.verificationPlanId,
|
|
1908
|
+
fingerprint.status,
|
|
1909
|
+
joinedList(fingerprint.failedIntents),
|
|
1910
|
+
fingerprint.primaryReason,
|
|
1911
|
+
fingerprint.failedIntentsHash,
|
|
1912
|
+
fingerprint.riskCodesHash,
|
|
1913
|
+
fingerprint.affectedSurfacesHash,
|
|
1914
|
+
fingerprint.firstSeenAt,
|
|
1915
|
+
fingerprint.lastSeenAt,
|
|
1916
|
+
fingerprint.seenCount,
|
|
1917
|
+
fingerprint.requiresNewEvidence ? 1 : 0,
|
|
1918
|
+
]);
|
|
1919
|
+
}
|
|
1920
|
+
for (const signal of sourceAnchorRiskSignals) {
|
|
1921
|
+
database.run('INSERT INTO source_anchor_risk_signals (anchor_id, path_hash, status, risk_signal, confidence, navigation_only, can_instruct_agent) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
|
1922
|
+
signal.anchorId,
|
|
1923
|
+
signal.pathHash,
|
|
1924
|
+
signal.status,
|
|
1925
|
+
signal.riskSignal,
|
|
1926
|
+
signal.confidence,
|
|
1927
|
+
signal.navigationOnly ? 1 : 0,
|
|
1928
|
+
signal.canInstructAgent ? 1 : 0,
|
|
1929
|
+
]);
|
|
1930
|
+
}
|
|
1931
|
+
populatePathSurfaceReadModel(database);
|
|
1932
|
+
populateSearchTables(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors);
|
|
1933
|
+
}
|
|
1934
|
+
function indexedFilesMatch(database, currentFiles) {
|
|
1935
|
+
const rows = queryRows(database, 'SELECT path, source_scope, content_hash, parser_version FROM indexed_files ORDER BY path');
|
|
1936
|
+
if (rows.length !== currentFiles.length) {
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
|
|
1940
|
+
for (const row of rows) {
|
|
1941
|
+
const storedPath = toSearchString(row.path);
|
|
1942
|
+
const current = currentByPath.get(storedPath);
|
|
1943
|
+
if (!current) {
|
|
1944
|
+
return false;
|
|
1945
|
+
}
|
|
1946
|
+
if (toSearchString(row.source_scope) !== current.sourceScope ||
|
|
1947
|
+
toSearchString(row.content_hash) !== current.contentHash ||
|
|
1948
|
+
toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1954
|
+
async function readIncrementalReuseDecision(SQL, databasePath, currentFiles, sourceScopeHash) {
|
|
1955
|
+
if (!existsSync(databasePath)) {
|
|
1956
|
+
return { reusable: false, rebuildReason: 'missing_index', capabilities: null };
|
|
1957
|
+
}
|
|
1958
|
+
let database;
|
|
1959
|
+
try {
|
|
1960
|
+
database = new SQL.Database(readFileSync(databasePath));
|
|
1961
|
+
if (readStoredSchemaVersion(database) !== LOCAL_INDEX_SCHEMA_VERSION) {
|
|
1962
|
+
return { reusable: false, rebuildReason: 'schema_version_mismatch', capabilities: null };
|
|
1963
|
+
}
|
|
1964
|
+
if (readMetadataValue(database, 'parser_version') !== LOCAL_INDEX_PARSER_VERSION) {
|
|
1965
|
+
return { reusable: false, rebuildReason: 'parser_version_mismatch', capabilities: null };
|
|
1966
|
+
}
|
|
1967
|
+
if (readMetadataValue(database, 'source_scope_hash') !== sourceScopeHash) {
|
|
1968
|
+
return { reusable: false, rebuildReason: 'source_scope_mismatch', capabilities: null };
|
|
1969
|
+
}
|
|
1970
|
+
if (!hasTable(database, 'indexed_files')) {
|
|
1971
|
+
return { reusable: false, rebuildReason: 'indexed_files_missing', capabilities: null };
|
|
1972
|
+
}
|
|
1973
|
+
if (!indexedFilesMatch(database, currentFiles)) {
|
|
1974
|
+
return { reusable: false, rebuildReason: 'file_fingerprint_mismatch', capabilities: null };
|
|
1975
|
+
}
|
|
1976
|
+
return {
|
|
1977
|
+
reusable: true,
|
|
1978
|
+
rebuildReason: null,
|
|
1979
|
+
capabilities: readStoredSearchCapabilities(database),
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
catch {
|
|
1983
|
+
return { reusable: false, rebuildReason: 'unreadable_index', capabilities: null };
|
|
1984
|
+
}
|
|
1985
|
+
finally {
|
|
1986
|
+
database?.close();
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* mf:anchor cli.index.create
|
|
1991
|
+
* purpose: Build the local SQLite index for workflow documents and optional source anchors.
|
|
1992
|
+
* search: mf index, local index, sqlite, source anchors, workflow documents
|
|
1993
|
+
* invariant: Source anchors are indexed only when requested by CLI flag or local index configuration.
|
|
1994
|
+
* risk: cache, config
|
|
1995
|
+
*/
|
|
1996
|
+
export async function createLocalIndex(projectRoot, options = {}) {
|
|
1997
|
+
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
1998
|
+
const dryRun = options.dryRun === true;
|
|
1999
|
+
const incremental = options.incremental === true;
|
|
2000
|
+
const indexMode = incremental ? 'incremental' : 'full';
|
|
2001
|
+
const documents = collectDocuments(projectRoot);
|
|
2002
|
+
const skills = collectSkills(documents);
|
|
2003
|
+
const skillRoutes = collectSkillRoutes(projectRoot);
|
|
2004
|
+
const commandIntents = collectCommandIntents(projectRoot);
|
|
2005
|
+
const sourceConfig = readLocalIndexSourceConfig(projectRoot);
|
|
2006
|
+
const includeSource = options.includeSource === true || sourceConfig.enabledByDefault;
|
|
2007
|
+
const sourceScopeHash = getSourceScopeHash(includeSource, sourceConfig);
|
|
2008
|
+
const previousSourceAnchors = includeSource
|
|
2009
|
+
? await readPreviousSourceAnchorSnapshots(databasePath).catch(() => [])
|
|
2010
|
+
: [];
|
|
2011
|
+
const sourceAnchors = includeSource
|
|
2012
|
+
? collectSourceAnchorIndexRecords(projectRoot, previousSourceAnchors, {
|
|
2013
|
+
...sourceConfig,
|
|
2014
|
+
excludeGeneratedOrVendor: true,
|
|
2015
|
+
})
|
|
2016
|
+
: [];
|
|
2017
|
+
const verificationEvidence = createVerificationEvidenceIndex(projectRoot);
|
|
2018
|
+
const indexedFiles = collectIndexedFileRecords(projectRoot, documents, sourceAnchors);
|
|
2019
|
+
let capabilities = searchCapabilities(false);
|
|
2020
|
+
let reusedExisting = false;
|
|
2021
|
+
let rebuildReason = null;
|
|
2022
|
+
const SQL = await loadSqlJs();
|
|
2023
|
+
const capabilityDatabase = new SQL.Database();
|
|
2024
|
+
capabilities = detectLocalSearchCapabilities(capabilityDatabase);
|
|
2025
|
+
capabilityDatabase.close();
|
|
2026
|
+
if (incremental) {
|
|
2027
|
+
const reuseDecision = await readIncrementalReuseDecision(SQL, databasePath, indexedFiles, sourceScopeHash);
|
|
2028
|
+
reusedExisting = reuseDecision.reusable;
|
|
2029
|
+
rebuildReason = reuseDecision.rebuildReason;
|
|
2030
|
+
capabilities = reuseDecision.capabilities ?? capabilities;
|
|
2031
|
+
}
|
|
2032
|
+
if (!dryRun && !reusedExisting) {
|
|
2033
|
+
const database = new SQL.Database();
|
|
2034
|
+
createSchema(database, capabilities);
|
|
2035
|
+
populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, verificationEvidence, indexMode, sourceScopeHash, includeSource, new Date().toISOString());
|
|
2036
|
+
mkdirSync(path.dirname(databasePath), { recursive: true });
|
|
2037
|
+
writeFileSync(databasePath, database.export());
|
|
2038
|
+
database.close();
|
|
2039
|
+
}
|
|
2040
|
+
return {
|
|
2041
|
+
schema_version: LOCAL_INDEX_SCHEMA_VERSION,
|
|
2042
|
+
command: 'index',
|
|
2043
|
+
ok: true,
|
|
2044
|
+
mustflow_root: path.resolve(projectRoot),
|
|
2045
|
+
database_path: databasePath,
|
|
2046
|
+
dry_run: dryRun,
|
|
2047
|
+
wrote_files: !dryRun && !reusedExisting,
|
|
2048
|
+
index_mode: indexMode,
|
|
2049
|
+
reused_existing: reusedExisting,
|
|
2050
|
+
rebuild_reason: rebuildReason,
|
|
2051
|
+
document_count: documents.length,
|
|
2052
|
+
skill_count: skills.length,
|
|
2053
|
+
skill_route_count: skillRoutes.length,
|
|
2054
|
+
command_intent_count: commandIntents.length,
|
|
2055
|
+
command_effect_count: commandIntents.reduce((count, intent) => count + intent.effects.length, 0),
|
|
2056
|
+
verification_evidence_summary_count: verificationEvidence.summaries.length,
|
|
2057
|
+
verification_plan_count: verificationEvidence.verificationPlans.length,
|
|
2058
|
+
acceptance_criteria_count: verificationEvidence.acceptanceCriteria.length,
|
|
2059
|
+
criterion_coverage_count: verificationEvidence.criterionCoverage.length,
|
|
2060
|
+
verification_receipt_summary_count: verificationEvidence.receipts.length,
|
|
2061
|
+
command_receipt_summary_count: verificationEvidence.commandReceiptSummaries.length,
|
|
2062
|
+
verification_coverage_state_count: verificationEvidence.coverageStates.length,
|
|
2063
|
+
verification_risk_signal_count: verificationEvidence.riskSignals.length,
|
|
2064
|
+
validation_ratchet_signal_count: verificationEvidence.validationRatchetSignals.length,
|
|
2065
|
+
completion_verdict_summary_count: verificationEvidence.completionVerdictSummaries.length,
|
|
2066
|
+
repro_route_count: verificationEvidence.reproRoutes.length,
|
|
2067
|
+
repro_observation_count: verificationEvidence.reproObservations.length,
|
|
2068
|
+
failure_fingerprint_count: verificationEvidence.failureFingerprints.length,
|
|
2069
|
+
source_index_enabled: includeSource,
|
|
2070
|
+
source_anchor_count: sourceAnchors.length,
|
|
2071
|
+
source_anchor_risk_signal_count: createSourceAnchorRiskSignals(sourceAnchors).length,
|
|
2072
|
+
search_backend: capabilities.backend,
|
|
2073
|
+
search_fts5_available: capabilities.fts5Available,
|
|
2074
|
+
content_mode: LOCAL_INDEX_CONTENT_MODE,
|
|
2075
|
+
store_full_content: LOCAL_INDEX_STORE_FULL_CONTENT,
|
|
2076
|
+
max_snippet_bytes_per_document: MAX_SNIPPET_BYTES_PER_DOCUMENT,
|
|
2077
|
+
excluded_raw_data_kinds: [...LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS],
|
|
2078
|
+
indexed_file_count: indexedFiles.length,
|
|
2079
|
+
indexed_paths: documents.map((document) => document.path),
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
function readStoredSchemaVersion(database) {
|
|
2083
|
+
return readMetadataValue(database, 'schema_version');
|
|
2084
|
+
}
|
|
2085
|
+
function getStalePaths(projectRoot, database) {
|
|
2086
|
+
const schemaVersion = readStoredSchemaVersion(database);
|
|
2087
|
+
if (schemaVersion !== LOCAL_INDEX_SCHEMA_VERSION) {
|
|
2088
|
+
return ['.mustflow/cache/mustflow.sqlite'];
|
|
2089
|
+
}
|
|
2090
|
+
if (hasTable(database, 'indexed_files')) {
|
|
2091
|
+
const stalePaths = new Set();
|
|
2092
|
+
const indexedRows = queryRows(database, 'SELECT path, source_scope, content_hash FROM indexed_files');
|
|
2093
|
+
const indexedPaths = new Set(indexedRows.map((row) => toSearchString(row.path)));
|
|
2094
|
+
for (const row of indexedRows) {
|
|
2095
|
+
const indexedPath = toSearchString(row.path);
|
|
2096
|
+
const sourceScope = toSearchString(row.source_scope) === 'source_anchor' ? 'source_anchor' : 'workflow';
|
|
2097
|
+
try {
|
|
2098
|
+
const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
|
|
2099
|
+
if (current.contentHash !== toSearchString(row.content_hash)) {
|
|
2100
|
+
stalePaths.add(indexedPath);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
catch {
|
|
2104
|
+
stalePaths.add(indexedPath);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
for (const document of collectDocuments(projectRoot)) {
|
|
2108
|
+
if (!indexedPaths.has(document.path)) {
|
|
2109
|
+
stalePaths.add(document.path);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return Array.from(stalePaths).sort((left, right) => left.localeCompare(right));
|
|
2113
|
+
}
|
|
2114
|
+
const indexedRows = queryRows(database, 'SELECT path, content_hash FROM documents');
|
|
2115
|
+
const indexedHashes = new Map(indexedRows.map((row) => [toSearchString(row.path), toSearchString(row.content_hash)]));
|
|
2116
|
+
const currentDocuments = collectDocuments(projectRoot);
|
|
2117
|
+
const currentHashes = new Map(currentDocuments.map((document) => [document.path, document.contentHash]));
|
|
2118
|
+
const stalePaths = new Set();
|
|
2119
|
+
for (const [indexedPath, indexedHash] of indexedHashes) {
|
|
2120
|
+
if (currentHashes.get(indexedPath) !== indexedHash) {
|
|
2121
|
+
stalePaths.add(indexedPath);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
for (const currentPath of currentHashes.keys()) {
|
|
2125
|
+
if (!indexedHashes.has(currentPath)) {
|
|
2126
|
+
stalePaths.add(currentPath);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
return Array.from(stalePaths).sort((left, right) => left.localeCompare(right));
|
|
2130
|
+
}
|
|
2131
|
+
function mapCommandLockConflict(row, intent) {
|
|
2132
|
+
const targetIsLeft = toSearchString(row.left_intent) === intent;
|
|
2133
|
+
const targetPrefix = targetIsLeft ? 'left' : 'right';
|
|
2134
|
+
const otherPrefix = targetIsLeft ? 'right' : 'left';
|
|
2135
|
+
return {
|
|
2136
|
+
intent: toSearchString(row[`${otherPrefix}_intent`]),
|
|
2137
|
+
lock: toSearchString(row.lock),
|
|
2138
|
+
paths: splitIndexedList(row[`${targetPrefix}_paths`]),
|
|
2139
|
+
modes: splitIndexedList(row[`${targetPrefix}_modes`]),
|
|
2140
|
+
concurrencies: splitIndexedList(row[`${targetPrefix}_concurrencies`]),
|
|
2141
|
+
conflictingPaths: splitIndexedList(row[`${otherPrefix}_paths`]),
|
|
2142
|
+
conflictingModes: splitIndexedList(row[`${otherPrefix}_modes`]),
|
|
2143
|
+
conflictingConcurrencies: splitIndexedList(row[`${otherPrefix}_concurrencies`]),
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* mf:anchor cli.index.command-effect-graph
|
|
2148
|
+
* purpose: Read command-effect lock and conflict explanations from the local SQLite index.
|
|
2149
|
+
* search: mf explain command, command locks, local index, sqlite graph
|
|
2150
|
+
* invariant: Indexed command-effect rows explain current commands.toml only when the index is fresh and never grant command authority.
|
|
2151
|
+
* risk: cache, config
|
|
2152
|
+
*/
|
|
2153
|
+
function queryLocalCommandEffectGraph(databasePath, database, intent) {
|
|
2154
|
+
const writeLocks = queryRows(database, `
|
|
2155
|
+
SELECT lock, paths, modes, sources, concurrencies, effect_count
|
|
2156
|
+
FROM command_write_locks
|
|
2157
|
+
WHERE intent = ?
|
|
2158
|
+
ORDER BY lock
|
|
2159
|
+
`, [intent]).map((row) => ({
|
|
2160
|
+
lock: toSearchString(row.lock),
|
|
2161
|
+
paths: splitIndexedList(row.paths),
|
|
2162
|
+
modes: splitIndexedList(row.modes),
|
|
2163
|
+
sources: splitIndexedList(row.sources),
|
|
2164
|
+
concurrencies: splitIndexedList(row.concurrencies),
|
|
2165
|
+
effectCount: typeof row.effect_count === 'number' && Number.isFinite(row.effect_count) ? row.effect_count : 0,
|
|
2166
|
+
}));
|
|
2167
|
+
const lockConflicts = queryRows(database, `
|
|
2168
|
+
SELECT
|
|
2169
|
+
left_intent,
|
|
2170
|
+
right_intent,
|
|
2171
|
+
lock,
|
|
2172
|
+
left_paths,
|
|
2173
|
+
right_paths,
|
|
2174
|
+
left_modes,
|
|
2175
|
+
right_modes,
|
|
2176
|
+
left_concurrencies,
|
|
2177
|
+
right_concurrencies
|
|
2178
|
+
FROM command_lock_conflicts
|
|
2179
|
+
WHERE left_intent = ? OR right_intent = ?
|
|
2180
|
+
ORDER BY lock, left_intent, right_intent
|
|
2181
|
+
`, [intent, intent]).map((row) => mapCommandLockConflict(row, intent));
|
|
2182
|
+
return {
|
|
2183
|
+
source: 'local_index',
|
|
2184
|
+
authority: 'explanation_only',
|
|
2185
|
+
commandAuthority: '.mustflow/config/commands.toml',
|
|
2186
|
+
grantsCommandAuthority: false,
|
|
2187
|
+
status: 'fresh',
|
|
2188
|
+
databasePath,
|
|
2189
|
+
indexFresh: true,
|
|
2190
|
+
stalePaths: [],
|
|
2191
|
+
writeLocks,
|
|
2192
|
+
lockConflicts,
|
|
2193
|
+
refreshHint: null,
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
export async function readLocalCommandEffectGraph(projectRoot, intent) {
|
|
2197
|
+
const graphs = await readLocalCommandEffectGraphs(projectRoot, [intent]);
|
|
2198
|
+
return graphs.get(intent) ?? createCommandEffectGraphStatus(getLocalIndexDatabasePath(projectRoot), 'unreadable');
|
|
2199
|
+
}
|
|
2200
|
+
export async function readLocalCommandEffectGraphs(projectRoot, intents) {
|
|
2201
|
+
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
2202
|
+
const intentNames = [...new Set(intents)];
|
|
2203
|
+
const statusMap = (status, stalePaths = []) => new Map(intentNames.map((intent) => [intent, createCommandEffectGraphStatus(databasePath, status, stalePaths)]));
|
|
2204
|
+
if (!existsSync(databasePath)) {
|
|
2205
|
+
return statusMap('missing');
|
|
2206
|
+
}
|
|
2207
|
+
const SQL = await loadSqlJs();
|
|
2208
|
+
const database = new SQL.Database(readFileSync(databasePath));
|
|
2209
|
+
try {
|
|
2210
|
+
const stalePaths = getStalePaths(projectRoot, database);
|
|
2211
|
+
if (stalePaths.length > 0) {
|
|
2212
|
+
return statusMap('stale', stalePaths);
|
|
2213
|
+
}
|
|
2214
|
+
return new Map(intentNames.map((intent) => [intent, queryLocalCommandEffectGraph(databasePath, database, intent)]));
|
|
2215
|
+
}
|
|
2216
|
+
catch {
|
|
2217
|
+
return statusMap('unreadable');
|
|
2218
|
+
}
|
|
2219
|
+
finally {
|
|
2220
|
+
database.close();
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
function createPathSurfaceReadModelStatus(databasePath, status, inputPath, stalePaths = []) {
|
|
2224
|
+
return {
|
|
2225
|
+
source: 'local_index',
|
|
2226
|
+
status,
|
|
2227
|
+
databasePath,
|
|
2228
|
+
indexFresh: status === 'fresh',
|
|
2229
|
+
stalePaths,
|
|
2230
|
+
inputPath,
|
|
2231
|
+
match: null,
|
|
2232
|
+
refreshHint: status === 'fresh' ? null : 'Run `mf index` to refresh path-surface explanations.',
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
function createLocalIndexPromptContextStatus(databasePath, status, stalePaths = [], capabilities = null) {
|
|
2236
|
+
return {
|
|
2237
|
+
source: 'local_index',
|
|
2238
|
+
status,
|
|
2239
|
+
databasePath,
|
|
2240
|
+
indexFresh: status === 'fresh',
|
|
2241
|
+
stalePaths,
|
|
2242
|
+
searchBackend: capabilities?.backend ?? null,
|
|
2243
|
+
searchFts5Available: capabilities?.fts5Available ?? null,
|
|
2244
|
+
refreshHint: status === 'fresh' ? null : 'Run `mf index` to refresh prompt-cache task context local-index metadata.',
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
export async function readLocalIndexPromptContext(projectRoot) {
|
|
2248
|
+
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
2249
|
+
if (!existsSync(databasePath)) {
|
|
2250
|
+
return createLocalIndexPromptContextStatus(databasePath, 'missing');
|
|
2251
|
+
}
|
|
2252
|
+
let database;
|
|
2253
|
+
try {
|
|
2254
|
+
const SQL = await loadSqlJs();
|
|
2255
|
+
database = new SQL.Database(readFileSync(databasePath));
|
|
2256
|
+
const capabilities = readStoredSearchCapabilities(database);
|
|
2257
|
+
const stalePaths = getStalePaths(projectRoot, database);
|
|
2258
|
+
if (stalePaths.length > 0) {
|
|
2259
|
+
return createLocalIndexPromptContextStatus(databasePath, 'stale', stalePaths, capabilities);
|
|
2260
|
+
}
|
|
2261
|
+
return createLocalIndexPromptContextStatus(databasePath, 'fresh', [], capabilities);
|
|
2262
|
+
}
|
|
2263
|
+
catch {
|
|
2264
|
+
return createLocalIndexPromptContextStatus(databasePath, 'unreadable');
|
|
2265
|
+
}
|
|
2266
|
+
finally {
|
|
2267
|
+
database?.close();
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
function createVerificationReadModelQueryStatus(databasePath, status, planId, stalePaths = []) {
|
|
2271
|
+
return {
|
|
2272
|
+
source: 'local_index',
|
|
2273
|
+
authority: 'evidence_only',
|
|
2274
|
+
commandAuthority: '.mustflow/config/commands.toml',
|
|
2275
|
+
grantsCommandAuthority: false,
|
|
2276
|
+
status,
|
|
2277
|
+
databasePath,
|
|
2278
|
+
indexFresh: status === 'fresh',
|
|
2279
|
+
stalePaths,
|
|
2280
|
+
planId,
|
|
2281
|
+
uncoveredCriteria: [],
|
|
2282
|
+
severeRisks: [],
|
|
2283
|
+
nonPassingReceipts: [],
|
|
2284
|
+
repeatedFailureFingerprints: [],
|
|
2285
|
+
validationWeakeningSignals: [],
|
|
2286
|
+
refreshHint: status === 'fresh' ? null : 'Run `mf index` to refresh verification read-model evidence.',
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
function readLatestVerificationPlanId(database) {
|
|
2290
|
+
const row = queryRows(database, `
|
|
2291
|
+
SELECT plan_id
|
|
2292
|
+
FROM verification_plans
|
|
2293
|
+
ORDER BY COALESCE(created_at, '') DESC, source_path DESC, plan_id DESC
|
|
2294
|
+
LIMIT 1
|
|
2295
|
+
`)[0];
|
|
2296
|
+
return toSearchString(row?.plan_id) || null;
|
|
2297
|
+
}
|
|
2298
|
+
function readUncoveredCriteria(database, planId) {
|
|
2299
|
+
return queryRows(database, `
|
|
2300
|
+
SELECT
|
|
2301
|
+
acceptance_criteria.criterion_id,
|
|
2302
|
+
acceptance_criteria.source,
|
|
2303
|
+
acceptance_criteria.reason,
|
|
2304
|
+
acceptance_criteria.surface,
|
|
2305
|
+
acceptance_criteria.path_hash,
|
|
2306
|
+
criterion_coverage.status AS coverage_status,
|
|
2307
|
+
criterion_coverage.receipt_count,
|
|
2308
|
+
criterion_coverage.gap_count,
|
|
2309
|
+
criterion_coverage.risk_count
|
|
2310
|
+
FROM acceptance_criteria
|
|
2311
|
+
LEFT JOIN criterion_coverage
|
|
2312
|
+
ON criterion_coverage.plan_id = acceptance_criteria.plan_id
|
|
2313
|
+
AND criterion_coverage.criterion_id = acceptance_criteria.criterion_id
|
|
2314
|
+
WHERE acceptance_criteria.plan_id = ?
|
|
2315
|
+
AND (criterion_coverage.status IS NULL OR criterion_coverage.status != 'covered')
|
|
2316
|
+
ORDER BY acceptance_criteria.criterion_id
|
|
2317
|
+
`, [planId]).map((row) => ({
|
|
2318
|
+
criterionId: toSearchString(row.criterion_id),
|
|
2319
|
+
source: toSearchString(row.source),
|
|
2320
|
+
reason: toSearchString(row.reason) || null,
|
|
2321
|
+
surface: toSearchString(row.surface) || null,
|
|
2322
|
+
pathHash: toSearchString(row.path_hash) || null,
|
|
2323
|
+
coverageStatus: toSearchString(row.coverage_status) || null,
|
|
2324
|
+
receiptCount: toNullableNumber(row.receipt_count) ?? 0,
|
|
2325
|
+
gapCount: toNullableNumber(row.gap_count) ?? 0,
|
|
2326
|
+
riskCount: toNullableNumber(row.risk_count) ?? 0,
|
|
2327
|
+
}));
|
|
2328
|
+
}
|
|
2329
|
+
function readSevereVerificationRisks(database, planId) {
|
|
2330
|
+
return queryRows(database, `
|
|
2331
|
+
SELECT
|
|
2332
|
+
verification_risk_signals.source_path,
|
|
2333
|
+
verification_risk_signals.ordinal,
|
|
2334
|
+
verification_risk_signals.code,
|
|
2335
|
+
verification_risk_signals.severity,
|
|
2336
|
+
verification_risk_signals.detail_hash
|
|
2337
|
+
FROM verification_risk_signals
|
|
2338
|
+
JOIN verification_evidence_summaries
|
|
2339
|
+
ON verification_evidence_summaries.source_path = verification_risk_signals.source_path
|
|
2340
|
+
WHERE verification_evidence_summaries.verification_plan_id = ?
|
|
2341
|
+
AND verification_risk_signals.severity IN ('high', 'critical')
|
|
2342
|
+
ORDER BY verification_risk_signals.source_path, verification_risk_signals.ordinal
|
|
2343
|
+
`, [planId]).map((row) => ({
|
|
2344
|
+
sourcePath: toSearchString(row.source_path),
|
|
2345
|
+
ordinal: toNullableNumber(row.ordinal) ?? 0,
|
|
2346
|
+
code: toSearchString(row.code),
|
|
2347
|
+
severity: toSearchString(row.severity),
|
|
2348
|
+
detailHash: toSearchString(row.detail_hash),
|
|
2349
|
+
}));
|
|
2350
|
+
}
|
|
2351
|
+
function readNonPassingReceipts(database, planId) {
|
|
2352
|
+
return queryRows(database, `
|
|
2353
|
+
SELECT
|
|
2354
|
+
receipt_hash,
|
|
2355
|
+
plan_id,
|
|
2356
|
+
intent,
|
|
2357
|
+
status,
|
|
2358
|
+
command_fingerprint,
|
|
2359
|
+
contract_fingerprint,
|
|
2360
|
+
current_state_hash,
|
|
2361
|
+
write_drift_status
|
|
2362
|
+
FROM command_receipt_summaries
|
|
2363
|
+
WHERE plan_id = ?
|
|
2364
|
+
AND (
|
|
2365
|
+
status != 'passed'
|
|
2366
|
+
OR write_drift_status IS NULL
|
|
2367
|
+
OR write_drift_status NOT IN ('clean', 'none')
|
|
2368
|
+
)
|
|
2369
|
+
ORDER BY intent, receipt_hash
|
|
2370
|
+
`, [planId]).map((row) => ({
|
|
2371
|
+
receiptHash: toSearchString(row.receipt_hash),
|
|
2372
|
+
planId: toSearchString(row.plan_id),
|
|
2373
|
+
intent: toSearchString(row.intent) || null,
|
|
2374
|
+
status: toSearchString(row.status),
|
|
2375
|
+
commandFingerprint: toSearchString(row.command_fingerprint) || null,
|
|
2376
|
+
contractFingerprint: toSearchString(row.contract_fingerprint) || null,
|
|
2377
|
+
currentStateHash: toSearchString(row.current_state_hash) || null,
|
|
2378
|
+
writeDriftStatus: toSearchString(row.write_drift_status) || null,
|
|
2379
|
+
}));
|
|
2380
|
+
}
|
|
2381
|
+
function readRepeatedFailureFingerprints(database, planId) {
|
|
2382
|
+
return queryRows(database, `
|
|
2383
|
+
SELECT
|
|
2384
|
+
source_path,
|
|
2385
|
+
fingerprint,
|
|
2386
|
+
verification_plan_id,
|
|
2387
|
+
status,
|
|
2388
|
+
failed_intents,
|
|
2389
|
+
primary_reason,
|
|
2390
|
+
failed_intents_hash,
|
|
2391
|
+
risk_codes_hash,
|
|
2392
|
+
affected_surfaces_hash,
|
|
2393
|
+
seen_count,
|
|
2394
|
+
requires_new_evidence
|
|
2395
|
+
FROM verification_failure_fingerprints
|
|
2396
|
+
WHERE verification_plan_id = ?
|
|
2397
|
+
AND requires_new_evidence = 1
|
|
2398
|
+
ORDER BY fingerprint
|
|
2399
|
+
`, [planId]).map((row) => ({
|
|
2400
|
+
sourcePath: toSearchString(row.source_path),
|
|
2401
|
+
fingerprint: toSearchString(row.fingerprint),
|
|
2402
|
+
verificationPlanId: toSearchString(row.verification_plan_id) || null,
|
|
2403
|
+
status: toSearchString(row.status),
|
|
2404
|
+
failedIntents: splitIndexedList(row.failed_intents),
|
|
2405
|
+
primaryReason: toSearchString(row.primary_reason) || null,
|
|
2406
|
+
failedIntentsHash: toSearchString(row.failed_intents_hash) || null,
|
|
2407
|
+
riskCodesHash: toSearchString(row.risk_codes_hash) || null,
|
|
2408
|
+
affectedSurfacesHash: toSearchString(row.affected_surfaces_hash) || null,
|
|
2409
|
+
seenCount: toNullableNumber(row.seen_count) ?? 0,
|
|
2410
|
+
requiresNewEvidence: Number(row.requires_new_evidence) === 1,
|
|
2411
|
+
}));
|
|
2412
|
+
}
|
|
2413
|
+
function readValidationWeakeningSignals(database, planId) {
|
|
2414
|
+
return queryRows(database, `
|
|
2415
|
+
SELECT signal_id, plan_id, code, severity, path_hash, before_hash, after_hash
|
|
2416
|
+
FROM validation_ratchet_signals
|
|
2417
|
+
WHERE plan_id = ?
|
|
2418
|
+
ORDER BY severity DESC, code, signal_id
|
|
2419
|
+
`, [planId]).map((row) => ({
|
|
2420
|
+
signalId: toSearchString(row.signal_id),
|
|
2421
|
+
planId: toSearchString(row.plan_id) || null,
|
|
2422
|
+
code: toSearchString(row.code),
|
|
2423
|
+
severity: toSearchString(row.severity),
|
|
2424
|
+
pathHash: toSearchString(row.path_hash),
|
|
2425
|
+
beforeHash: toSearchString(row.before_hash) || null,
|
|
2426
|
+
afterHash: toSearchString(row.after_hash) || null,
|
|
2427
|
+
}));
|
|
2428
|
+
}
|
|
2429
|
+
function queryLocalVerificationReadModel(databasePath, database, planId) {
|
|
2430
|
+
const selectedPlanId = planId ?? readLatestVerificationPlanId(database);
|
|
2431
|
+
const base = createVerificationReadModelQueryStatus(databasePath, 'fresh', selectedPlanId);
|
|
2432
|
+
if (!selectedPlanId) {
|
|
2433
|
+
return base;
|
|
2434
|
+
}
|
|
2435
|
+
return {
|
|
2436
|
+
...base,
|
|
2437
|
+
uncoveredCriteria: readUncoveredCriteria(database, selectedPlanId),
|
|
2438
|
+
severeRisks: readSevereVerificationRisks(database, selectedPlanId),
|
|
2439
|
+
nonPassingReceipts: readNonPassingReceipts(database, selectedPlanId),
|
|
2440
|
+
repeatedFailureFingerprints: readRepeatedFailureFingerprints(database, selectedPlanId),
|
|
2441
|
+
validationWeakeningSignals: readValidationWeakeningSignals(database, selectedPlanId),
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
export async function readLocalVerificationReadModelQueries(projectRoot, planId) {
|
|
2445
|
+
return readLocalVerificationReadModelQueriesForPlan(projectRoot, planId);
|
|
2446
|
+
}
|
|
2447
|
+
export async function readLocalVerificationReadModelQueriesForPlan(projectRoot, planId) {
|
|
2448
|
+
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
2449
|
+
if (!existsSync(databasePath)) {
|
|
2450
|
+
return createVerificationReadModelQueryStatus(databasePath, 'missing', planId);
|
|
2451
|
+
}
|
|
2452
|
+
const SQL = await loadSqlJs();
|
|
2453
|
+
const database = new SQL.Database(readFileSync(databasePath));
|
|
2454
|
+
try {
|
|
2455
|
+
const requiredTables = [
|
|
2456
|
+
'acceptance_criteria',
|
|
2457
|
+
'criterion_coverage',
|
|
2458
|
+
'verification_evidence_summaries',
|
|
2459
|
+
'verification_plans',
|
|
2460
|
+
'verification_risk_signals',
|
|
2461
|
+
'command_receipt_summaries',
|
|
2462
|
+
'verification_failure_fingerprints',
|
|
2463
|
+
'validation_ratchet_signals',
|
|
2464
|
+
];
|
|
2465
|
+
if (requiredTables.some((tableName) => !hasTable(database, tableName))) {
|
|
2466
|
+
return createVerificationReadModelQueryStatus(databasePath, 'unreadable', planId);
|
|
2467
|
+
}
|
|
2468
|
+
const stalePaths = getStalePaths(projectRoot, database);
|
|
2469
|
+
if (stalePaths.length > 0) {
|
|
2470
|
+
return createVerificationReadModelQueryStatus(databasePath, 'stale', planId, stalePaths);
|
|
2471
|
+
}
|
|
2472
|
+
return queryLocalVerificationReadModel(databasePath, database, planId);
|
|
2473
|
+
}
|
|
2474
|
+
catch {
|
|
2475
|
+
return createVerificationReadModelQueryStatus(databasePath, 'unreadable', planId);
|
|
2476
|
+
}
|
|
2477
|
+
finally {
|
|
2478
|
+
database.close();
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
export async function readLatestLocalVerificationReadModelQueries(projectRoot) {
|
|
2482
|
+
return readLocalVerificationReadModelQueriesForPlan(projectRoot, null);
|
|
2483
|
+
}
|
|
2484
|
+
function pathSurfaceReadModelWithMatch(base, match) {
|
|
2485
|
+
return {
|
|
2486
|
+
...base,
|
|
2487
|
+
match,
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
function readPathSurfaceReasonMap(database) {
|
|
2491
|
+
const byRule = new Map();
|
|
2492
|
+
for (const row of queryRows(database, 'SELECT rule_id, reason_kind, reason FROM path_surface_reasons ORDER BY rule_id, reason_kind, ordinal')) {
|
|
2493
|
+
const ruleId = toSearchString(row.rule_id);
|
|
2494
|
+
const reasonKind = toSearchString(row.reason_kind);
|
|
2495
|
+
const reason = toSearchString(row.reason);
|
|
2496
|
+
let reasonsByKind = byRule.get(ruleId);
|
|
2497
|
+
if (!reasonsByKind) {
|
|
2498
|
+
reasonsByKind = new Map();
|
|
2499
|
+
byRule.set(ruleId, reasonsByKind);
|
|
2500
|
+
}
|
|
2501
|
+
const reasons = reasonsByKind.get(reasonKind) ?? [];
|
|
2502
|
+
reasons.push(reason);
|
|
2503
|
+
reasonsByKind.set(reasonKind, reasons);
|
|
2504
|
+
}
|
|
2505
|
+
return byRule;
|
|
2506
|
+
}
|
|
2507
|
+
function readPathSurfaceRuleMatches(database) {
|
|
2508
|
+
const reasons = readPathSurfaceReasonMap(database);
|
|
2509
|
+
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) => {
|
|
2510
|
+
const ruleId = toSearchString(row.rule_id);
|
|
2511
|
+
const reasonsByKind = reasons.get(ruleId);
|
|
2512
|
+
const reasonList = (kind) => reasonsByKind?.get(kind) ?? [];
|
|
2513
|
+
return {
|
|
2514
|
+
ruleId,
|
|
2515
|
+
patternKind: toSearchString(row.pattern_kind),
|
|
2516
|
+
pattern: toSearchString(row.pattern),
|
|
2517
|
+
patternFlags: toSearchString(row.pattern_flags),
|
|
2518
|
+
changeKinds: reasonList('change_kind'),
|
|
2519
|
+
surface: {
|
|
2520
|
+
kind: toSearchString(row.surface_kind),
|
|
2521
|
+
category: toSearchString(row.category),
|
|
2522
|
+
isPublicSurface: Number(row.is_public_surface) === 1,
|
|
2523
|
+
validationReasons: reasonList('validation_reason'),
|
|
2524
|
+
affectedContracts: reasonList('affected_contract'),
|
|
2525
|
+
updatePolicy: toSearchString(row.update_policy),
|
|
2526
|
+
driftChecks: reasonList('drift_check'),
|
|
2527
|
+
},
|
|
2528
|
+
};
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
function matchPathSurfaceRule(relativePath, rules) {
|
|
2532
|
+
if (!relativePath) {
|
|
2533
|
+
return null;
|
|
2534
|
+
}
|
|
2535
|
+
for (const rule of rules) {
|
|
2536
|
+
try {
|
|
2537
|
+
if (new RegExp(rule.pattern, rule.patternFlags).test(relativePath)) {
|
|
2538
|
+
return rule;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
catch {
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
return null;
|
|
2546
|
+
}
|
|
2547
|
+
export async function readLocalPathSurfaces(projectRoot, relativePaths) {
|
|
2548
|
+
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
2549
|
+
const normalizedPaths = [...new Set(relativePaths.map((relativePath) => toPosixPath(relativePath)).filter(Boolean))];
|
|
2550
|
+
const statusMap = (status, stalePaths = []) => new Map(normalizedPaths.map((relativePath) => [
|
|
2551
|
+
relativePath,
|
|
2552
|
+
createPathSurfaceReadModelStatus(databasePath, status, relativePath, stalePaths),
|
|
2553
|
+
]));
|
|
2554
|
+
if (!existsSync(databasePath)) {
|
|
2555
|
+
return statusMap('missing');
|
|
2556
|
+
}
|
|
2557
|
+
const SQL = await loadSqlJs();
|
|
2558
|
+
const database = new SQL.Database(readFileSync(databasePath));
|
|
2559
|
+
try {
|
|
2560
|
+
const stalePaths = getStalePaths(projectRoot, database);
|
|
2561
|
+
if (stalePaths.length > 0) {
|
|
2562
|
+
return statusMap('stale', stalePaths);
|
|
2563
|
+
}
|
|
2564
|
+
const rules = readPathSurfaceRuleMatches(database);
|
|
2565
|
+
return new Map(normalizedPaths.map((relativePath) => {
|
|
2566
|
+
const base = createPathSurfaceReadModelStatus(databasePath, 'fresh', relativePath);
|
|
2567
|
+
return [relativePath, pathSurfaceReadModelWithMatch(base, matchPathSurfaceRule(relativePath, rules))];
|
|
2568
|
+
}));
|
|
2569
|
+
}
|
|
2570
|
+
catch {
|
|
2571
|
+
return statusMap('unreadable');
|
|
2572
|
+
}
|
|
2573
|
+
finally {
|
|
2574
|
+
database.close();
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
export async function readLocalPathSurface(projectRoot, relativePath) {
|
|
2578
|
+
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
2579
|
+
const inputPath = relativePath ? toPosixPath(relativePath) : null;
|
|
2580
|
+
if (!inputPath) {
|
|
2581
|
+
if (!existsSync(databasePath)) {
|
|
2582
|
+
return createPathSurfaceReadModelStatus(databasePath, 'missing', null);
|
|
2583
|
+
}
|
|
2584
|
+
const SQL = await loadSqlJs();
|
|
2585
|
+
const database = new SQL.Database(readFileSync(databasePath));
|
|
2586
|
+
try {
|
|
2587
|
+
const stalePaths = getStalePaths(projectRoot, database);
|
|
2588
|
+
return createPathSurfaceReadModelStatus(databasePath, stalePaths.length > 0 ? 'stale' : 'fresh', null, stalePaths);
|
|
2589
|
+
}
|
|
2590
|
+
catch {
|
|
2591
|
+
return createPathSurfaceReadModelStatus(databasePath, 'unreadable', null);
|
|
2592
|
+
}
|
|
2593
|
+
finally {
|
|
2594
|
+
database.close();
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
const surfaces = await readLocalPathSurfaces(projectRoot, [inputPath]);
|
|
2598
|
+
return surfaces.get(inputPath) ?? createPathSurfaceReadModelStatus(databasePath, 'unreadable', inputPath);
|
|
2599
|
+
}
|
|
2600
|
+
export async function readLocalSourceAnchorVerdictRisks(projectRoot, relativePaths) {
|
|
2601
|
+
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
2602
|
+
const normalizedPaths = new Set(relativePaths.map((relativePath) => toPosixPath(relativePath)).filter(Boolean));
|
|
2603
|
+
if (!existsSync(databasePath) || normalizedPaths.size === 0) {
|
|
2604
|
+
return [];
|
|
2605
|
+
}
|
|
2606
|
+
const SQL = await loadSqlJs();
|
|
2607
|
+
const database = new SQL.Database(readFileSync(databasePath));
|
|
2608
|
+
try {
|
|
2609
|
+
const stalePaths = getStalePaths(projectRoot, database);
|
|
2610
|
+
if (stalePaths.length > 0 || !hasTable(database, 'source_anchors') || !hasTable(database, 'source_anchor_status')) {
|
|
2611
|
+
return [];
|
|
2612
|
+
}
|
|
2613
|
+
const rows = queryRows(database, `
|
|
2614
|
+
SELECT
|
|
2615
|
+
source_anchors.id,
|
|
2616
|
+
source_anchors.path,
|
|
2617
|
+
source_anchors.line_start,
|
|
2618
|
+
source_anchors.invariant,
|
|
2619
|
+
source_anchors.risk,
|
|
2620
|
+
source_anchor_status.status
|
|
2621
|
+
FROM source_anchors
|
|
2622
|
+
JOIN source_anchor_status ON source_anchor_status.anchor_id = source_anchors.id
|
|
2623
|
+
WHERE source_anchor_status.status IN ('changed', 'review', 'stale')
|
|
2624
|
+
ORDER BY source_anchors.id
|
|
2625
|
+
`);
|
|
2626
|
+
return rows
|
|
2627
|
+
.map((row) => {
|
|
2628
|
+
const relativePath = toSearchString(row.path);
|
|
2629
|
+
const riskTags = splitIndexedList(row.risk);
|
|
2630
|
+
const status = toSearchString(row.status);
|
|
2631
|
+
if (!normalizedPaths.has(relativePath) ||
|
|
2632
|
+
!hasHighRiskSourceAnchorRiskTags(riskTags) ||
|
|
2633
|
+
(status !== 'changed' && status !== 'review' && status !== 'stale')) {
|
|
2634
|
+
return null;
|
|
2635
|
+
}
|
|
2636
|
+
return {
|
|
2637
|
+
source: 'local_index',
|
|
2638
|
+
authority: 'evidence_only',
|
|
2639
|
+
anchorId: toSearchString(row.id),
|
|
2640
|
+
path: relativePath,
|
|
2641
|
+
lineStart: Number(row.line_start),
|
|
2642
|
+
status,
|
|
2643
|
+
riskTags,
|
|
2644
|
+
invariant: toSearchString(row.invariant) || null,
|
|
2645
|
+
};
|
|
2646
|
+
})
|
|
2647
|
+
.filter((risk) => risk !== null);
|
|
2648
|
+
}
|
|
2649
|
+
catch {
|
|
2650
|
+
return [];
|
|
2651
|
+
}
|
|
2652
|
+
finally {
|
|
2653
|
+
database.close();
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
function getSectionHeadings(database, documentPath) {
|
|
2657
|
+
return queryRows(database, 'SELECT heading FROM sections WHERE document_path = ? ORDER BY ordinal', [documentPath]).map((row) => toSearchString(row.heading));
|
|
2658
|
+
}
|
|
2659
|
+
function getDocumentTerms(database, documentPath) {
|
|
2660
|
+
return queryRows(database, 'SELECT term FROM document_terms WHERE document_path = ? ORDER BY term', [documentPath]).map((row) => toSearchString(row.term));
|
|
2661
|
+
}
|
|
2662
|
+
function getCommandEffects(database, intent) {
|
|
2663
|
+
return queryRows(database, 'SELECT intent, source, access, mode, path, lock, concurrency FROM command_effects WHERE intent = ? ORDER BY lock, path, mode', [intent]).map((row) => ({
|
|
2664
|
+
intent: toSearchString(row.intent),
|
|
2665
|
+
source: toSearchString(row.source),
|
|
2666
|
+
access: toSearchString(row.access),
|
|
2667
|
+
mode: toSearchString(row.mode),
|
|
2668
|
+
path: row.path === null || row.path === undefined ? null : toSearchString(row.path),
|
|
2669
|
+
lock: toSearchString(row.lock),
|
|
2670
|
+
concurrency: toSearchString(row.concurrency),
|
|
2671
|
+
}));
|
|
2672
|
+
}
|
|
2673
|
+
const EMPTY_INDEXED_SEARCH_MATCHES = {
|
|
2674
|
+
active: false,
|
|
2675
|
+
documents: new Set(),
|
|
2676
|
+
skills: new Set(),
|
|
2677
|
+
skillRoutes: new Set(),
|
|
2678
|
+
commandIntents: new Set(),
|
|
2679
|
+
sourceAnchors: new Set(),
|
|
2680
|
+
};
|
|
2681
|
+
function buildFtsQuery(query) {
|
|
2682
|
+
const tokens = extractSearchTokens(query);
|
|
2683
|
+
if (tokens.length === 0) {
|
|
2684
|
+
return null;
|
|
2685
|
+
}
|
|
2686
|
+
return [...new Set(tokens)].map((token) => `"${token.replaceAll('"', '""')}"`).join(' AND ');
|
|
2687
|
+
}
|
|
2688
|
+
function queryFtsSet(database, sql, ftsQuery, column) {
|
|
2689
|
+
return new Set(queryRows(database, sql, [ftsQuery]).map((row) => toSearchString(row[column])));
|
|
2690
|
+
}
|
|
2691
|
+
function mergeSearchSets(left, right) {
|
|
2692
|
+
return new Set([...left, ...right]);
|
|
2693
|
+
}
|
|
2694
|
+
function mergeIndexedSearchMatches(left, right) {
|
|
2695
|
+
return {
|
|
2696
|
+
active: left.active || right.active,
|
|
2697
|
+
documents: mergeSearchSets(left.documents, right.documents),
|
|
2698
|
+
skills: mergeSearchSets(left.skills, right.skills),
|
|
2699
|
+
skillRoutes: mergeSearchSets(left.skillRoutes, right.skillRoutes),
|
|
2700
|
+
commandIntents: mergeSearchSets(left.commandIntents, right.commandIntents),
|
|
2701
|
+
sourceAnchors: mergeSearchSets(left.sourceAnchors, right.sourceAnchors),
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
function queryNgramSet(database, targetKind, grams) {
|
|
2705
|
+
const placeholders = grams.map(() => '?').join(', ');
|
|
2706
|
+
if (!placeholders) {
|
|
2707
|
+
return new Set();
|
|
2708
|
+
}
|
|
2709
|
+
return new Set(queryRows(database, `SELECT target_key
|
|
2710
|
+
FROM search_ngrams
|
|
2711
|
+
WHERE target_kind = ? AND gram IN (${placeholders})
|
|
2712
|
+
GROUP BY target_key
|
|
2713
|
+
HAVING COUNT(DISTINCT gram) = ?`, [targetKind, ...grams, grams.length]).map((row) => toSearchString(row.target_key)));
|
|
2714
|
+
}
|
|
2715
|
+
function getNgramSearchMatches(database, query) {
|
|
2716
|
+
if (!hasTable(database, 'search_ngrams')) {
|
|
2717
|
+
return EMPTY_INDEXED_SEARCH_MATCHES;
|
|
2718
|
+
}
|
|
2719
|
+
const grams = buildSearchNgrams([query]);
|
|
2720
|
+
if (grams.length === 0) {
|
|
2721
|
+
return EMPTY_INDEXED_SEARCH_MATCHES;
|
|
2722
|
+
}
|
|
2723
|
+
return {
|
|
2724
|
+
active: true,
|
|
2725
|
+
documents: queryNgramSet(database, 'document', grams),
|
|
2726
|
+
skills: queryNgramSet(database, 'skill', grams),
|
|
2727
|
+
skillRoutes: queryNgramSet(database, 'skill_route', grams),
|
|
2728
|
+
commandIntents: queryNgramSet(database, 'command_intent', grams),
|
|
2729
|
+
sourceAnchors: queryNgramSet(database, 'source_anchor', grams),
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
function getIndexedSearchMatches(database, query) {
|
|
2733
|
+
const capabilities = readStoredSearchCapabilities(database);
|
|
2734
|
+
const ftsQuery = capabilities.backend === SEARCH_BACKEND_FTS5 ? buildFtsQuery(query) : null;
|
|
2735
|
+
const ngramMatches = getNgramSearchMatches(database, query);
|
|
2736
|
+
if (!ftsQuery) {
|
|
2737
|
+
return ngramMatches;
|
|
2738
|
+
}
|
|
2739
|
+
try {
|
|
2740
|
+
const ftsMatches = {
|
|
2741
|
+
active: true,
|
|
2742
|
+
documents: queryFtsSet(database, 'SELECT path FROM search_documents_fts WHERE search_documents_fts MATCH ?', ftsQuery, 'path'),
|
|
2743
|
+
skills: queryFtsSet(database, 'SELECT name FROM search_skills_fts WHERE search_skills_fts MATCH ?', ftsQuery, 'name'),
|
|
2744
|
+
skillRoutes: queryFtsSet(database, 'SELECT route_key FROM search_skill_routes_fts WHERE search_skill_routes_fts MATCH ?', ftsQuery, 'route_key'),
|
|
2745
|
+
commandIntents: queryFtsSet(database, 'SELECT name FROM search_command_intents_fts WHERE search_command_intents_fts MATCH ?', ftsQuery, 'name'),
|
|
2746
|
+
sourceAnchors: queryFtsSet(database, 'SELECT id FROM search_source_anchors_fts WHERE search_source_anchors_fts MATCH ?', ftsQuery, 'id'),
|
|
2747
|
+
};
|
|
2748
|
+
return mergeIndexedSearchMatches(ftsMatches, ngramMatches);
|
|
2749
|
+
}
|
|
2750
|
+
catch {
|
|
2751
|
+
return ngramMatches;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
function matchesIndexedOrTableScan(fields, query, indexedMatches, matchSet, key) {
|
|
2755
|
+
return (indexedMatches.active && matchSet.has(key)) || isMatched(fields, query);
|
|
2756
|
+
}
|
|
2757
|
+
function scoreIndexedOrTableScan(primaryFields, secondaryFields, query, indexedMatches, matchSet, key) {
|
|
2758
|
+
const tableScore = scoreMatch(primaryFields, secondaryFields, query);
|
|
2759
|
+
return indexedMatches.active && matchSet.has(key) ? Math.max(tableScore, 20) : tableScore;
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* mf:anchor cli.search.local-index
|
|
2763
|
+
* purpose: Search the local index while preserving workflow authority above source navigation hints.
|
|
2764
|
+
* search: mf search, scope workflow source all, authority rank, navigation only
|
|
2765
|
+
* invariant: Source anchor results remain navigation-only and cannot outrank command or workflow authority.
|
|
2766
|
+
* risk: cache, config
|
|
2767
|
+
*/
|
|
2768
|
+
export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
2769
|
+
const normalizedQuery = normalizeSearchText(query);
|
|
2770
|
+
const limit = Math.max(1, Math.min(options.limit ?? 10, 50));
|
|
2771
|
+
const scope = options.scope ?? 'workflow';
|
|
2772
|
+
const databasePath = getLocalIndexDatabasePath(projectRoot);
|
|
2773
|
+
if (!existsSync(databasePath)) {
|
|
2774
|
+
throw new Error('Local mustflow index not found. Run `mf index` before searching.');
|
|
2775
|
+
}
|
|
2776
|
+
if (normalizedQuery.length === 0) {
|
|
2777
|
+
throw new Error('Search query must not be empty.');
|
|
2778
|
+
}
|
|
2779
|
+
const SQL = await loadSqlJs();
|
|
2780
|
+
const database = new SQL.Database(readFileSync(databasePath));
|
|
2781
|
+
const cacheLayers = readCacheLayerSets(projectRoot);
|
|
2782
|
+
let capabilities = searchCapabilities(false);
|
|
2783
|
+
const results = [];
|
|
2784
|
+
try {
|
|
2785
|
+
const stalePaths = getStalePaths(projectRoot, database);
|
|
2786
|
+
capabilities = readStoredSearchCapabilities(database);
|
|
2787
|
+
const indexedMatches = getIndexedSearchMatches(database, normalizedQuery);
|
|
2788
|
+
if (stalePaths.length > 0) {
|
|
2789
|
+
throw new Error(`Local mustflow index is stale: ${stalePaths.join(', ')}. Run \`mf index\` before searching. Refresh command: mf index`);
|
|
2790
|
+
}
|
|
2791
|
+
if (scope === 'workflow' || scope === 'all') {
|
|
2792
|
+
for (const row of queryRows(database, 'SELECT path, type, title, content_snippet FROM documents')) {
|
|
2793
|
+
const pathValue = toSearchString(row.path);
|
|
2794
|
+
const typeValue = toSearchString(row.type);
|
|
2795
|
+
const title = toSearchString(row.title);
|
|
2796
|
+
const contentSnippet = toSearchString(row.content_snippet);
|
|
2797
|
+
const sectionHeadings = getSectionHeadings(database, pathValue);
|
|
2798
|
+
const documentTerms = getDocumentTerms(database, pathValue);
|
|
2799
|
+
const primaryFields = [pathValue, title];
|
|
2800
|
+
const secondaryFields = [typeValue, contentSnippet, ...sectionHeadings, ...documentTerms];
|
|
2801
|
+
const fields = [...primaryFields, ...secondaryFields];
|
|
2802
|
+
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.documents, pathValue)) {
|
|
2803
|
+
continue;
|
|
2804
|
+
}
|
|
2805
|
+
results.push(withCacheHint({
|
|
2806
|
+
kind: 'document',
|
|
2807
|
+
path: pathValue,
|
|
2808
|
+
title,
|
|
2809
|
+
document_type: typeValue,
|
|
2810
|
+
...workflowAuthorityForDocument(typeValue),
|
|
2811
|
+
match: getMatchSnippet(fields, normalizedQuery),
|
|
2812
|
+
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.documents, pathValue),
|
|
2813
|
+
}, cacheLayers));
|
|
2814
|
+
}
|
|
2815
|
+
for (const row of queryRows(database, 'SELECT name, path, title FROM skills')) {
|
|
2816
|
+
const name = toSearchString(row.name);
|
|
2817
|
+
const pathValue = toSearchString(row.path);
|
|
2818
|
+
const title = toSearchString(row.title);
|
|
2819
|
+
const fields = [name, pathValue, title];
|
|
2820
|
+
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.skills, name)) {
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
results.push(withCacheHint({
|
|
2824
|
+
kind: 'skill',
|
|
2825
|
+
name,
|
|
2826
|
+
path: pathValue,
|
|
2827
|
+
title,
|
|
2828
|
+
...skillAuthority(),
|
|
2829
|
+
match: getMatchSnippet(fields, normalizedQuery),
|
|
2830
|
+
score: scoreIndexedOrTableScan([name, pathValue, title], [], normalizedQuery, indexedMatches, indexedMatches.skills, name),
|
|
2831
|
+
}, cacheLayers));
|
|
2832
|
+
}
|
|
2833
|
+
for (const row of queryRows(database, 'SELECT skill_name, skill_path, trigger, required_input, edit_scope, risk, verification_intents, expected_output FROM skill_routes')) {
|
|
2834
|
+
const name = toSearchString(row.skill_name);
|
|
2835
|
+
const pathValue = toSearchString(row.skill_path);
|
|
2836
|
+
const trigger = toSearchString(row.trigger);
|
|
2837
|
+
const requiredInput = toSearchString(row.required_input);
|
|
2838
|
+
const editScope = toSearchString(row.edit_scope);
|
|
2839
|
+
const risk = toSearchString(row.risk);
|
|
2840
|
+
const verificationIntents = splitVerificationIntents(toSearchString(row.verification_intents));
|
|
2841
|
+
const expectedOutput = toSearchString(row.expected_output);
|
|
2842
|
+
const primaryFields = [name, trigger];
|
|
2843
|
+
const secondaryFields = [pathValue, requiredInput, editScope, risk, expectedOutput];
|
|
2844
|
+
const fields = [...primaryFields, ...secondaryFields];
|
|
2845
|
+
const routeKey = skillRouteKey({ skillName: name, trigger });
|
|
2846
|
+
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.skillRoutes, routeKey)) {
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
results.push(withCacheHint({
|
|
2850
|
+
kind: 'skill_route',
|
|
2851
|
+
name,
|
|
2852
|
+
path: pathValue,
|
|
2853
|
+
title: name,
|
|
2854
|
+
route_trigger: trigger,
|
|
2855
|
+
route_risk: risk,
|
|
2856
|
+
verification_intents: verificationIntents,
|
|
2857
|
+
...skillAuthority(),
|
|
2858
|
+
match: getMatchSnippet(fields, normalizedQuery),
|
|
2859
|
+
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.skillRoutes, routeKey),
|
|
2860
|
+
}, cacheLayers));
|
|
2861
|
+
}
|
|
2862
|
+
for (const row of queryRows(database, 'SELECT name, status, lifecycle, run_policy, description FROM command_intents')) {
|
|
2863
|
+
const name = toSearchString(row.name);
|
|
2864
|
+
const status = toSearchString(row.status);
|
|
2865
|
+
const lifecycle = toSearchString(row.lifecycle);
|
|
2866
|
+
const runPolicy = toSearchString(row.run_policy);
|
|
2867
|
+
const description = toSearchString(row.description);
|
|
2868
|
+
const effects = getCommandEffects(database, name);
|
|
2869
|
+
const effectLocks = [...new Set(effects.map((effect) => effect.lock))].sort((left, right) => left.localeCompare(right));
|
|
2870
|
+
const effectPaths = [
|
|
2871
|
+
...new Set(effects.map((effect) => effect.path).filter((effectPath) => effectPath !== null)),
|
|
2872
|
+
].sort((left, right) => left.localeCompare(right));
|
|
2873
|
+
const effectModes = [...new Set(effects.map((effect) => effect.mode))].sort((left, right) => left.localeCompare(right));
|
|
2874
|
+
const primaryFields = [name];
|
|
2875
|
+
const secondaryFields = [status, lifecycle, runPolicy, description, ...effectLocks, ...effectPaths, ...effectModes];
|
|
2876
|
+
const fields = [...primaryFields, ...secondaryFields];
|
|
2877
|
+
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.commandIntents, name)) {
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
results.push(withCacheHint({
|
|
2881
|
+
kind: 'command_intent',
|
|
2882
|
+
name,
|
|
2883
|
+
title: description || name,
|
|
2884
|
+
effect_locks: effectLocks,
|
|
2885
|
+
effect_paths: effectPaths,
|
|
2886
|
+
effect_modes: effectModes,
|
|
2887
|
+
...commandIntentAuthority(),
|
|
2888
|
+
match: getMatchSnippet(fields, normalizedQuery),
|
|
2889
|
+
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.commandIntents, name),
|
|
2890
|
+
}, cacheLayers));
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
if (scope === 'source' || scope === 'all') {
|
|
2894
|
+
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')) {
|
|
2895
|
+
const id = toSearchString(row.id);
|
|
2896
|
+
const pathValue = toSearchString(row.path);
|
|
2897
|
+
const purpose = toSearchString(row.purpose);
|
|
2898
|
+
const searchTerms = toSearchString(row.search_terms);
|
|
2899
|
+
const invariant = toSearchString(row.invariant);
|
|
2900
|
+
const risk = toSearchString(row.risk);
|
|
2901
|
+
const primaryFields = [id, pathValue];
|
|
2902
|
+
const secondaryFields = [purpose, searchTerms, invariant, risk];
|
|
2903
|
+
const fields = [...primaryFields, ...secondaryFields];
|
|
2904
|
+
if (!matchesIndexedOrTableScan(fields, normalizedQuery, indexedMatches, indexedMatches.sourceAnchors, id)) {
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
results.push(withCacheHint({
|
|
2908
|
+
kind: 'source_anchor',
|
|
2909
|
+
anchor_id: id,
|
|
2910
|
+
name: id,
|
|
2911
|
+
path: pathValue,
|
|
2912
|
+
line_start: Number(row.line_start),
|
|
2913
|
+
title: purpose || id,
|
|
2914
|
+
risk,
|
|
2915
|
+
...sourceAnchorAuthority(),
|
|
2916
|
+
stale_status: toSearchString(row.status),
|
|
2917
|
+
stale_confidence: Number(row.confidence),
|
|
2918
|
+
match: getMatchSnippet(fields, normalizedQuery),
|
|
2919
|
+
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.sourceAnchors, id),
|
|
2920
|
+
}, cacheLayers));
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
finally {
|
|
2925
|
+
database.close();
|
|
2926
|
+
}
|
|
2927
|
+
const sortedResults = results
|
|
2928
|
+
.sort((left, right) => {
|
|
2929
|
+
if (scope === 'all' && left.authority_rank !== right.authority_rank) {
|
|
2930
|
+
return left.authority_rank - right.authority_rank;
|
|
2931
|
+
}
|
|
2932
|
+
return right.score - left.score || (left.path ?? left.name ?? '').localeCompare(right.path ?? right.name ?? '');
|
|
2933
|
+
})
|
|
2934
|
+
.slice(0, limit);
|
|
2935
|
+
return {
|
|
2936
|
+
schema_version: LOCAL_INDEX_SCHEMA_VERSION,
|
|
2937
|
+
command: 'search',
|
|
2938
|
+
ok: true,
|
|
2939
|
+
mustflow_root: path.resolve(projectRoot),
|
|
2940
|
+
database_path: databasePath,
|
|
2941
|
+
query: normalizedQuery,
|
|
2942
|
+
limit,
|
|
2943
|
+
scope,
|
|
2944
|
+
index_fresh: true,
|
|
2945
|
+
stale_paths: [],
|
|
2946
|
+
search_backend: capabilities.backend,
|
|
2947
|
+
search_fts5_available: capabilities.fts5Available,
|
|
2948
|
+
result_count: sortedResults.length,
|
|
2949
|
+
results: sortedResults,
|
|
2950
|
+
};
|
|
2951
|
+
}
|