mustflow 2.32.0 → 2.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli/commands/api/actions.js +55 -0
  2. package/dist/cli/commands/api/report-runner.js +62 -0
  3. package/dist/cli/commands/api/serve.js +149 -0
  4. package/dist/cli/commands/api/workspace-recommendations.js +13 -0
  5. package/dist/cli/commands/api.js +15 -275
  6. package/dist/cli/lib/local-index/search-read-model.js +44 -7
  7. package/dist/cli/lib/validation/frontmatter.js +75 -0
  8. package/dist/cli/lib/validation/index.js +4 -86
  9. package/dist/cli/lib/validation/safe-read.js +13 -0
  10. package/dist/core/active-run-locks.js +110 -10
  11. package/dist/core/run-performance-history.js +14 -1
  12. package/dist/core/validation-ratchet.js +1 -1
  13. package/package.json +1 -1
  14. package/templates/default/i18n.toml +55 -19
  15. package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +15 -7
  16. package/templates/default/locales/en/.mustflow/skills/INDEX.md +20 -8
  17. package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +9 -2
  18. package/templates/default/locales/en/.mustflow/skills/ai-generated-code-hardening/SKILL.md +249 -0
  19. package/templates/default/locales/en/.mustflow/skills/api-contract-change/SKILL.md +16 -11
  20. package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +11 -4
  21. package/templates/default/locales/en/.mustflow/skills/backend-reliability-change/SKILL.md +289 -0
  22. package/templates/default/locales/en/.mustflow/skills/css-code-change/SKILL.md +24 -14
  23. package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +18 -7
  24. package/templates/default/locales/en/.mustflow/skills/frontend-render-stability/SKILL.md +144 -0
  25. package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +70 -18
  26. package/templates/default/locales/en/.mustflow/skills/html-code-change/SKILL.md +20 -13
  27. package/templates/default/locales/en/.mustflow/skills/http-delivery-streaming/SKILL.md +205 -0
  28. package/templates/default/locales/en/.mustflow/skills/performance-budget-check/SKILL.md +9 -7
  29. package/templates/default/locales/en/.mustflow/skills/proactive-risk-surfacing/SKILL.md +198 -0
  30. package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +27 -11
  31. package/templates/default/locales/en/.mustflow/skills/routes.toml +37 -1
  32. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +41 -17
  33. package/templates/default/locales/en/.mustflow/skills/service-boundary-architecture/SKILL.md +167 -0
  34. package/templates/default/locales/en/.mustflow/skills/tailwind-code-change/SKILL.md +37 -23
  35. package/templates/default/locales/en/.mustflow/skills/tauri-code-change/SKILL.md +27 -10
  36. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +22 -4
  37. package/templates/default/locales/en/.mustflow/skills/unocss-code-change/SKILL.md +34 -15
  38. package/templates/default/locales/en/.mustflow/skills/version-freshness-check/SKILL.md +29 -5
  39. package/templates/default/locales/en/AGENTS.md +3 -2
  40. package/templates/default/locales/ko/.mustflow/docs/agent-workflow.md +13 -8
  41. package/templates/default/locales/ko/AGENTS.md +2 -2
  42. package/templates/default/manifest.toml +37 -1
@@ -0,0 +1,55 @@
1
+ export const API_REPORT_ACTIONS = [
2
+ {
3
+ action: 'workspace-summary',
4
+ requiresChanged: false,
5
+ helpKey: 'api.help.action.workspaceSummary',
6
+ example: 'mf api workspace-summary --json',
7
+ },
8
+ {
9
+ action: 'command-catalog',
10
+ requiresChanged: false,
11
+ helpKey: 'api.help.action.commandCatalog',
12
+ example: 'mf api command-catalog --json',
13
+ },
14
+ {
15
+ action: 'verification-plan',
16
+ requiresChanged: true,
17
+ helpKey: 'api.help.action.verificationPlan',
18
+ example: 'mf api verification-plan --changed --json',
19
+ },
20
+ {
21
+ action: 'latest-evidence',
22
+ requiresChanged: false,
23
+ helpKey: 'api.help.action.latestEvidence',
24
+ example: 'mf api latest-evidence --json',
25
+ },
26
+ {
27
+ action: 'diff-risk',
28
+ requiresChanged: true,
29
+ helpKey: 'api.help.action.diffRisk',
30
+ example: 'mf api diff-risk --changed --json',
31
+ },
32
+ {
33
+ action: 'health',
34
+ requiresChanged: false,
35
+ helpKey: 'api.help.action.health',
36
+ example: 'mf api health --json',
37
+ },
38
+ {
39
+ action: 'locks',
40
+ requiresChanged: false,
41
+ helpKey: 'api.help.action.locks',
42
+ example: 'mf api locks --json',
43
+ },
44
+ ];
45
+ const API_REPORT_ACTION_NAMES = new Set(API_REPORT_ACTIONS.map((spec) => spec.action));
46
+ export function isApiReportAction(value) {
47
+ return API_REPORT_ACTION_NAMES.has(value);
48
+ }
49
+ export function apiReportActionSpec(action) {
50
+ const spec = API_REPORT_ACTIONS.find((candidate) => candidate.action === action);
51
+ if (!spec) {
52
+ throw new Error(`Unknown API report action: ${action}`);
53
+ }
54
+ return spec;
55
+ }
@@ -0,0 +1,62 @@
1
+ import { printUsageError } from '../../lib/cli-output.js';
2
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../../lib/option-parser.js';
3
+ import { t } from '../../lib/i18n.js';
4
+ const API_JSON_ONLY_OPTIONS = [
5
+ { name: '--json', kind: 'boolean' },
6
+ { name: '--help', kind: 'boolean', aliases: ['-h'] },
7
+ ];
8
+ const API_CHANGED_JSON_OPTIONS = [
9
+ { name: '--changed', kind: 'boolean' },
10
+ { name: '--json', kind: 'boolean' },
11
+ { name: '--help', kind: 'boolean', aliases: ['-h'] },
12
+ ];
13
+ function validateJsonOnlyAction(action, args, reporter, lang, runtime) {
14
+ const parsed = parseCliOptions(args, API_JSON_ONLY_OPTIONS);
15
+ if (hasParsedCliOption(parsed, '--help')) {
16
+ reporter.stdout(runtime.getHelp(lang));
17
+ return false;
18
+ }
19
+ if (parsed.error) {
20
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf api --help', runtime.getHelp(lang), lang);
21
+ return false;
22
+ }
23
+ if (!hasParsedCliOption(parsed, '--json')) {
24
+ printUsageError(reporter, t(lang, 'api.error.actionRequiresJson', { action }), 'mf api --help', runtime.getHelp(lang), lang);
25
+ return false;
26
+ }
27
+ return true;
28
+ }
29
+ function validateChangedJsonAction(action, args, reporter, lang, runtime) {
30
+ const parsed = parseCliOptions(args, API_CHANGED_JSON_OPTIONS);
31
+ if (hasParsedCliOption(parsed, '--help')) {
32
+ reporter.stdout(runtime.getHelp(lang));
33
+ return false;
34
+ }
35
+ if (parsed.error) {
36
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf api --help', runtime.getHelp(lang), lang);
37
+ return false;
38
+ }
39
+ if (!hasParsedCliOption(parsed, '--json')) {
40
+ printUsageError(reporter, t(lang, 'api.error.actionRequiresJson', { action }), 'mf api --help', runtime.getHelp(lang), lang);
41
+ return false;
42
+ }
43
+ if (!hasParsedCliOption(parsed, '--changed')) {
44
+ printUsageError(reporter, t(lang, 'api.error.actionRequiresChanged', { action }), 'mf api --help', runtime.getHelp(lang), lang);
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ export function runJsonOnlyApiReport(action, args, reporter, lang, runtime) {
50
+ if (!validateJsonOnlyAction(action, args, reporter, lang, runtime)) {
51
+ return hasCliOptionToken(args, '--help', ['-h']) ? 0 : 1;
52
+ }
53
+ runtime.writeReport(action, reporter);
54
+ return 0;
55
+ }
56
+ export function runChangedApiReport(action, args, reporter, lang, runtime) {
57
+ if (!validateChangedJsonAction(action, args, reporter, lang, runtime)) {
58
+ return hasCliOptionToken(args, '--help', ['-h']) ? 0 : 1;
59
+ }
60
+ runtime.writeReport(action, reporter);
61
+ return 0;
62
+ }
@@ -0,0 +1,149 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { apiReportActionSpec, isApiReportAction } from './actions.js';
3
+ import { printUsageError } from '../../lib/cli-output.js';
4
+ import { formatCliOptionParseError, hasParsedCliOption, parseCliOptions, } from '../../lib/option-parser.js';
5
+ import { isRecord } from '../../lib/command-contract.js';
6
+ import { t } from '../../lib/i18n.js';
7
+ const API_SERVE_SCHEMA_VERSION = '1';
8
+ const API_SERVE_OPTIONS = [
9
+ { name: '--stdio', kind: 'boolean' },
10
+ { name: '--help', kind: 'boolean', aliases: ['-h'] },
11
+ ];
12
+ function createApiServePolicy() {
13
+ return {
14
+ mode: 'read_only',
15
+ executes_commands: false,
16
+ direct_commands_allowed: false,
17
+ raw_output_included: false,
18
+ hidden_reasoning_included: false,
19
+ };
20
+ }
21
+ function createApiServeError(id, code, message) {
22
+ return {
23
+ schema_version: API_SERVE_SCHEMA_VERSION,
24
+ command: 'api serve',
25
+ transport: 'stdio',
26
+ id,
27
+ ok: false,
28
+ policy: createApiServePolicy(),
29
+ error: {
30
+ code,
31
+ message,
32
+ },
33
+ };
34
+ }
35
+ function createApiServeSuccess(id, result) {
36
+ return {
37
+ schema_version: API_SERVE_SCHEMA_VERSION,
38
+ command: 'api serve',
39
+ transport: 'stdio',
40
+ id,
41
+ ok: true,
42
+ policy: createApiServePolicy(),
43
+ result,
44
+ };
45
+ }
46
+ function readApiServeId(request) {
47
+ if (!isRecord(request) || !Object.hasOwn(request, 'id')) {
48
+ return null;
49
+ }
50
+ const value = request.id;
51
+ if (typeof value === 'string' || typeof value === 'number' || value === null) {
52
+ return value;
53
+ }
54
+ return null;
55
+ }
56
+ function parseApiServeRequestLine(line) {
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(line);
60
+ }
61
+ catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ return {
64
+ request: null,
65
+ error: createApiServeError(null, 'invalid_json', `Invalid JSON request: ${message}`),
66
+ };
67
+ }
68
+ const id = readApiServeId(parsed);
69
+ if (!isRecord(parsed)) {
70
+ return {
71
+ request: null,
72
+ error: createApiServeError(id, 'invalid_request', 'Request must be a JSON object.'),
73
+ };
74
+ }
75
+ return {
76
+ request: {
77
+ id,
78
+ action: parsed.action,
79
+ changed: parsed.changed,
80
+ },
81
+ error: null,
82
+ };
83
+ }
84
+ function createApiServeResponse(request, runtime) {
85
+ const id = typeof request.id === 'string' || typeof request.id === 'number' || request.id === null ? request.id : null;
86
+ if (typeof request.action !== 'string') {
87
+ return createApiServeError(id, 'invalid_request', 'Request field "action" must be a string.');
88
+ }
89
+ if (!isApiReportAction(request.action)) {
90
+ return createApiServeError(id, 'unknown_action', `Unknown api action: ${request.action}`);
91
+ }
92
+ const spec = apiReportActionSpec(request.action);
93
+ if (spec.requiresChanged && request.changed !== true) {
94
+ return createApiServeError(id, 'action_requires_changed', `${request.action} requires changed: true.`);
95
+ }
96
+ if (!spec.requiresChanged && request.changed === true) {
97
+ return createApiServeError(id, 'action_does_not_accept_changed', `${request.action} does not accept changed: true.`);
98
+ }
99
+ try {
100
+ return createApiServeSuccess(id, runtime.createReport(request.action));
101
+ }
102
+ catch (error) {
103
+ return createApiServeError(id, 'report_unavailable', error instanceof Error ? error.message : String(error));
104
+ }
105
+ }
106
+ function writeApiServeResponse(response, reporter) {
107
+ const line = `${JSON.stringify(response)}\n`;
108
+ if (reporter.writeStdout) {
109
+ reporter.writeStdout(line);
110
+ return;
111
+ }
112
+ reporter.stdout(line.trimEnd());
113
+ }
114
+ export async function runApiServe(args, reporter, lang, runtime) {
115
+ const parsed = parseCliOptions(args, API_SERVE_OPTIONS);
116
+ if (hasParsedCliOption(parsed, '--help')) {
117
+ reporter.stdout(runtime.getHelp(lang));
118
+ return 0;
119
+ }
120
+ if (parsed.error) {
121
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf api --help', runtime.getHelp(lang), lang);
122
+ return 1;
123
+ }
124
+ if (!hasParsedCliOption(parsed, '--stdio')) {
125
+ printUsageError(reporter, t(lang, 'api.error.serveRequiresStdio'), 'mf api --help', runtime.getHelp(lang), lang);
126
+ return 1;
127
+ }
128
+ const input = createInterface({
129
+ input: process.stdin,
130
+ crlfDelay: Infinity,
131
+ });
132
+ for await (const rawLine of input) {
133
+ const line = rawLine.trim();
134
+ if (line.length === 0) {
135
+ continue;
136
+ }
137
+ const parsed = parseApiServeRequestLine(line);
138
+ if (parsed.error) {
139
+ writeApiServeResponse(parsed.error, reporter);
140
+ continue;
141
+ }
142
+ if (!parsed.request) {
143
+ writeApiServeResponse(createApiServeError(null, 'invalid_request', 'Request must be a JSON object.'), reporter);
144
+ continue;
145
+ }
146
+ writeApiServeResponse(createApiServeResponse(parsed.request, runtime), reporter);
147
+ }
148
+ return 0;
149
+ }
@@ -0,0 +1,13 @@
1
+ export function createRecommendedNextCommands(output) {
2
+ if (!output.installed) {
3
+ return ['mf init --dry-run', 'mf init --yes'];
4
+ }
5
+ if (!output.check.ok) {
6
+ return ['mf check', 'mf status --json', 'mf update --dry-run'];
7
+ }
8
+ const commands = ['mf context --json', 'mf doctor --json', 'mf check --strict'];
9
+ if (output.git_state.status === 'available' && output.git_state.changed_file_count !== null && output.git_state.changed_file_count > 0) {
10
+ commands.push('mf classify --changed --json', 'mf verify --changed --plan-only --json');
11
+ }
12
+ return commands;
13
+ }
@@ -1,13 +1,16 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { createInterface } from 'node:readline';
4
3
  import { createClassifyOutput } from './classify.js';
4
+ import { API_REPORT_ACTIONS, apiReportActionSpec, isApiReportAction, } from './api/actions.js';
5
+ import { runChangedApiReport, runJsonOnlyApiReport } from './api/report-runner.js';
6
+ import { runApiServe } from './api/serve.js';
7
+ import { createRecommendedNextCommands } from './api/workspace-recommendations.js';
5
8
  import { listActiveRunLocks } from '../../core/active-run-locks.js';
6
9
  import { createChangeVerificationReport, } from '../../core/change-verification.js';
7
10
  import { readUtf8FileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
8
11
  import { createVerificationPlanId } from '../../core/verification-plan-id.js';
9
12
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
10
- import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
13
+ import { hasCliOptionToken } from '../lib/option-parser.js';
11
14
  import { getAgentContext } from '../lib/agent-context.js';
12
15
  import { isRecord, readCommandContract, readPositiveInteger, readString, readStringArray, } from '../lib/command-contract.js';
13
16
  import { readGitChangedFiles } from '../lib/git-changes.js';
@@ -22,79 +25,10 @@ const API_LATEST_EVIDENCE_SCHEMA_VERSION = '1';
22
25
  const API_DIFF_RISK_SCHEMA_VERSION = '1';
23
26
  const API_HEALTH_SCHEMA_VERSION = '1';
24
27
  const API_LOCKS_SCHEMA_VERSION = '1';
25
- const API_SERVE_SCHEMA_VERSION = '1';
26
28
  const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
27
29
  const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
28
30
  const LOCKS_RELATIVE_PATH = '.mustflow/state/locks';
29
31
  const MUSTFLOW_JSON_MAX_BYTES = 1024 * 1024;
30
- const API_JSON_ONLY_OPTIONS = [
31
- { name: '--json', kind: 'boolean' },
32
- { name: '--help', kind: 'boolean', aliases: ['-h'] },
33
- ];
34
- const API_CHANGED_JSON_OPTIONS = [
35
- { name: '--changed', kind: 'boolean' },
36
- { name: '--json', kind: 'boolean' },
37
- { name: '--help', kind: 'boolean', aliases: ['-h'] },
38
- ];
39
- const API_SERVE_OPTIONS = [
40
- { name: '--stdio', kind: 'boolean' },
41
- { name: '--help', kind: 'boolean', aliases: ['-h'] },
42
- ];
43
- const API_REPORT_ACTIONS = [
44
- {
45
- action: 'workspace-summary',
46
- requiresChanged: false,
47
- helpKey: 'api.help.action.workspaceSummary',
48
- example: 'mf api workspace-summary --json',
49
- },
50
- {
51
- action: 'command-catalog',
52
- requiresChanged: false,
53
- helpKey: 'api.help.action.commandCatalog',
54
- example: 'mf api command-catalog --json',
55
- },
56
- {
57
- action: 'verification-plan',
58
- requiresChanged: true,
59
- helpKey: 'api.help.action.verificationPlan',
60
- example: 'mf api verification-plan --changed --json',
61
- },
62
- {
63
- action: 'latest-evidence',
64
- requiresChanged: false,
65
- helpKey: 'api.help.action.latestEvidence',
66
- example: 'mf api latest-evidence --json',
67
- },
68
- {
69
- action: 'diff-risk',
70
- requiresChanged: true,
71
- helpKey: 'api.help.action.diffRisk',
72
- example: 'mf api diff-risk --changed --json',
73
- },
74
- {
75
- action: 'health',
76
- requiresChanged: false,
77
- helpKey: 'api.help.action.health',
78
- example: 'mf api health --json',
79
- },
80
- {
81
- action: 'locks',
82
- requiresChanged: false,
83
- helpKey: 'api.help.action.locks',
84
- example: 'mf api locks --json',
85
- },
86
- ];
87
- const API_REPORT_ACTION_NAMES = new Set(API_REPORT_ACTIONS.map((spec) => spec.action));
88
- function isApiReportAction(value) {
89
- return API_REPORT_ACTION_NAMES.has(value);
90
- }
91
- function apiReportActionSpec(action) {
92
- const spec = API_REPORT_ACTIONS.find((candidate) => candidate.action === action);
93
- if (!spec) {
94
- throw new Error(`Unknown API report action: ${action}`);
95
- }
96
- return spec;
97
- }
98
32
  export function getApiHelp(lang = 'en') {
99
33
  return renderHelp({
100
34
  usage: 'mf api <action> [options]',
@@ -194,19 +128,6 @@ function createRecommendedReadSurfaces(context) {
194
128
  ];
195
129
  return [...new Set(surfaces)];
196
130
  }
197
- function createRecommendedNextCommands(output) {
198
- if (!output.installed) {
199
- return ['mf init --dry-run', 'mf init --yes'];
200
- }
201
- if (!output.check.ok) {
202
- return ['mf check', 'mf status --json', 'mf update --dry-run'];
203
- }
204
- const commands = ['mf context --json', 'mf doctor --json', 'mf check --strict'];
205
- if (output.git_state.status === 'available' && output.git_state.changed_file_count !== null && output.git_state.changed_file_count > 0) {
206
- commands.push('mf classify --changed --json', 'mf verify --changed --plan-only --json');
207
- }
208
- return commands;
209
- }
210
131
  function isSafeIntentName(value) {
211
132
  return /^[A-Za-z0-9_-]+$/u.test(value);
212
133
  }
@@ -1045,194 +966,6 @@ function createApiReport(action) {
1045
966
  function writeApiReport(action, reporter) {
1046
967
  reporter.stdout(JSON.stringify(createApiReport(action), null, 2));
1047
968
  }
1048
- function createApiServePolicy() {
1049
- return {
1050
- mode: 'read_only',
1051
- executes_commands: false,
1052
- direct_commands_allowed: false,
1053
- raw_output_included: false,
1054
- hidden_reasoning_included: false,
1055
- };
1056
- }
1057
- function createApiServeError(id, code, message) {
1058
- return {
1059
- schema_version: API_SERVE_SCHEMA_VERSION,
1060
- command: 'api serve',
1061
- transport: 'stdio',
1062
- id,
1063
- ok: false,
1064
- policy: createApiServePolicy(),
1065
- error: {
1066
- code,
1067
- message,
1068
- },
1069
- };
1070
- }
1071
- function createApiServeSuccess(id, result) {
1072
- return {
1073
- schema_version: API_SERVE_SCHEMA_VERSION,
1074
- command: 'api serve',
1075
- transport: 'stdio',
1076
- id,
1077
- ok: true,
1078
- policy: createApiServePolicy(),
1079
- result,
1080
- };
1081
- }
1082
- function readApiServeId(request) {
1083
- if (!isRecord(request) || !Object.hasOwn(request, 'id')) {
1084
- return null;
1085
- }
1086
- const value = request.id;
1087
- if (typeof value === 'string' || typeof value === 'number' || value === null) {
1088
- return value;
1089
- }
1090
- return null;
1091
- }
1092
- function parseApiServeRequestLine(line) {
1093
- let parsed;
1094
- try {
1095
- parsed = JSON.parse(line);
1096
- }
1097
- catch (error) {
1098
- const message = error instanceof Error ? error.message : String(error);
1099
- return {
1100
- request: null,
1101
- error: createApiServeError(null, 'invalid_json', `Invalid JSON request: ${message}`),
1102
- };
1103
- }
1104
- const id = readApiServeId(parsed);
1105
- if (!isRecord(parsed)) {
1106
- return {
1107
- request: null,
1108
- error: createApiServeError(id, 'invalid_request', 'Request must be a JSON object.'),
1109
- };
1110
- }
1111
- return {
1112
- request: {
1113
- id,
1114
- action: parsed.action,
1115
- changed: parsed.changed,
1116
- },
1117
- error: null,
1118
- };
1119
- }
1120
- function createApiServeResponse(request) {
1121
- const id = typeof request.id === 'string' || typeof request.id === 'number' || request.id === null ? request.id : null;
1122
- if (typeof request.action !== 'string') {
1123
- return createApiServeError(id, 'invalid_request', 'Request field "action" must be a string.');
1124
- }
1125
- if (!isApiReportAction(request.action)) {
1126
- return createApiServeError(id, 'unknown_action', `Unknown api action: ${request.action}`);
1127
- }
1128
- const spec = apiReportActionSpec(request.action);
1129
- if (spec.requiresChanged && request.changed !== true) {
1130
- return createApiServeError(id, 'action_requires_changed', `${request.action} requires changed: true.`);
1131
- }
1132
- if (!spec.requiresChanged && request.changed === true) {
1133
- return createApiServeError(id, 'action_does_not_accept_changed', `${request.action} does not accept changed: true.`);
1134
- }
1135
- try {
1136
- return createApiServeSuccess(id, createApiReport(request.action));
1137
- }
1138
- catch (error) {
1139
- return createApiServeError(id, 'report_unavailable', error instanceof Error ? error.message : String(error));
1140
- }
1141
- }
1142
- function writeApiServeResponse(response, reporter) {
1143
- const line = `${JSON.stringify(response)}\n`;
1144
- if (reporter.writeStdout) {
1145
- reporter.writeStdout(line);
1146
- return;
1147
- }
1148
- reporter.stdout(line.trimEnd());
1149
- }
1150
- async function runApiServe(args, reporter, lang) {
1151
- const parsed = parseCliOptions(args, API_SERVE_OPTIONS);
1152
- if (hasParsedCliOption(parsed, '--help')) {
1153
- reporter.stdout(getApiHelp(lang));
1154
- return 0;
1155
- }
1156
- if (parsed.error) {
1157
- printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf api --help', getApiHelp(lang), lang);
1158
- return 1;
1159
- }
1160
- if (!hasParsedCliOption(parsed, '--stdio')) {
1161
- printUsageError(reporter, t(lang, 'api.error.serveRequiresStdio'), 'mf api --help', getApiHelp(lang), lang);
1162
- return 1;
1163
- }
1164
- const input = createInterface({
1165
- input: process.stdin,
1166
- crlfDelay: Infinity,
1167
- });
1168
- for await (const rawLine of input) {
1169
- const line = rawLine.trim();
1170
- if (line.length === 0) {
1171
- continue;
1172
- }
1173
- const parsed = parseApiServeRequestLine(line);
1174
- if (parsed.error) {
1175
- writeApiServeResponse(parsed.error, reporter);
1176
- continue;
1177
- }
1178
- if (!parsed.request) {
1179
- writeApiServeResponse(createApiServeError(null, 'invalid_request', 'Request must be a JSON object.'), reporter);
1180
- continue;
1181
- }
1182
- writeApiServeResponse(createApiServeResponse(parsed.request), reporter);
1183
- }
1184
- return 0;
1185
- }
1186
- function validateJsonOnlyAction(action, args, reporter, lang) {
1187
- const parsed = parseCliOptions(args, API_JSON_ONLY_OPTIONS);
1188
- if (hasParsedCliOption(parsed, '--help')) {
1189
- reporter.stdout(getApiHelp(lang));
1190
- return false;
1191
- }
1192
- if (parsed.error) {
1193
- printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf api --help', getApiHelp(lang), lang);
1194
- return false;
1195
- }
1196
- if (!hasParsedCliOption(parsed, '--json')) {
1197
- printUsageError(reporter, t(lang, 'api.error.actionRequiresJson', { action }), 'mf api --help', getApiHelp(lang), lang);
1198
- return false;
1199
- }
1200
- return true;
1201
- }
1202
- function validateChangedJsonAction(action, args, reporter, lang) {
1203
- const parsed = parseCliOptions(args, API_CHANGED_JSON_OPTIONS);
1204
- if (hasParsedCliOption(parsed, '--help')) {
1205
- reporter.stdout(getApiHelp(lang));
1206
- return false;
1207
- }
1208
- if (parsed.error) {
1209
- printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf api --help', getApiHelp(lang), lang);
1210
- return false;
1211
- }
1212
- if (!hasParsedCliOption(parsed, '--json')) {
1213
- printUsageError(reporter, t(lang, 'api.error.actionRequiresJson', { action }), 'mf api --help', getApiHelp(lang), lang);
1214
- return false;
1215
- }
1216
- if (!hasParsedCliOption(parsed, '--changed')) {
1217
- printUsageError(reporter, t(lang, 'api.error.actionRequiresChanged', { action }), 'mf api --help', getApiHelp(lang), lang);
1218
- return false;
1219
- }
1220
- return true;
1221
- }
1222
- function runJsonOnlyApiReport(action, args, reporter, lang) {
1223
- if (!validateJsonOnlyAction(action, args, reporter, lang)) {
1224
- return hasCliOptionToken(args, '--help', ['-h']) ? 0 : 1;
1225
- }
1226
- writeApiReport(action, reporter);
1227
- return 0;
1228
- }
1229
- function runChangedApiReport(action, args, reporter, lang) {
1230
- if (!validateChangedJsonAction(action, args, reporter, lang)) {
1231
- return hasCliOptionToken(args, '--help', ['-h']) ? 0 : 1;
1232
- }
1233
- writeApiReport(action, reporter);
1234
- return 0;
1235
- }
1236
969
  export function runApi(args, reporter, lang = 'en') {
1237
970
  if (hasCliOptionToken(args, '--help', ['-h'])) {
1238
971
  reporter.stdout(getApiHelp(lang));
@@ -1248,12 +981,19 @@ export function runApi(args, reporter, lang = 'en') {
1248
981
  return 1;
1249
982
  }
1250
983
  if (action === 'serve') {
1251
- return runApiServe(rest, reporter, lang);
984
+ return runApiServe(rest, reporter, lang, {
985
+ createReport: createApiReport,
986
+ getHelp: getApiHelp,
987
+ });
1252
988
  }
1253
989
  if (isApiReportAction(action)) {
990
+ const reportRuntime = {
991
+ getHelp: getApiHelp,
992
+ writeReport: writeApiReport,
993
+ };
1254
994
  return apiReportActionSpec(action).requiresChanged
1255
- ? runChangedApiReport(action, rest, reporter, lang)
1256
- : runJsonOnlyApiReport(action, rest, reporter, lang);
995
+ ? runChangedApiReport(action, rest, reporter, lang, reportRuntime)
996
+ : runJsonOnlyApiReport(action, rest, reporter, lang, reportRuntime);
1257
997
  }
1258
998
  printUsageError(reporter, t(lang, 'api.error.unknownAction', { action }), 'mf api --help', getApiHelp(lang), lang);
1259
999
  return 1;
@@ -107,11 +107,45 @@ function sourceAnchorAuthority() {
107
107
  can_instruct_agent: false,
108
108
  };
109
109
  }
110
- function getSectionHeadings(database, documentPath) {
111
- return queryRows(database, 'SELECT heading FROM sections WHERE document_path = ? ORDER BY ordinal', [documentPath]).map((row) => toSearchString(row.heading));
110
+ function createEmptyDocumentSearchMetadata() {
111
+ return { sectionHeadings: [], documentTerms: [] };
112
112
  }
113
- function getDocumentTerms(database, documentPath) {
114
- return queryRows(database, 'SELECT term FROM document_terms WHERE document_path = ? ORDER BY term', [documentPath]).map((row) => toSearchString(row.term));
113
+ function queryRowsByDocumentPath(database, sqlPrefix, documentPaths) {
114
+ const rows = [];
115
+ const chunkSize = 200;
116
+ for (let start = 0; start < documentPaths.length; start += chunkSize) {
117
+ const chunk = documentPaths.slice(start, start + chunkSize);
118
+ rows.push(...queryRows(database, `${sqlPrefix} WHERE document_path IN (${sqlPlaceholders(chunk)})`, chunk));
119
+ }
120
+ return rows;
121
+ }
122
+ function readDocumentSearchMetadata(database, documentPaths) {
123
+ const uniquePaths = [...new Set(documentPaths)].filter((documentPath) => documentPath.length > 0);
124
+ const metadata = new Map(uniquePaths.map((documentPath) => [documentPath, { sectionHeadings: [], documentTerms: [] }]));
125
+ if (uniquePaths.length === 0) {
126
+ return metadata;
127
+ }
128
+ for (const row of queryRowsByDocumentPath(database, 'SELECT document_path, ordinal, heading FROM sections', uniquePaths).sort((left, right) => {
129
+ const leftPath = toSearchString(left.document_path);
130
+ const rightPath = toSearchString(right.document_path);
131
+ return leftPath.localeCompare(rightPath) || Number(left.ordinal ?? 0) - Number(right.ordinal ?? 0);
132
+ })) {
133
+ const entry = metadata.get(toSearchString(row.document_path));
134
+ if (entry) {
135
+ entry.sectionHeadings.push(toSearchString(row.heading));
136
+ }
137
+ }
138
+ for (const row of queryRowsByDocumentPath(database, 'SELECT document_path, term FROM document_terms', uniquePaths).sort((left, right) => {
139
+ const leftPath = toSearchString(left.document_path);
140
+ const rightPath = toSearchString(right.document_path);
141
+ return leftPath.localeCompare(rightPath) || toSearchString(left.term).localeCompare(toSearchString(right.term));
142
+ })) {
143
+ const entry = metadata.get(toSearchString(row.document_path));
144
+ if (entry) {
145
+ entry.documentTerms.push(toSearchString(row.term));
146
+ }
147
+ }
148
+ return metadata;
115
149
  }
116
150
  function commandEffectFromRow(row) {
117
151
  return {
@@ -363,13 +397,16 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
363
397
  throw new Error(`Local mustflow index is stale: ${stalePaths.join(', ')}. Run \`mf index\` before searching. Refresh command: mf index`);
364
398
  }
365
399
  if (scope === 'workflow' || scope === 'all') {
366
- for (const row of queryCandidateRows(database, 'SELECT path, type, title, content_snippet FROM documents', 'path', indexedMatches.documents, indexedMatches)) {
400
+ const documentRows = queryCandidateRows(database, 'SELECT path, type, title, content_snippet FROM documents', 'path', indexedMatches.documents, indexedMatches);
401
+ const metadataByDocument = readDocumentSearchMetadata(database, documentRows.map((row) => toSearchString(row.path)));
402
+ for (const row of documentRows) {
367
403
  const pathValue = toSearchString(row.path);
368
404
  const typeValue = toSearchString(row.type);
369
405
  const title = toSearchString(row.title);
370
406
  const contentSnippet = toSearchString(row.content_snippet);
371
- const sectionHeadings = getSectionHeadings(database, pathValue);
372
- const documentTerms = getDocumentTerms(database, pathValue);
407
+ const metadata = metadataByDocument.get(pathValue) ?? createEmptyDocumentSearchMetadata();
408
+ const sectionHeadings = metadata.sectionHeadings;
409
+ const documentTerms = metadata.documentTerms;
373
410
  const primaryFields = [pathValue, title];
374
411
  const secondaryFields = [typeValue, contentSnippet, ...sectionHeadings, ...documentTerms];
375
412
  const fields = [...primaryFields, ...secondaryFields];