mustflow 2.115.3 → 2.115.5

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.
@@ -1,4 +1,4 @@
1
- export const LOCAL_INDEX_SCHEMA_VERSION = '20';
1
+ export const LOCAL_INDEX_SCHEMA_VERSION = '21';
2
2
  export const LOCAL_INDEX_PARSER_VERSION = '1';
3
3
  export const DEFAULT_DATABASE_RELATIVE_PATH = '.mustflow/cache/mustflow.sqlite';
4
4
  export const LATEST_RUN_STATE_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
@@ -1,6 +1,6 @@
1
- import { LOCAL_INDEX_SCHEMA_VERSION } from './constants.js';
1
+ import { DEFAULT_DATABASE_RELATIVE_PATH, LOCAL_INDEX_SCHEMA_VERSION } from './constants.js';
2
2
  import { hasTable, queryRows, readMetadataValue, toSearchString } from './database-read.js';
3
- import { normalizeIndexedFileSourceScope, readIndexedFileRecord } from './source-index.js';
3
+ import { collectSourceAnchorCandidatePaths, getSourceScopeHash, normalizeIndexedFileSourceScope, readIndexedFileRecord, readLocalIndexSourceConfig, } from './source-index.js';
4
4
  import { collectDocuments } from './workflow-documents.js';
5
5
  export function readStoredSchemaVersion(database) {
6
6
  return readMetadataValue(database, 'schema_version');
@@ -11,46 +11,76 @@ export function getStalePaths(projectRoot, database, options = {}) {
11
11
  if (schemaVersion !== LOCAL_INDEX_SCHEMA_VERSION) {
12
12
  return ['.mustflow/cache/mustflow.sqlite'];
13
13
  }
14
- if (hasTable(database, 'indexed_files')) {
15
- const stalePaths = new Set();
16
- const indexedRows = queryRows(database, 'SELECT path, source_scope, content_hash FROM indexed_files');
17
- const indexedPaths = new Set(indexedRows.map((row) => toSearchString(row.path)));
18
- for (const row of indexedRows) {
19
- const indexedPath = toSearchString(row.path);
20
- const sourceScope = normalizeIndexedFileSourceScope(toSearchString(row.source_scope));
21
- if (!includeState && sourceScope === 'state') {
22
- continue;
23
- }
24
- try {
25
- const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
26
- if (current.contentHash !== toSearchString(row.content_hash)) {
27
- stalePaths.add(indexedPath);
28
- }
29
- }
30
- catch {
14
+ if (!hasTable(database, 'indexed_files') ||
15
+ !hasTable(database, 'indexed_source_candidates') ||
16
+ !hasTable(database, 'source_anchors') ||
17
+ !hasTable(database, 'source_anchor_status')) {
18
+ return [DEFAULT_DATABASE_RELATIVE_PATH];
19
+ }
20
+ const stalePaths = new Set();
21
+ const indexedRows = queryRows(database, 'SELECT path, source_scope, content_hash FROM indexed_files');
22
+ const indexedPaths = new Set(indexedRows.map((row) => toSearchString(row.path)));
23
+ const indexedSourcePaths = new Set(queryRows(database, 'SELECT path FROM indexed_source_candidates').map((row) => toSearchString(row.path)));
24
+ for (const sourcePath of indexedSourcePaths) {
25
+ if (!indexedPaths.has(sourcePath)) {
26
+ stalePaths.add(sourcePath);
27
+ }
28
+ }
29
+ for (const row of indexedRows) {
30
+ const indexedPath = toSearchString(row.path);
31
+ const sourceScope = normalizeIndexedFileSourceScope(toSearchString(row.source_scope));
32
+ if (!includeState && sourceScope === 'state') {
33
+ continue;
34
+ }
35
+ try {
36
+ const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
37
+ if (current.contentHash !== toSearchString(row.content_hash)) {
31
38
  stalePaths.add(indexedPath);
32
39
  }
33
40
  }
34
- for (const document of collectDocuments(projectRoot)) {
35
- if (!indexedPaths.has(document.path)) {
36
- stalePaths.add(document.path);
37
- }
41
+ catch {
42
+ stalePaths.add(indexedPath);
38
43
  }
39
- return Array.from(stalePaths).sort((left, right) => left.localeCompare(right));
40
44
  }
41
- const indexedRows = queryRows(database, 'SELECT path, content_hash FROM documents');
42
- const indexedHashes = new Map(indexedRows.map((row) => [toSearchString(row.path), toSearchString(row.content_hash)]));
43
- const currentDocuments = collectDocuments(projectRoot);
44
- const currentHashes = new Map(currentDocuments.map((document) => [document.path, document.contentHash]));
45
- const stalePaths = new Set();
46
- for (const [indexedPath, indexedHash] of indexedHashes) {
47
- if (currentHashes.get(indexedPath) !== indexedHash) {
48
- stalePaths.add(indexedPath);
45
+ for (const document of collectDocuments(projectRoot)) {
46
+ if (!indexedPaths.has(document.path)) {
47
+ stalePaths.add(document.path);
49
48
  }
50
49
  }
51
- for (const currentPath of currentHashes.keys()) {
52
- if (!indexedHashes.has(currentPath)) {
53
- stalePaths.add(currentPath);
50
+ const sourceIndexEnabledValue = readMetadataValue(database, 'source_index_enabled');
51
+ if (sourceIndexEnabledValue !== 'true' && sourceIndexEnabledValue !== 'false') {
52
+ stalePaths.add(DEFAULT_DATABASE_RELATIVE_PATH);
53
+ }
54
+ else {
55
+ const sourceIndexEnabled = sourceIndexEnabledValue === 'true';
56
+ const hasIndexedSourceScope = indexedRows.some((row) => normalizeIndexedFileSourceScope(toSearchString(row.source_scope)) === 'source_anchor');
57
+ const hasSourceAnchorRows = hasTable(database, 'source_anchors') && queryRows(database, 'SELECT id FROM source_anchors LIMIT 1').length > 0;
58
+ if (!sourceIndexEnabled && (indexedSourcePaths.size > 0 || hasIndexedSourceScope || hasSourceAnchorRows)) {
59
+ stalePaths.add(DEFAULT_DATABASE_RELATIVE_PATH);
60
+ }
61
+ try {
62
+ const sourceConfig = readLocalIndexSourceConfig(projectRoot);
63
+ const storedSourceScopeHash = readMetadataValue(database, 'source_scope_hash');
64
+ const currentSourceScopeHash = getSourceScopeHash(sourceIndexEnabled, sourceConfig);
65
+ if (storedSourceScopeHash !== currentSourceScopeHash) {
66
+ stalePaths.add(DEFAULT_DATABASE_RELATIVE_PATH);
67
+ }
68
+ if (sourceIndexEnabled) {
69
+ const currentSourcePaths = new Set(collectSourceAnchorCandidatePaths(projectRoot, sourceConfig));
70
+ for (const sourcePath of currentSourcePaths) {
71
+ if (!indexedSourcePaths.has(sourcePath)) {
72
+ stalePaths.add(sourcePath);
73
+ }
74
+ }
75
+ for (const sourcePath of indexedSourcePaths) {
76
+ if (!currentSourcePaths.has(sourcePath)) {
77
+ stalePaths.add(sourcePath);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ catch {
83
+ stalePaths.add(DEFAULT_DATABASE_RELATIVE_PATH);
54
84
  }
55
85
  }
56
86
  return Array.from(stalePaths).sort((left, right) => left.localeCompare(right));
@@ -158,6 +158,14 @@ function indexedFilesMatch(database, currentFiles) {
158
158
  }
159
159
  return true;
160
160
  }
161
+ function indexedSourceCandidatesMatch(database, currentPaths) {
162
+ if (!hasTable(database, 'indexed_source_candidates')) {
163
+ return false;
164
+ }
165
+ const storedPaths = new Set(queryRows(database, 'SELECT path FROM indexed_source_candidates').map((row) => toSearchString(row.path)));
166
+ const currentPathSet = new Set(currentPaths);
167
+ return storedPaths.size === currentPathSet.size && [...storedPaths].every((storedPath) => currentPathSet.has(storedPath));
168
+ }
161
169
  const INDEXED_FILE_MTIME_TOLERANCE_MS = 1;
162
170
  function indexedFileMtimeMsEqual(storedMtimeMs, currentMtimeMs) {
163
171
  return storedMtimeMs !== null && Math.abs(storedMtimeMs - currentMtimeMs) <= INDEXED_FILE_MTIME_TOLERANCE_MS;
@@ -183,8 +191,8 @@ function indexedFileMetadataMatch(database, currentFiles) {
183
191
  }
184
192
  return true;
185
193
  }
186
- async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, currentFiles, sourceScopeHash, dryRun, indexMode) {
187
- if (!currentFiles) {
194
+ async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, currentFiles, currentSourceCandidatePaths, sourceScopeHash, dryRun, indexMode) {
195
+ if (!currentFiles || !currentSourceCandidatePaths) {
188
196
  return { result: null, rebuildReason: null };
189
197
  }
190
198
  if (!existsSync(databasePath)) {
@@ -205,6 +213,9 @@ async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, cur
205
213
  if (!hasTable(database, 'indexed_files')) {
206
214
  return { result: null, rebuildReason: 'indexed_files_missing' };
207
215
  }
216
+ if (!indexedSourceCandidatesMatch(database, currentSourceCandidatePaths)) {
217
+ return { result: null, rebuildReason: 'source_scope_mismatch' };
218
+ }
208
219
  if (!indexedFileMetadataMatch(database, currentFiles)) {
209
220
  return { result: null, rebuildReason: 'file_fingerprint_mismatch' };
210
221
  }
@@ -225,7 +236,7 @@ async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, cur
225
236
  database?.close();
226
237
  }
227
238
  }
228
- async function readIncrementalReuseDecision(SQL, databasePath, currentFiles, sourceScopeHash) {
239
+ async function readIncrementalReuseDecision(SQL, databasePath, currentFiles, currentSourceCandidatePaths, sourceScopeHash) {
229
240
  if (!existsSync(databasePath)) {
230
241
  return { reusable: false, rebuildReason: 'missing_index', capabilities: null };
231
242
  }
@@ -244,6 +255,9 @@ async function readIncrementalReuseDecision(SQL, databasePath, currentFiles, sou
244
255
  if (!hasTable(database, 'indexed_files')) {
245
256
  return { reusable: false, rebuildReason: 'indexed_files_missing', capabilities: null };
246
257
  }
258
+ if (!indexedSourceCandidatesMatch(database, currentSourceCandidatePaths)) {
259
+ return { reusable: false, rebuildReason: 'source_scope_mismatch', capabilities: null };
260
+ }
247
261
  if (!indexedFilesMatch(database, currentFiles)) {
248
262
  return { reusable: false, rebuildReason: 'file_fingerprint_mismatch', capabilities: null };
249
263
  }
@@ -284,7 +298,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
284
298
  capabilityDatabase.close();
285
299
  if (incremental) {
286
300
  const preflightFiles = collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig);
287
- const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles, sourceScopeHash, dryRun, indexMode);
301
+ const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles?.records ?? null, preflightFiles?.sourceCandidatePaths ?? null, sourceScopeHash, dryRun, indexMode);
288
302
  if (preflightReuse.result) {
289
303
  return preflightReuse.result;
290
304
  }
@@ -307,16 +321,16 @@ export async function createLocalIndex(projectRoot, options = {}) {
307
321
  const verificationEvidence = createVerificationEvidenceIndex(projectRoot);
308
322
  const indexedFiles = collectIndexedFileRecords(projectRoot, documents, sourceAnchors, sourceAnchorCandidatePaths);
309
323
  if (incremental) {
310
- const reuseDecision = await readIncrementalReuseDecision(SQL, databasePath, indexedFiles, sourceScopeHash);
324
+ const reuseDecision = await readIncrementalReuseDecision(SQL, databasePath, indexedFiles, sourceAnchorCandidatePaths, sourceScopeHash);
311
325
  reusedExisting = reuseDecision.reusable;
312
- rebuildReason = reuseDecision.rebuildReason ?? rebuildReason;
326
+ rebuildReason = reuseDecision.reusable ? null : (reuseDecision.rebuildReason ?? rebuildReason);
313
327
  capabilities = reuseDecision.capabilities ?? capabilities;
314
328
  }
315
329
  if (!dryRun && !reusedExisting) {
316
330
  const database = new SQL.Database();
317
331
  try {
318
332
  createSchema(database, capabilities);
319
- populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, verificationEvidence, indexMode, sourceScopeHash, includeSource, new Date().toISOString());
333
+ populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, sourceAnchorCandidatePaths, verificationEvidence, indexMode, sourceScopeHash, includeSource, new Date().toISOString());
320
334
  writeFileInsideWithoutSymlinks(projectRoot, databasePath, database.export());
321
335
  }
322
336
  finally {
@@ -702,10 +716,13 @@ export async function readLocalSourceAnchorCheckWarnings(projectRoot) {
702
716
  if (readMetadataValue(database, 'source_index_enabled') !== 'true') {
703
717
  return [];
704
718
  }
705
- if (!hasTable(database, 'indexed_files') || !hasTable(database, 'source_anchors') || !hasTable(database, 'source_anchor_status')) {
719
+ if (!hasTable(database, 'indexed_files') ||
720
+ !hasTable(database, 'indexed_source_candidates') ||
721
+ !hasTable(database, 'source_anchors') ||
722
+ !hasTable(database, 'source_anchor_status')) {
706
723
  return ['source anchor local index is missing source-anchor tables; refresh with mf index --source'];
707
724
  }
708
- const indexedSourcePaths = new Set(queryRows(database, 'SELECT path FROM indexed_files WHERE source_scope = "source_anchor"')
725
+ const indexedSourcePaths = new Set(queryRows(database, 'SELECT path FROM indexed_source_candidates')
709
726
  .map((row) => toSearchString(row.path))
710
727
  .filter(Boolean));
711
728
  const staleSourcePaths = getStalePaths(projectRoot, database, { includeState: false }).filter((stalePath) => indexedSourcePaths.has(stalePath));
@@ -172,7 +172,7 @@ function rollbackTransaction(database) {
172
172
  // Keep the original indexing failure as the actionable error.
173
173
  }
174
174
  }
175
- export function populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, verificationEvidence, indexMode, sourceScopeHash, sourceIndexEnabled, indexedAt) {
175
+ export function populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, indexedSourceCandidates, verificationEvidence, indexMode, sourceScopeHash, sourceIndexEnabled, indexedAt) {
176
176
  const sourceAnchorRiskSignals = createSourceAnchorRiskSignals(sourceAnchors);
177
177
  database.run('BEGIN TRANSACTION');
178
178
  try {
@@ -223,6 +223,9 @@ export function populateDatabase(database, capabilities, documents, skills, skil
223
223
  LOCAL_INDEX_PARSER_VERSION,
224
224
  ]);
225
225
  }
226
+ for (const sourceCandidatePath of indexedSourceCandidates) {
227
+ database.run('INSERT INTO indexed_source_candidates (path) VALUES (?)', [sourceCandidatePath]);
228
+ }
226
229
  for (const document of documents) {
227
230
  database.run('INSERT INTO documents (path, type, title, locale, revision, content_hash, content_snippet) VALUES (?, ?, ?, ?, ?, ?, ?)', [
228
231
  document.path,
@@ -1,5 +1,10 @@
1
1
  import { SEARCH_BACKEND_FTS5 } from './constants.js';
2
2
  export function createSchema(database, capabilities) {
3
+ database.run('PRAGMA foreign_keys = ON');
4
+ const [foreignKeyResult] = database.exec('PRAGMA foreign_keys');
5
+ if (foreignKeyResult?.values[0]?.[0] !== 1) {
6
+ throw new Error('SQLite foreign-key enforcement is unavailable for the local index.');
7
+ }
3
8
  database.run(`
4
9
  CREATE TABLE metadata (
5
10
  key TEXT PRIMARY KEY,
@@ -17,6 +22,11 @@ CREATE TABLE indexed_files (
17
22
  parser_version TEXT NOT NULL
18
23
  );
19
24
 
25
+ CREATE TABLE indexed_source_candidates (
26
+ path TEXT PRIMARY KEY,
27
+ FOREIGN KEY (path) REFERENCES indexed_files(path) ON UPDATE RESTRICT ON DELETE RESTRICT
28
+ );
29
+
20
30
  CREATE TABLE documents (
21
31
  path TEXT PRIMARY KEY,
22
32
  type TEXT NOT NULL,
@@ -1,11 +1,34 @@
1
- import { existsSync, readFileSync, statSync } from 'node:fs';
1
+ import { existsSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isRecord, readStringArray } from '../command-contract.js';
4
+ import { MUSTFLOW_TEXT_MAX_BYTES, mustflowProjectPath } from '../mustflow-read.js';
4
5
  import { readMustflowTomlFile } from '../toml.js';
6
+ import { ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from '../../../core/safe-filesystem.js';
5
7
  import { listSourceAnchorFiles } from '../../../core/source-anchors.js';
6
8
  import { INDEX_CONFIG_RELATIVE_PATH, LATEST_RUN_STATE_RELATIVE_PATH, MUSTFLOW_RELATIVE_PATH, SOURCE_INDEX_MAX_FILE_BYTES, } from './constants.js';
7
9
  import { sha256Bytes, sha256Text } from './hashing.js';
8
10
  import { getExistingIndexablePaths } from './workflow-documents.js';
11
+ function indexedProjectPath(projectRoot, relativePath) {
12
+ if (relativePath.length === 0 ||
13
+ relativePath.includes('\0') ||
14
+ relativePath.includes('\\') ||
15
+ path.posix.isAbsolute(relativePath) ||
16
+ path.win32.isAbsolute(relativePath)) {
17
+ throw new Error(`Indexed file path must be a canonical project-relative path: ${relativePath}`);
18
+ }
19
+ const segments = relativePath.split('/');
20
+ if (segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
21
+ throw new Error(`Indexed file path must be a canonical project-relative path: ${relativePath}`);
22
+ }
23
+ const fullPath = mustflowProjectPath(projectRoot, relativePath);
24
+ ensureInsideWithoutSymlinks(projectRoot, fullPath);
25
+ return fullPath;
26
+ }
27
+ function readIndexedFileBytes(projectRoot, relativePath) {
28
+ return readFileInsideWithoutSymlinks(projectRoot, indexedProjectPath(projectRoot, relativePath), {
29
+ maxBytes: MUSTFLOW_TEXT_MAX_BYTES,
30
+ });
31
+ }
9
32
  function readIndexToml(projectRoot) {
10
33
  const indexConfigPath = path.join(projectRoot, ...INDEX_CONFIG_RELATIVE_PATH.split('/'));
11
34
  if (!existsSync(indexConfigPath)) {
@@ -69,13 +92,13 @@ export function readIndexedFileRecord(projectRoot, relativePath, sourceScope, co
69
92
  const metadata = readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope);
70
93
  return {
71
94
  ...metadata,
72
- contentHash: contentHash ?? sha256Bytes(readFileSync(path.join(projectRoot, ...relativePath.split('/')))),
95
+ contentHash: contentHash ?? sha256Bytes(readIndexedFileBytes(projectRoot, relativePath)),
73
96
  };
74
97
  }
75
98
  function hashIndexedFileMetadataRecord(projectRoot, metadata) {
76
99
  return {
77
100
  ...metadata,
78
- contentHash: sha256Bytes(readFileSync(path.join(projectRoot, ...metadata.path.split('/')))),
101
+ contentHash: sha256Bytes(readIndexedFileBytes(projectRoot, metadata.path)),
79
102
  };
80
103
  }
81
104
  function tryHashIndexedFileMetadataRecord(projectRoot, metadata) {
@@ -92,8 +115,11 @@ export function hashIndexedFileMetadataRecords(projectRoot, metadataRecords) {
92
115
  .filter((record) => Boolean(record));
93
116
  }
94
117
  export function readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope) {
95
- const fullPath = path.join(projectRoot, ...relativePath.split('/'));
118
+ const fullPath = indexedProjectPath(projectRoot, relativePath);
96
119
  const stats = statSync(fullPath);
120
+ if (!stats.isFile()) {
121
+ throw new Error(`Indexed path must be a regular file: ${relativePath}`);
122
+ }
97
123
  return {
98
124
  path: relativePath,
99
125
  sourceScope,
@@ -117,10 +143,7 @@ export function collectIndexedFileRecords(projectRoot, documents, sourceAnchors,
117
143
  const sourcePaths = new Set([...sourceAnchorCandidatePaths, ...sourceAnchors.map((anchor) => anchor.path)]);
118
144
  for (const anchorPath of [...sourcePaths].sort((left, right) => left.localeCompare(right))) {
119
145
  if (!records.has(anchorPath)) {
120
- const record = tryReadIndexedFileRecord(projectRoot, anchorPath, 'source_anchor');
121
- if (record) {
122
- records.set(anchorPath, record);
123
- }
146
+ records.set(anchorPath, readIndexedFileRecord(projectRoot, anchorPath, 'source_anchor'));
124
147
  }
125
148
  }
126
149
  if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
@@ -132,19 +155,25 @@ export function collectIndexedFileRecords(projectRoot, documents, sourceAnchors,
132
155
  return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
133
156
  }
134
157
  export function collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) {
135
- return listSourceAnchorFiles(projectRoot, {
158
+ const paths = listSourceAnchorFiles(projectRoot, {
136
159
  ...sourceConfig,
137
160
  excludeGeneratedOrVendor: true,
138
161
  });
162
+ for (const relativePath of paths) {
163
+ indexedProjectPath(projectRoot, relativePath);
164
+ }
165
+ return paths;
139
166
  }
140
167
  export function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig) {
141
168
  const records = new Map();
169
+ let sourceCandidatePaths = [];
142
170
  for (const relativePath of getExistingIndexablePaths(projectRoot)) {
143
171
  records.set(relativePath, readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
144
172
  }
145
173
  if (includeSource) {
146
174
  try {
147
- for (const sourcePath of collectSourceAnchorCandidatePaths(projectRoot, sourceConfig)) {
175
+ sourceCandidatePaths = collectSourceAnchorCandidatePaths(projectRoot, sourceConfig);
176
+ for (const sourcePath of sourceCandidatePaths) {
148
177
  if (!records.has(sourcePath)) {
149
178
  records.set(sourcePath, readIndexedFileMetadataRecord(projectRoot, sourcePath, 'source_anchor'));
150
179
  }
@@ -157,5 +186,8 @@ export function collectFastPreflightIndexedFileMetadataRecords(projectRoot, incl
157
186
  if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
158
187
  records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
159
188
  }
160
- return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
189
+ return {
190
+ records: [...records.values()].sort((left, right) => left.path.localeCompare(right.path)),
191
+ sourceCandidatePaths,
192
+ };
161
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.115.3",
3
+ "version": "2.115.5",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -62,7 +62,7 @@ translations = {}
62
62
  [documents."skills.index"]
63
63
  source = "locales/en/.mustflow/skills/INDEX.md"
64
64
  source_locale = "en"
65
- revision = 228
65
+ revision = 229
66
66
  translations = {}
67
67
 
68
68
  [documents."skill.adapter-boundary"]
@@ -1164,7 +1164,7 @@ translations = {}
1164
1164
  [documents."skill.release-publish-change"]
1165
1165
  source = "locales/en/.mustflow/skills/release-publish-change/SKILL.md"
1166
1166
  source_locale = "en"
1167
- revision = 2
1167
+ revision = 3
1168
1168
  translations = {}
1169
1169
 
1170
1170
  [documents."skill.security-privacy-review"]
@@ -2,7 +2,7 @@
2
2
  mustflow_doc: skills.index
3
3
  locale: en
4
4
  canonical: true
5
- revision: 228
5
+ revision: 229
6
6
  authority: router
7
7
  lifecycle: mustflow-owned
8
8
  ---
@@ -632,7 +632,7 @@ routes. Event routes stay inactive until their event occurs.
632
632
  | `README.md` is created, restructured, or substantially rewritten | `.mustflow/skills/readme-authoring/SKILL.md` | User request, existing README if any, repository evidence, nearest instructions, and command contracts | `README.md` and directly linked public docs | invented project claims, marketing drift, or loss of human-authored intent | `docs_validate_fast`, `mustflow_check` | Evidence-based README changes, preserved or deferred sections, verification notes |
633
633
  | README content is generated, reviewed, or audited for unsupported AI-generated claims, fake commands, invented environment variables, README-as-investment-deck overclaim, unshipped roadmap-as-current wording, fake or internal-demo usage examples, TODO/temporary/internal-state wording, executable install/run/test/build contracts, pasteable examples, public API examples, security exposure, performance, platform support, license, trademark, copyright, credit, attribution, roadmap, architecture, or file-tree explanations | `.mustflow/skills/readme-evidence-gate/SKILL.md` | README draft or sections, evidence ledger, repository files, package manager metadata, lockfiles, `bin`, `exports`, public exports, command sources, CLI parser/help/output evidence, config/env sources, support and badge evidence, tests, license and notice files, SPDX identifiers, source headers, package metadata, contributor and trademark evidence, CI matrices, Dockerfiles, benchmarks, maintained docs, screenshots, `.env.example`, code-block execution policy, clean-environment evidence, secret-scanning policy, and external or AI source text | README wording and directly synchronized README examples, docs links, placeholder examples, code-block labels, legal/credit wording, or screenshot guidance | hallucinated README contract, fake quick-start, invented env/API, subjective maintenance-debt prose, unsupported pasteable snippet, untested executable README contract, OS-shell mismatch, missing server readiness, unsupported security/performance/platform/license/trademark claim, mixed roadmap/current support, hidden internal demo prerequisite, unsupported badge or support claim, real-looking fake key, API key in URL query, internal URL, private IP, production path, pasteable production command, unsafe screenshot, or speculative file tree | `docs_validate_fast`, `test_related`, `test_release`, `mustflow_check` | README evidence ledger, claims kept/qualified/moved/removed, subjective phrases converted, overclaim/support/badge/failure-path checks, executable-command and clean-environment checks, pasteable contract checks, legal/SPDX/NOTICE/trademark/credit checks, security exposure checks, unsupported claims found, verification notes, and remaining README evidence risk |
634
634
  | Release notes, changelog entries, public change summaries, release preparation copy, or package release wording are drafted or revised | `.mustflow/skills/release-notes-authoring/SKILL.md` | User-provided change summary, current diff summary, release audience, public surfaces, version source, and command contract entries | Release notes, changelog entries, release preparation notes, and directly synchronized docs or package metadata | invented release history, inflated public claims, internal noise, stale version or migration notes, or unverified release evidence | `changes_status`, `changes_diff_summary`, `docs_validate_fast`, `test_release`, `mustflow_check` | Release audience, categorized notes, excluded internal changes, version or migration checks, verification, skipped release-history checks, and remaining release-note risk |
635
- | Release publishing, package registry publication, remote release channels, Git tags, GitHub Releases, release assets, npm, PyPI, crates.io, Go modules, Docker images, Homebrew formulae or casks, app updater metadata, version bump decisions, artifact inspection, post-publish smoke tests, rollback or yanking plans, or user installation paths are created, changed, reviewed, or reported | `.mustflow/skills/release-publish-change/SKILL.md` | Release target, version, channel, package name, module path, image name, tag, artifact names, expected assets, public contract source, artifact inspection method, remote publication surface, recovery model, and command contract entries | Version metadata, release workflows, package manifests, artifact manifests, changelog or release-preparation docs, package tests, install-smoke expectations, release validation tests, and installed-template metadata | local-only release claim, wrong version bump, stale artifact, registry overwrite assumption, missing asset, bad checksum or signature, moved Go tag, unverified Docker digest, updater metadata breakage, missing user-path smoke test, or false rollback claim | `changes_status`, `changes_diff_summary`, `lint`, `build`, `test_related`, `test`, `docs_validate_fast`, `test_release`, `mustflow_check` | Release target, version and channel, public API classification, artifact inspection evidence, remote publication state, user-path smoke result, synchronized surfaces, recovery classification, verification, and remaining release-publish risk |
635
+ | Release publishing, package registry publication, remote release channels, Git tags, GitHub Releases, release assets, npm, PyPI, crates.io, Go modules, Docker images, Homebrew formulae or casks, app updater metadata, version bump decisions, artifact inspection, post-publish smoke tests, rollback or yanking plans, or user installation paths are created, changed, reviewed, or reported | `.mustflow/skills/release-publish-change/SKILL.md` | Release target, exact immutable version, channel, package name, module path, image name, tag, artifact names, expected assets, public contract source, artifact inspection method, remote publication surface, consumer environment, public entrypoints, recovery model, and command contract entries | Version metadata, release workflows, package manifests, artifact manifests, changelog or release-preparation docs, package tests, install-smoke expectations, release validation tests, and installed-template metadata | local-only release claim, wrong version bump, stale artifact, cache-only or internal-entrypoint-only smoke, registry overwrite assumption, missing asset, bad checksum or signature, moved Go tag, unverified Docker digest, updater metadata breakage, missing user-path smoke test, or false rollback claim | `changes_status`, `changes_diff_summary`, `lint`, `build`, `test_related`, `test`, `docs_validate_fast`, `test_release`, `mustflow_check` | Release target, exact version and channel, public API classification, artifact inspection evidence, remote publication state, public-entrypoint user-path smoke and environment evidence, synchronized surfaces, recovery classification, verification, and remaining release-publish risk |
636
636
  | Search-friendly ad-supported articles, blog posts, guides, reviews, comparisons, FAQs, or evergreen content are planned, written, edited, reviewed, or reported | `.mustflow/skills/search-ad-content-authoring/SKILL.md` | Search intent, reader task, content type, source freshness needs, monetization constraints, article draft or outline, and command contract entries | Article outlines, headings, paragraphs, tables, lists, FAQs, images, links, disclosures, content docs, templates, tests, and reports | keyword stuffing, thin filler, misleading ad adjacency, stale policy or ranking claims, unsupported revenue claims, accessibility or layout instability, or copied competitor content | `changes_status`, `changes_diff_summary`, `docs_validate_fast`, `test_release`, `mustflow_check` | Search intent, outline shape, content structure checks, source freshness, ad layout and trust checks, omitted or verified claims, verification, and remaining content risk |
637
637
  | Documentation review queue entries or selected docs need prose cleanup for LLM-like wording, AI-slop signals, low-specificity boilerplate, literal translation, unnatural tone, Korean technical translationese, or domain-term drift | `.mustflow/skills/docs-prose-review/SKILL.md` | Review queue entry or selected document path, review comment if present, target language, audience or genre, domain terminology, reviewer metadata | Selected documentation file and review ledger entry | meaning drift, fake authorship attribution, invented evidence, over-editing, or stale queue state | `docs_validate`, `mustflow_check` | Prose issues fixed, preserved technical meaning, recorded review status, verification notes |
638
638
  | Korean or English prose is supplied to extract reusable elegant wording candidates, store selected modular phrase fragments, polish prose with a curated phrase bank, or improve wording for report-style answers, final reports, GitHub issue bodies, pull request descriptions, review replies, maintainer-facing comments, release or update notes, documentation prose, summaries, or explanatory writing after the facts are established | `.mustflow/skills/writing-elegance/SKILL.md` | Source text, mode, target register, target surface, user keep or reject choices, current phrase bank when storing or applying expressions, owning workflow skill when evidence or repository policy matters, and command contract entries when files change | Candidate tables, selected phrase-bank entries, polished report or GitHub wording, `references/phrase-bank.md`, synchronized template copy, route metadata, template manifest, i18n metadata, and directly tied tests | over-specific sentence capture, proper-name leakage, private detail storage, ornamental wording, technical meaning drift, docs-prose-review overlap, GitHub quality-gate overlap, release-note authority drift, completion-evidence drift, phrase-bank bloat, or skipped template sync | `changes_status`, `changes_diff_summary`, `docs_validate_fast`, `test_release`, `mustflow_check` | Mode, target surface, candidate table or stored entries, entries kept or rejected, phrase-bank updates or polish boundary, owning skill applied or deferred, template sync, verification, and remaining style or specificity risk |
@@ -2,7 +2,7 @@
2
2
  mustflow_doc: skill.release-publish-change
3
3
  locale: en
4
4
  canonical: true
5
- revision: 2
5
+ revision: 3
6
6
  lifecycle: mustflow-owned
7
7
  authority: procedure
8
8
  name: release-publish-change
@@ -60,7 +60,7 @@ The release is not done when tests pass locally, a version string changes, or a
60
60
  and whether each belongs to publication, branch CI, tag CI, release asset generation, or another
61
61
  independent verification path.
62
62
  - Recovery model: unpublish, yank, deprecate, republish with new version, move channel pointer, revoke asset, restore from backup, or forward fix.
63
- - Configured command intents for build, package inspection, release verification, docs validation, and user installation or updater smoke test. If no such intent exists, report the missing intent instead of inventing a raw command.
63
+ - Configured command intents for build, package inspection, release verification, docs validation, and user installation or updater smoke test. For post-publish checks, also identify the exact immutable version, intended registry or channel, consumer environment, public entrypoints, and runtime or platform evidence. If no such intent exists, report the missing intent instead of inventing a raw command.
64
64
 
65
65
  <!-- mustflow-section: preconditions -->
66
66
  ## Preconditions
@@ -101,6 +101,14 @@ The release is not done when tests pass locally, a version string changes, or a
101
101
  - GitHub Releases depend on Git tags, but release assets, checksums, signatures, and release body are separate evidence surfaces.
102
102
  - App updater channels depend on metadata and signature state, not only uploaded installers.
103
103
  5. For npm-style package publication, verify package metadata, packed file list, entrypoints, bin links, README, LICENSE, access, provenance or trusted publisher setup, registry target, and exact published version behavior through configured intents.
104
+ - Keep pre-publish packed-artifact checks separate from post-publish registry checks. A local tarball or workspace install does not prove that the immutable registry version users receive is installable.
105
+ - Run the post-publish smoke in a fresh consumer root outside the source checkout. Install the exact immutable name and version from the intended registry without workspace links or local archive fallbacks.
106
+ - Use a fresh or isolated package-manager cache strategy and prefer current registry metadata. A cache-only success is not independent remote-channel evidence.
107
+ - Disable lifecycle scripts by default so a package cannot repair missing build output during installation. If lifecycle scripts are an intentional public contract, test that behavior separately with the required sandbox and approval boundary.
108
+ - Execute the public command shims, exported entrypoints, or documented import path that users invoke. Calling an internal module file directly does not prove that package-manager shims, aliases, permissions, or entry metadata work.
109
+ - Exercise every documented command alias that is part of the release contract, then run one minimal documented workflow that crosses initialization or configuration validation when applicable.
110
+ - Retry only bounded transient registry conditions such as propagation delay, throttling, or transport reset. Do not retry authentication, package metadata, entrypoint, or runtime failures into a false pass.
111
+ - Keep temporary state owned and removable, preserve the primary failure if cleanup also fails, and report the exact package version, registry, package-manager version, runtime, operating system, and public entrypoints exercised.
104
112
  6. For PyPI-style publication, verify source distribution, wheel contents, metadata, Python version constraints, entrypoints, README rendering, filename uniqueness, and install smoke path through configured intents.
105
113
  7. For crates.io-style publication, verify manifest metadata, include and exclude rules, packaged file list, feature combinations, docs expectations, and yank-forward-fix policy.
106
114
  8. For Go modules, treat the Git tag as the release. Verify module path, semantic tag, major-version path rules, tag target commit, proxy/cache implications, and module consumer smoke path. Do not move or delete tags as a casual recovery shortcut.
@@ -137,7 +145,7 @@ The release is not done when tests pass locally, a version string changes, or a
137
145
  - Remote publication status is classified as not started, prepared, published, verified, failed, yanked, deprecated, superseded, or unknown.
138
146
  - Branch, tag, and publication workflow checks are classified separately as green, failing, pending,
139
147
  skipped, not applicable, or unknown.
140
- - User installation, pull, download, or updater smoke test status is known or explicitly reported as skipped.
148
+ - Independent consumer-root installation, pull, download, or updater smoke status through public entrypoints is known with environment evidence, or explicitly reported as skipped.
141
149
  - Recovery plan matches the channel's actual permanence and rules.
142
150
 
143
151
  <!-- mustflow-section: verification -->
@@ -155,7 +163,7 @@ Use configured oneshot command intents when available:
155
163
  - `test_release`
156
164
  - `mustflow_check`
157
165
 
158
- Prefer configured release, package-inspection, artifact-inspection, install-smoke, updater-smoke, checksum, signature, provenance, or registry-verification intents when the command contract exposes them.
166
+ Prefer configured release, package-inspection, artifact-inspection, install-smoke, updater-smoke, checksum, signature, provenance, or registry-verification intents when the command contract exposes them. A local packed-artifact check may support pre-publish confidence, but it does not satisfy an independent post-publish installation claim.
159
167
 
160
168
  Do not infer package manager, registry, Docker, Git, Homebrew, or updater commands from project files. If the needed intent is missing, report the missing command contract instead of writing a raw command into the skill or final release procedure.
161
169
 
@@ -180,7 +188,7 @@ Do not infer package manager, registry, Docker, Git, Homebrew, or updater comman
180
188
  - Artifact contents inspected
181
189
  - Remote publication state
182
190
  - Branch, tag, and publication check-suite state
183
- - User installation, download, pull, or updater smoke path result
191
+ - User installation, download, pull, or updater smoke result, including exact immutable version, registry or channel, public entrypoints, package manager, runtime, operating system, retry outcome, and skipped platforms
184
192
  - Synchronized version, docs, manifest, workflow, and test surfaces
185
193
  - Recovery or rollback classification
186
194
  - Command intents run
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.115.3"
3
+ version = "2.115.5"
4
4
  description = "Minimal workflow for LLM agents to read, edit, and verify their work in a repository."
5
5
  common_root = "common"
6
6
  locales_root = "locales"