mustflow 2.70.0 → 2.74.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 (55) hide show
  1. package/README.md +20 -6
  2. package/dist/cli/commands/api.js +17 -0
  3. package/dist/cli/commands/check.js +38 -26
  4. package/dist/cli/commands/doctor.js +17 -5
  5. package/dist/cli/commands/evidence.js +71 -0
  6. package/dist/cli/commands/index.js +24 -9
  7. package/dist/cli/commands/map.js +20 -7
  8. package/dist/cli/commands/run.js +2 -1
  9. package/dist/cli/commands/script-pack.js +124 -0
  10. package/dist/cli/commands/update.js +52 -39
  11. package/dist/cli/commands/verify.js +50 -15
  12. package/dist/cli/commands/workspace.js +2 -0
  13. package/dist/cli/i18n/en.js +38 -0
  14. package/dist/cli/i18n/es.js +38 -0
  15. package/dist/cli/i18n/fr.js +38 -0
  16. package/dist/cli/i18n/hi.js +38 -0
  17. package/dist/cli/i18n/ko.js +38 -0
  18. package/dist/cli/i18n/zh.js +38 -0
  19. package/dist/cli/index.js +1 -0
  20. package/dist/cli/lib/active-command-lock.js +96 -0
  21. package/dist/cli/lib/agent-context.js +179 -10
  22. package/dist/cli/lib/command-registry.js +6 -0
  23. package/dist/cli/lib/dashboard-export.js +1 -0
  24. package/dist/cli/lib/script-pack-registry.js +27 -0
  25. package/dist/cli/script-packs/core-text-budget.js +241 -0
  26. package/dist/core/active-run-locks.js +7 -1
  27. package/dist/core/change-verification.js +10 -0
  28. package/dist/core/completion-verdict.js +14 -1
  29. package/dist/core/complexity-budget.js +206 -0
  30. package/dist/core/conflict-ledger.js +122 -0
  31. package/dist/core/failure-replay-capsule.js +213 -0
  32. package/dist/core/public-json-contracts.js +27 -0
  33. package/dist/core/risk-priced-evidence.js +213 -0
  34. package/dist/core/script-check-result.js +1 -0
  35. package/dist/core/text-budget.js +262 -0
  36. package/dist/core/verification-evidence.js +61 -13
  37. package/package.json +1 -1
  38. package/schemas/README.md +23 -11
  39. package/schemas/change-verification-report.schema.json +29 -0
  40. package/schemas/context-report.schema.json +58 -2
  41. package/schemas/dashboard-export.schema.json +42 -1
  42. package/schemas/diff-risk.schema.json +6 -0
  43. package/schemas/evidence-report.schema.json +45 -0
  44. package/schemas/latest-run-pointer.schema.json +50 -1
  45. package/schemas/script-pack-catalog.schema.json +68 -0
  46. package/schemas/text-budget-report.schema.json +131 -0
  47. package/schemas/verification-plan.schema.json +32 -0
  48. package/schemas/verify-report.schema.json +360 -1
  49. package/schemas/verify-run-manifest.schema.json +50 -1
  50. package/schemas/workspace-verification-plan.schema.json +32 -0
  51. package/templates/default/i18n.toml +2 -2
  52. package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -2
  53. package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +19 -2
  54. package/templates/default/locales/en/.mustflow/skills/routes.toml +1 -1
  55. package/templates/default/manifest.toml +1 -1
package/dist/cli/index.js CHANGED
@@ -54,6 +54,7 @@ function getTopLevelHelp(lang) {
54
54
  'mf verify --reason code_change',
55
55
  'mf line-endings check',
56
56
  'mf quality check',
57
+ 'mf script-pack run core/text-budget check README.md --max 5000',
57
58
  'mf version --check',
58
59
  'mf version-sources --json',
59
60
  ],
@@ -0,0 +1,96 @@
1
+ import { ACTIVE_RUN_LOCK_ID_ENV, acquireActiveRunLock, } from '../../core/active-run-locks.js';
2
+ import { renderCliError } from './cli-output.js';
3
+ import { t } from './i18n.js';
4
+ export const REPO_MAP_WRITE_EFFECTS = [
5
+ { type: 'write', mode: 'replace', path: 'REPO_MAP.md', concurrency: 'exclusive' },
6
+ ];
7
+ export const LOCAL_INDEX_WRITE_EFFECTS = [
8
+ {
9
+ type: 'write',
10
+ mode: 'replace',
11
+ path: '.mustflow/cache/**',
12
+ lock: 'local_index_cache',
13
+ concurrency: 'exclusive',
14
+ },
15
+ ];
16
+ export const MUSTFLOW_UPDATE_APPLY_EFFECTS = [
17
+ { type: 'write', mode: 'replace', path: 'AGENTS.md', concurrency: 'exclusive' },
18
+ { type: 'write', mode: 'replace', path: '.mustflow/config/manifest.lock.toml', concurrency: 'exclusive' },
19
+ { type: 'write', mode: 'replace', path: '.mustflow/config/commands.toml', concurrency: 'exclusive' },
20
+ { type: 'write', mode: 'replace', path: '.mustflow/config/mustflow.toml', concurrency: 'exclusive' },
21
+ { type: 'write', mode: 'replace', path: '.mustflow/config/preferences.toml', concurrency: 'exclusive' },
22
+ { type: 'write', mode: 'replace', path: '.mustflow/context/**', concurrency: 'exclusive' },
23
+ { type: 'write', mode: 'replace', path: '.mustflow/docs/**', concurrency: 'exclusive' },
24
+ { type: 'write', mode: 'replace', path: '.mustflow/skills/**', concurrency: 'exclusive' },
25
+ { type: 'write', mode: 'replace', path: '.mustflow/backups/**', concurrency: 'exclusive' },
26
+ ];
27
+ export const GENERATED_SURFACE_READ_EFFECTS = [
28
+ { type: 'read', mode: 'read', path: 'REPO_MAP.md', concurrency: 'shared' },
29
+ { type: 'read', mode: 'read', path: '.mustflow/config/manifest.lock.toml', concurrency: 'shared' },
30
+ {
31
+ type: 'read',
32
+ mode: 'read',
33
+ path: '.mustflow/cache/**',
34
+ lock: 'local_index_cache',
35
+ concurrency: 'shared',
36
+ },
37
+ ];
38
+ function effectToToml(effect) {
39
+ const output = {
40
+ type: effect.type,
41
+ };
42
+ if (effect.mode) {
43
+ output.mode = effect.mode;
44
+ }
45
+ if (effect.path) {
46
+ output.path = effect.path;
47
+ }
48
+ if (effect.paths) {
49
+ output.paths = [...effect.paths];
50
+ }
51
+ if (effect.lock) {
52
+ output.lock = effect.lock;
53
+ }
54
+ if (effect.concurrency) {
55
+ output.concurrency = effect.concurrency;
56
+ }
57
+ return output;
58
+ }
59
+ function createSyntheticCommandContract(intentName, effects) {
60
+ return {
61
+ defaults: { default_cwd: '.' },
62
+ resources: {},
63
+ intents: {
64
+ [intentName]: {
65
+ cwd: '.',
66
+ writes: [],
67
+ effects: effects.map(effectToToml),
68
+ },
69
+ },
70
+ };
71
+ }
72
+ function parentRunId() {
73
+ const value = process.env[ACTIVE_RUN_LOCK_ID_ENV]?.trim();
74
+ return value && value.length > 0 ? value : null;
75
+ }
76
+ export function acquireActiveCommandLock(projectRoot, displayName, effects) {
77
+ return acquireActiveRunLock(projectRoot, createSyntheticCommandContract(displayName, effects), displayName, {
78
+ commandHash: null,
79
+ ignoreRunId: parentRunId(),
80
+ ignorePid: process.ppid,
81
+ });
82
+ }
83
+ export function renderActiveCommandLockConflictMessage(displayName, conflicts, lang) {
84
+ const [first] = conflicts;
85
+ const detail = first
86
+ ? t(lang, 'run.error.activeLockConflictDetail', {
87
+ lock: first.lock,
88
+ intent: first.conflictsWithIntent,
89
+ pid: first.conflictsWithPid,
90
+ })
91
+ : t(lang, 'run.error.activeLockConflictUnknown');
92
+ return t(lang, 'run.error.activeLockConflict', { intent: displayName, detail });
93
+ }
94
+ export function reportActiveCommandLockConflict(reporter, displayName, conflicts, helpCommand, lang) {
95
+ reporter.stderr(renderCliError(renderActiveCommandLockConflictMessage(displayName, conflicts, lang), helpCommand, lang));
96
+ }
@@ -87,10 +87,20 @@ function readNumber(table, key) {
87
87
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
88
88
  }
89
89
  function readPathContext(projectRoot, paths) {
90
- return paths.map((relativePath) => ({
91
- path: toPosixPath(relativePath),
92
- exists: safeExists(projectRoot, relativePath),
93
- }));
90
+ return paths.map((relativePath) => {
91
+ const normalizedPath = toPosixPath(relativePath);
92
+ const content = safeRead(projectRoot, normalizedPath);
93
+ return {
94
+ path: normalizedPath,
95
+ exists: content !== null,
96
+ trust: createContextTrust({
97
+ relativePath: normalizedPath,
98
+ cacheLayer: null,
99
+ contentHash: content === null ? null : sha256(content),
100
+ exists: content !== null,
101
+ }),
102
+ };
103
+ });
94
104
  }
95
105
  function readScalarObject(table) {
96
106
  if (!table) {
@@ -111,6 +121,100 @@ function readScalarObject(table) {
111
121
  function sha256(content) {
112
122
  return `sha256:${createHash('sha256').update(content).digest('hex')}`;
113
123
  }
124
+ function trustSourceKind(relativePath, sourceKind) {
125
+ if (sourceKind === 'runtime_volatile') {
126
+ return 'runtime_volatile';
127
+ }
128
+ if (relativePath === null) {
129
+ return sourceKind === 'dynamic_selection' ? 'source_placeholder' : 'unknown_file';
130
+ }
131
+ if (relativePath === COMMANDS_RELATIVE_PATH) {
132
+ return 'command_contract';
133
+ }
134
+ if (relativePath === LATEST_RUN_RELATIVE_PATH || relativePath.startsWith('.mustflow/state/')) {
135
+ return 'generated_state';
136
+ }
137
+ if (relativePath.startsWith('.mustflow/cache/')) {
138
+ return 'generated_cache';
139
+ }
140
+ if (relativePath.startsWith('.mustflow/skills/') && relativePath.endsWith('/SKILL.md')) {
141
+ return 'skill_file';
142
+ }
143
+ if (relativePath.startsWith('.mustflow/context/')) {
144
+ return 'context_file';
145
+ }
146
+ if (relativePath === 'AGENTS.md' ||
147
+ relativePath.startsWith('.mustflow/docs/') ||
148
+ relativePath.startsWith('.mustflow/config/') ||
149
+ relativePath.startsWith('.mustflow/skills/')) {
150
+ return 'workflow_file';
151
+ }
152
+ return 'unknown_file';
153
+ }
154
+ function trustAuthority(relativePath, sourceKind) {
155
+ if (relativePath === 'AGENTS.md') {
156
+ return 'binding';
157
+ }
158
+ if (relativePath === COMMANDS_RELATIVE_PATH) {
159
+ return 'command_contract';
160
+ }
161
+ if (relativePath === MUSTFLOW_RELATIVE_PATH ||
162
+ relativePath === PREFERENCES_RELATIVE_PATH ||
163
+ relativePath === TECHNOLOGY_RELATIVE_PATH) {
164
+ return 'configuration';
165
+ }
166
+ if (relativePath?.startsWith('.mustflow/docs/')) {
167
+ return 'workflow_policy';
168
+ }
169
+ if (relativePath?.startsWith('.mustflow/skills/') && relativePath.endsWith('/SKILL.md')) {
170
+ return 'procedure';
171
+ }
172
+ if (relativePath?.startsWith('.mustflow/context/')) {
173
+ return 'contextual';
174
+ }
175
+ if (sourceKind === 'generated_cache') {
176
+ return 'generated';
177
+ }
178
+ if (sourceKind === 'generated_state') {
179
+ return 'evidence_only';
180
+ }
181
+ if (sourceKind === 'runtime_volatile') {
182
+ return 'volatile';
183
+ }
184
+ if (sourceKind === 'source_placeholder') {
185
+ return 'hint';
186
+ }
187
+ return 'unknown';
188
+ }
189
+ function createContextTrust(input) {
190
+ const normalizedPath = input.relativePath === null ? null : toPosixPath(input.relativePath);
191
+ const sourceKind = trustSourceKind(normalizedPath, input.sourceKind ?? null);
192
+ const authority = trustAuthority(normalizedPath, sourceKind);
193
+ const freshness = sourceKind === 'runtime_volatile'
194
+ ? 'runtime_volatile'
195
+ : input.contentHash !== null
196
+ ? 'hash_verified'
197
+ : input.exists === false
198
+ ? 'missing'
199
+ : sourceKind === 'source_placeholder'
200
+ ? 'dynamic'
201
+ : 'unchecked';
202
+ return {
203
+ source_kind: sourceKind,
204
+ authority,
205
+ cache_layer: input.cacheLayer,
206
+ freshness,
207
+ content_hash: input.contentHash,
208
+ can_instruct_agent: authority === 'binding' ||
209
+ authority === 'workflow_policy' ||
210
+ authority === 'configuration' ||
211
+ authority === 'command_contract' ||
212
+ authority === 'procedure' ||
213
+ authority === 'contextual',
214
+ grants_command_authority: authority === 'command_contract',
215
+ notes: input.notes ?? [],
216
+ };
217
+ }
114
218
  function estimateTokens(renderedBytes) {
115
219
  return Math.ceil(renderedBytes / 4);
116
220
  }
@@ -396,10 +500,19 @@ function readStablePromptCacheLayer(projectRoot, mustflow) {
396
500
  const read = readOptionalStringArray(layer, 'read') ?? [...DEFAULT_PROMPT_CACHE_STABLE_READ];
397
501
  const documents = read.map((relativePath) => {
398
502
  const content = safeRead(projectRoot, relativePath);
503
+ const contentHash = content === null ? null : sha256(content);
504
+ const normalizedPath = toPosixPath(relativePath);
399
505
  return {
400
- path: toPosixPath(relativePath),
506
+ path: normalizedPath,
401
507
  exists: content !== null,
402
- content_hash: content === null ? null : sha256(content),
508
+ content_hash: contentHash,
509
+ trust: createContextTrust({
510
+ relativePath: normalizedPath,
511
+ cacheLayer: 'stable',
512
+ contentHash,
513
+ exists: content !== null,
514
+ sourceKind: 'file_reference',
515
+ }),
403
516
  };
404
517
  });
405
518
  const cacheMaterial = documents
@@ -515,6 +628,7 @@ function readStablePromptBundleLayer(projectRoot, mustflow) {
515
628
  blocks: read.map((relativePath, index) => {
516
629
  const path = toPosixPath(relativePath);
517
630
  const content = safeRead(projectRoot, relativePath);
631
+ const contentHash = content === null ? null : sha256(content);
518
632
  const renderedBytes = content === null ? null : measurePromptCacheReferenceBlockBytes(relativePath, content);
519
633
  const issue = content === null ? `stable prefix document is missing: ${path}` : null;
520
634
  return {
@@ -527,12 +641,19 @@ function readStablePromptBundleLayer(projectRoot, mustflow) {
527
641
  source_kind: 'file_reference',
528
642
  selection_policy: 'always_rendered',
529
643
  content_included: false,
530
- content_hash: content === null ? null : sha256(content),
644
+ content_hash: contentHash,
531
645
  rendered_digest: content === null ? null : renderedDigest(relativePath, content),
532
646
  rendered_bytes: renderedBytes,
533
647
  estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
534
648
  cacheability: 'provider_prefix_candidate',
535
649
  reload_on: ['stable_file_hash_changed'],
650
+ trust: createContextTrust({
651
+ relativePath: path,
652
+ cacheLayer: 'stable',
653
+ contentHash,
654
+ exists: content !== null,
655
+ sourceKind: 'file_reference',
656
+ }),
536
657
  issue,
537
658
  };
538
659
  }),
@@ -589,6 +710,14 @@ function readTaskPromptBundleLayer(projectRoot, mustflow, routeReport) {
589
710
  estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
590
711
  cacheability: taskSourceCacheability(source, selectionPolicy),
591
712
  reload_on: taskSourceReloadOn(source, selectionPolicy),
713
+ trust: createContextTrust({
714
+ relativePath: routeCandidateContent !== null ? SKILL_ROUTE_CANDIDATES_VIRTUAL_PATH : referencePath,
715
+ cacheLayer: 'task',
716
+ contentHash,
717
+ exists: routeCandidateContent !== null || content !== null ? true : sourceKind === 'file_reference' ? false : null,
718
+ sourceKind,
719
+ notes: source === 'skill_route_candidates' ? ['derived from route resolver input'] : [],
720
+ }),
592
721
  issue,
593
722
  };
594
723
  if (source !== 'matching_skill') {
@@ -597,6 +726,7 @@ function readTaskPromptBundleLayer(projectRoot, mustflow, routeReport) {
597
726
  const selectedBlocks = selectedSkillPaths.map((skillPath, selectedIndex) => {
598
727
  const normalizedPath = toPosixPath(skillPath);
599
728
  const skillContent = safeRead(projectRoot, normalizedPath);
729
+ const skillContentHash = skillContent === null ? null : sha256(skillContent);
600
730
  const skillRenderedBytes = skillContent === null ? null : measurePromptCacheReferenceBlockBytes(normalizedPath, skillContent);
601
731
  const skillIssue = skillContent === null
602
732
  ? taskSourceIssue(normalizedPath, 'hash_only_deferred')
@@ -611,12 +741,20 @@ function readTaskPromptBundleLayer(projectRoot, mustflow, routeReport) {
611
741
  source_kind: skillContent === null ? 'dynamic_selection' : 'file_reference',
612
742
  selection_policy: 'selected_at_runtime',
613
743
  content_included: false,
614
- content_hash: skillContent === null ? null : sha256(skillContent),
744
+ content_hash: skillContentHash,
615
745
  rendered_digest: skillContent === null ? null : renderedDigest(normalizedPath, skillContent),
616
746
  rendered_bytes: skillRenderedBytes,
617
747
  estimated_tokens: skillRenderedBytes === null ? null : estimateTokens(skillRenderedBytes),
618
748
  cacheability: 'runtime_selection',
619
749
  reload_on: ['route_resolution_changed', 'source_file_hash_changed'],
750
+ trust: createContextTrust({
751
+ relativePath: normalizedPath,
752
+ cacheLayer: 'task',
753
+ contentHash: skillContentHash,
754
+ exists: skillContent !== null,
755
+ sourceKind: skillContent === null ? 'dynamic_selection' : 'file_reference',
756
+ notes: ['selected by task route resolver'],
757
+ }),
620
758
  issue: skillIssue,
621
759
  };
622
760
  });
@@ -646,6 +784,14 @@ function readVolatilePromptBundleLayer(mustflow) {
646
784
  estimated_tokens: null,
647
785
  cacheability: 'volatile_suffix',
648
786
  reload_on: ['volatile_state_changed'],
787
+ trust: createContextTrust({
788
+ relativePath: null,
789
+ cacheLayer: 'volatile',
790
+ contentHash: null,
791
+ exists: null,
792
+ sourceKind: 'runtime_volatile',
793
+ notes: ['not rendered in stable or task prompt cache layers'],
794
+ }),
649
795
  issue,
650
796
  })),
651
797
  };
@@ -663,10 +809,27 @@ function readPromptBundle(projectRoot, mustflow, profile, routeReport = null) {
663
809
  }
664
810
  const blocks = layers.flatMap((layer) => layer.blocks);
665
811
  const shapeMaterial = blocks
666
- .map((block) => [block.cache_layer, block.order, block.kind, block.path ?? '', block.source ?? '', block.selection_policy, block.cacheability].join('\0'))
812
+ .map((block) => [
813
+ block.cache_layer,
814
+ block.order,
815
+ block.kind,
816
+ block.path ?? '',
817
+ block.source ?? '',
818
+ block.selection_policy,
819
+ block.cacheability,
820
+ block.trust.authority,
821
+ block.trust.source_kind,
822
+ ].join('\0'))
667
823
  .join('\n');
668
824
  const bundleMaterial = blocks
669
- .map((block) => [block.id, block.content_hash ?? '', block.rendered_digest ?? '', block.issue ?? ''].join('\0'))
825
+ .map((block) => [
826
+ block.id,
827
+ block.content_hash ?? '',
828
+ block.rendered_digest ?? '',
829
+ block.trust.freshness,
830
+ block.trust.content_hash ?? '',
831
+ block.issue ?? '',
832
+ ].join('\0'))
670
833
  .join('\n');
671
834
  return {
672
835
  schema_version: '1',
@@ -744,6 +907,9 @@ function blockLocation(block) {
744
907
  function compareStringArray(left, right) {
745
908
  return left.length === right.length && left.every((entry, index) => entry === right[index]);
746
909
  }
910
+ function contextTrustMatches(left, right) {
911
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
912
+ }
747
913
  function changedBlockFields(current, baseline) {
748
914
  const fields = [];
749
915
  if (current.id !== baseline.id) {
@@ -779,6 +945,9 @@ function changedBlockFields(current, baseline) {
779
945
  if (current.cacheability !== baseline.cacheability) {
780
946
  fields.push('cacheability');
781
947
  }
948
+ if (!contextTrustMatches(current.trust, baseline.trust)) {
949
+ fields.push('trust');
950
+ }
782
951
  if (!compareStringArray(current.reload_on, baseline.reload_on)) {
783
952
  fields.push('reload_on');
784
953
  }
@@ -95,6 +95,12 @@ export const COMMAND_DEFINITIONS = [
95
95
  summaryKey: 'command.quality.summary',
96
96
  loadRunner: async () => (await import('../commands/quality.js')).runQuality,
97
97
  },
98
+ {
99
+ id: 'script-pack',
100
+ usage: 'mf script-pack',
101
+ summaryKey: 'command.scriptPack.summary',
102
+ loadRunner: async () => (await import('../commands/script-pack.js')).runScriptPack,
103
+ },
98
104
  {
99
105
  id: 'run',
100
106
  usage: 'mf run',
@@ -448,6 +448,7 @@ function createDashboardHarnessReport(statusValue, docsReviewValue) {
448
448
  verification: {
449
449
  completion_verdict: completionVerdict,
450
450
  evidence_model: evidenceModel,
451
+ conflict_ledger: evidenceModel.conflict_ledger,
451
452
  changed_file_count: changedFileCount,
452
453
  changed_surfaces: changedSurfaces,
453
454
  decision_graph_summary: decisionGraphSummary,
@@ -0,0 +1,27 @@
1
+ function scriptRef(packId, scriptId) {
2
+ return `${packId}/${scriptId}`;
3
+ }
4
+ export const SCRIPT_PACKS = [
5
+ {
6
+ id: 'core',
7
+ summaryKey: 'scriptPack.pack.core.summary',
8
+ scripts: [
9
+ {
10
+ packId: 'core',
11
+ id: 'text-budget',
12
+ ref: scriptRef('core', 'text-budget'),
13
+ usage: 'mf script-pack run core/text-budget check <path...> [options]',
14
+ summaryKey: 'scriptPack.script.textBudget.summary',
15
+ actions: ['check'],
16
+ reportSchemaFile: 'text-budget-report.schema.json',
17
+ loadRunner: async () => (await import('../script-packs/core-text-budget.js')).runCoreTextBudgetScript,
18
+ },
19
+ ],
20
+ },
21
+ ];
22
+ export function listScriptPackScripts() {
23
+ return SCRIPT_PACKS.flatMap((pack) => pack.scripts);
24
+ }
25
+ export function findScriptPackScript(ref) {
26
+ return listScriptPackScripts().find((script) => script.ref === ref);
27
+ }
@@ -0,0 +1,241 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { inspectTextBudget, TEXT_BUDGET_SCRIPT_REF, TEXT_BUDGET_UNITS, } from '../../core/text-budget.js';
6
+ const TEXT_BUDGET_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--min', kind: 'string' },
9
+ { name: '--max', kind: 'string' },
10
+ { name: '--exact', kind: 'string' },
11
+ { name: '--unit', kind: 'string' },
12
+ { name: '--json-pointer', kind: 'string' },
13
+ ];
14
+ export function getCoreTextBudgetHelp(lang = 'en') {
15
+ return renderHelp({
16
+ usage: 'mf script-pack run core/text-budget check <path...> [options]',
17
+ summary: t(lang, 'textBudget.help.summary'),
18
+ options: [
19
+ { label: '--min <count>', description: t(lang, 'textBudget.help.option.min') },
20
+ { label: '--max <count>', description: t(lang, 'textBudget.help.option.max') },
21
+ { label: '--exact <count>', description: t(lang, 'textBudget.help.option.exact') },
22
+ { label: '--unit <unit>', description: t(lang, 'textBudget.help.option.unit') },
23
+ { label: '--json-pointer <pointer>', description: t(lang, 'textBudget.help.option.jsonPointer') },
24
+ { label: '--json', description: t(lang, 'cli.option.json') },
25
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
26
+ ],
27
+ examples: [
28
+ 'mf script-pack run core/text-budget check README.md --max 5000',
29
+ 'mf script-pack run core/text-budget check package.json --json-pointer /description --max 80 --json',
30
+ 'mf script-pack run core/text-budget check docs/intro.md --min 20 --max 120 --unit word',
31
+ ],
32
+ exitCodes: [
33
+ { label: '0', description: t(lang, 'textBudget.help.exit.ok') },
34
+ { label: '1', description: t(lang, 'textBudget.help.exit.fail') },
35
+ ],
36
+ }, lang);
37
+ }
38
+ function parseNonNegativeInteger(value, option, lang) {
39
+ if (value === null) {
40
+ return { value: null };
41
+ }
42
+ if (!/^(?:0|[1-9]\d*)$/u.test(value)) {
43
+ return { value: null, error: t(lang, 'textBudget.error.invalidNumber', { option, value }) };
44
+ }
45
+ const parsed = Number(value);
46
+ if (!Number.isSafeInteger(parsed)) {
47
+ return { value: null, error: t(lang, 'textBudget.error.invalidNumber', { option, value }) };
48
+ }
49
+ return { value: parsed };
50
+ }
51
+ function parseTextBudgetUnit(value, lang) {
52
+ if (value === null) {
53
+ return { value: 'grapheme' };
54
+ }
55
+ if (TEXT_BUDGET_UNITS.includes(value)) {
56
+ return { value: value };
57
+ }
58
+ return {
59
+ value: 'grapheme',
60
+ error: t(lang, 'textBudget.error.invalidUnit', { unit: value, allowed: TEXT_BUDGET_UNITS.join(', ') }),
61
+ };
62
+ }
63
+ function parseTextBudgetOptions(args, lang) {
64
+ const [action, ...rest] = args;
65
+ const parsed = parseCliOptions(rest, TEXT_BUDGET_OPTIONS, {
66
+ allowPositionals: true,
67
+ allowEmptyStringValues: true,
68
+ });
69
+ const json = hasParsedCliOption(parsed, '--json');
70
+ const min = parseNonNegativeInteger(getParsedCliStringOption(parsed, '--min'), '--min', lang);
71
+ const max = parseNonNegativeInteger(getParsedCliStringOption(parsed, '--max'), '--max', lang);
72
+ const exact = parseNonNegativeInteger(getParsedCliStringOption(parsed, '--exact'), '--exact', lang);
73
+ const unit = parseTextBudgetUnit(getParsedCliStringOption(parsed, '--unit'), lang);
74
+ const jsonPointer = getParsedCliStringOption(parsed, '--json-pointer');
75
+ if (action !== 'check') {
76
+ return {
77
+ action: 'check',
78
+ json,
79
+ paths: parsed.positionals,
80
+ min: min.value,
81
+ max: max.value,
82
+ exact: exact.value,
83
+ unit: unit.value,
84
+ jsonPointer,
85
+ error: action ? t(lang, 'textBudget.error.unknownAction', { action }) : t(lang, 'textBudget.error.missingAction'),
86
+ };
87
+ }
88
+ if (parsed.error) {
89
+ return {
90
+ action,
91
+ json,
92
+ paths: parsed.positionals,
93
+ min: min.value,
94
+ max: max.value,
95
+ exact: exact.value,
96
+ unit: unit.value,
97
+ jsonPointer,
98
+ error: formatCliOptionParseError(parsed.error, lang),
99
+ };
100
+ }
101
+ for (const candidate of [min, max, exact, unit]) {
102
+ if (candidate.error) {
103
+ return {
104
+ action,
105
+ json,
106
+ paths: parsed.positionals,
107
+ min: min.value,
108
+ max: max.value,
109
+ exact: exact.value,
110
+ unit: unit.value,
111
+ jsonPointer,
112
+ error: candidate.error,
113
+ };
114
+ }
115
+ }
116
+ if (parsed.positionals.length === 0) {
117
+ return {
118
+ action,
119
+ json,
120
+ paths: parsed.positionals,
121
+ min: min.value,
122
+ max: max.value,
123
+ exact: exact.value,
124
+ unit: unit.value,
125
+ jsonPointer,
126
+ error: t(lang, 'textBudget.error.missingPath'),
127
+ };
128
+ }
129
+ if (exact.value !== null && (min.value !== null || max.value !== null)) {
130
+ return {
131
+ action,
132
+ json,
133
+ paths: parsed.positionals,
134
+ min: min.value,
135
+ max: max.value,
136
+ exact: exact.value,
137
+ unit: unit.value,
138
+ jsonPointer,
139
+ error: t(lang, 'textBudget.error.exactConflict'),
140
+ };
141
+ }
142
+ if (exact.value === null && min.value === null && max.value === null) {
143
+ return {
144
+ action,
145
+ json,
146
+ paths: parsed.positionals,
147
+ min: min.value,
148
+ max: max.value,
149
+ exact: exact.value,
150
+ unit: unit.value,
151
+ jsonPointer,
152
+ error: t(lang, 'textBudget.error.missingBudget'),
153
+ };
154
+ }
155
+ if (min.value !== null && max.value !== null && min.value > max.value) {
156
+ return {
157
+ action,
158
+ json,
159
+ paths: parsed.positionals,
160
+ min: min.value,
161
+ max: max.value,
162
+ exact: exact.value,
163
+ unit: unit.value,
164
+ jsonPointer,
165
+ error: t(lang, 'textBudget.error.minGreaterThanMax'),
166
+ };
167
+ }
168
+ return {
169
+ action,
170
+ json,
171
+ paths: parsed.positionals,
172
+ min: min.value,
173
+ max: max.value,
174
+ exact: exact.value,
175
+ unit: unit.value,
176
+ jsonPointer,
177
+ };
178
+ }
179
+ function renderBudget(report) {
180
+ if (report.policy.exact !== null) {
181
+ return `exact ${report.policy.exact} ${report.policy.unit}`;
182
+ }
183
+ const lower = report.policy.min === null ? null : `min ${report.policy.min}`;
184
+ const upper = report.policy.max === null ? null : `max ${report.policy.max}`;
185
+ return [lower, upper].filter((part) => part !== null).join(', ') + ` ${report.policy.unit}`;
186
+ }
187
+ function renderTextBudgetSummary(report, lang) {
188
+ const lines = [
189
+ t(lang, 'textBudget.title'),
190
+ `${t(lang, 'scriptPack.label.script')}: ${TEXT_BUDGET_SCRIPT_REF}`,
191
+ `${t(lang, 'label.status')}: ${report.status}`,
192
+ `${t(lang, 'textBudget.label.budget')}: ${renderBudget(report)}`,
193
+ `${t(lang, 'textBudget.label.checkedTargets')}: ${report.metrics.length}`,
194
+ `${t(lang, 'textBudget.label.findings')}: ${report.findings.length}`,
195
+ ];
196
+ if (report.metrics.length > 0) {
197
+ lines.push(t(lang, 'textBudget.label.metrics'));
198
+ for (const metric of report.metrics) {
199
+ const pointer = metric.json_pointer === null ? '' : ` ${metric.json_pointer}`;
200
+ lines.push(`- ${metric.path}${pointer}: ${metric.value} ${metric.unit}`);
201
+ }
202
+ }
203
+ if (report.findings.length > 0) {
204
+ lines.push(t(lang, 'textBudget.label.findings'));
205
+ for (const finding of report.findings) {
206
+ lines.push(`- ${finding.path}: ${finding.code} (${finding.message})`);
207
+ }
208
+ }
209
+ if (report.issues.length > 0) {
210
+ lines.push(t(lang, 'textBudget.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
211
+ }
212
+ if (report.findings.length === 0 && report.issues.length === 0) {
213
+ lines.push(t(lang, 'textBudget.clean'));
214
+ }
215
+ return lines.join('\n');
216
+ }
217
+ export function runCoreTextBudgetScript(args, reporter, lang = 'en') {
218
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
219
+ reporter.stdout(getCoreTextBudgetHelp(lang));
220
+ return 0;
221
+ }
222
+ const options = parseTextBudgetOptions(args, lang);
223
+ if (options.error) {
224
+ printUsageError(reporter, options.error, 'mf script-pack run core/text-budget --help', getCoreTextBudgetHelp(lang), lang);
225
+ return 1;
226
+ }
227
+ const report = inspectTextBudget(resolveMustflowRoot(), {
228
+ paths: options.paths,
229
+ min: options.min,
230
+ max: options.max,
231
+ exact: options.exact,
232
+ unit: options.unit,
233
+ jsonPointer: options.jsonPointer,
234
+ });
235
+ if (options.json) {
236
+ reporter.stdout(JSON.stringify(report, null, 2));
237
+ return report.ok ? 0 : 1;
238
+ }
239
+ reporter.stdout(renderTextBudgetSummary(report, lang));
240
+ return report.ok ? 0 : 1;
241
+ }