mustflow 2.22.4 → 2.22.9

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 (72) hide show
  1. package/README.md +17 -75
  2. package/dist/cli/commands/classify.js +2 -0
  3. package/dist/cli/commands/contract-lint.js +2 -2
  4. package/dist/cli/commands/dashboard.js +23 -75
  5. package/dist/cli/commands/help.js +8 -9
  6. package/dist/cli/commands/impact.js +2 -3
  7. package/dist/cli/commands/init.js +61 -5
  8. package/dist/cli/commands/run/receipt.js +1 -0
  9. package/dist/cli/commands/run.js +14 -1
  10. package/dist/cli/commands/update.js +2 -2
  11. package/dist/cli/commands/verify/evidence-input.js +269 -0
  12. package/dist/cli/commands/verify/input.js +212 -0
  13. package/dist/cli/commands/verify.js +23 -482
  14. package/dist/cli/commands/version-sources.js +2 -3
  15. package/dist/cli/i18n/en.js +5 -0
  16. package/dist/cli/i18n/es.js +5 -0
  17. package/dist/cli/i18n/fr.js +5 -0
  18. package/dist/cli/i18n/hi.js +5 -0
  19. package/dist/cli/i18n/ko.js +5 -0
  20. package/dist/cli/i18n/zh.js +5 -0
  21. package/dist/cli/lib/agent-context.js +6 -11
  22. package/dist/cli/lib/dashboard-export.js +2 -0
  23. package/dist/cli/lib/dashboard-mutations.js +79 -0
  24. package/dist/cli/lib/local-index/command-effect-index.js +25 -0
  25. package/dist/cli/lib/local-index/hashing.js +7 -0
  26. package/dist/cli/lib/local-index/index.js +127 -823
  27. package/dist/cli/lib/local-index/source-index.js +137 -0
  28. package/dist/cli/lib/local-index/verification-evidence.js +451 -0
  29. package/dist/cli/lib/local-index/workflow-documents.js +204 -0
  30. package/dist/cli/lib/mustflow-read.js +41 -0
  31. package/dist/cli/lib/project-root.js +1 -2
  32. package/dist/cli/lib/repo-map.js +65 -16
  33. package/dist/cli/lib/run-root-trust.js +27 -0
  34. package/dist/cli/lib/templates.js +124 -8
  35. package/dist/cli/lib/toml.js +6 -1
  36. package/dist/cli/lib/validation/constants.js +2 -0
  37. package/dist/cli/lib/validation/index.js +291 -22
  38. package/dist/cli/lib/validation/primitives.js +2 -2
  39. package/dist/cli/lib/validation/test-selection.js +2 -2
  40. package/dist/core/bounded-output.js +32 -7
  41. package/dist/core/change-classification-policy.js +47 -0
  42. package/dist/core/change-classification.js +10 -43
  43. package/dist/core/check-issues.js +7 -1
  44. package/dist/core/command-contract-validation.js +28 -4
  45. package/dist/core/command-env.js +1 -1
  46. package/dist/core/config-loading.js +9 -3
  47. package/dist/core/contract-lint.js +8 -3
  48. package/dist/core/correlation-id.js +16 -0
  49. package/dist/core/run-receipt.js +1 -0
  50. package/dist/core/safe-filesystem.js +11 -4
  51. package/dist/core/skill-route-alignment.js +1 -0
  52. package/dist/core/skill-route-explanation.js +9 -3
  53. package/dist/core/test-selection.js +2 -3
  54. package/dist/core/verification-scheduler.js +7 -6
  55. package/dist/core/version-sources.js +2 -3
  56. package/package.json +4 -1
  57. package/schemas/README.md +4 -0
  58. package/schemas/change-verification-report.schema.json +4 -0
  59. package/schemas/classify-report.schema.json +4 -0
  60. package/schemas/commands.schema.json +1 -0
  61. package/schemas/dashboard-export.schema.json +4 -0
  62. package/schemas/latest-run-pointer.schema.json +4 -0
  63. package/schemas/run-receipt.schema.json +4 -0
  64. package/schemas/verify-report.schema.json +4 -0
  65. package/schemas/verify-run-manifest.schema.json +4 -0
  66. package/templates/default/i18n.toml +3 -3
  67. package/templates/default/locales/en/.mustflow/skills/INDEX.md +10 -6
  68. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +25 -2
  69. package/templates/default/locales/en/.mustflow/skills/routes.toml +2 -2
  70. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +9 -1
  71. package/templates/default/locales/en/.mustflow/skills/test-design-guard/SKILL.md +9 -1
  72. package/templates/default/manifest.toml +1 -1
@@ -583,6 +583,8 @@ Lee estos archivos antes de trabajar:
583
583
  "init.error.invalidPreference": "Anulación de preferencia de inicio no válida: {value}",
584
584
  "init.error.invalidPreferenceValue": "Valor no valido para {key}: {value}",
585
585
  "init.error.unsupportedPreference": "Ajuste de preferencia de inicio no admitido: {key}",
586
+ "init.error.promptInputTooLarge": "La entrada estándar de init interactivo es demasiado grande; se esperaban como máximo {maxBytes} bytes.",
587
+ "init.error.promptInputTooManyResponses": "La entrada estándar de init interactivo tiene demasiadas respuestas; se esperaban como máximo {maxResponses} líneas.",
586
588
  "init.prompt.locale": "¿Qué idioma deben usar los documentos mustflow?",
587
589
  "init.prompt.profile": "¿Qué perfil de proyecto debe usar mustflow?",
588
590
  "init.prompt.agentLang": "¿Qué idioma deben usar los agentes en los informes finales?",
@@ -650,6 +652,7 @@ Lee estos archivos antes de trabajar:
650
652
  "run.help.option.dryRun": "Imprime un plan de comando sin ejecutarlo",
651
653
  "run.help.option.planOnly": "Alias de --dry-run",
652
654
  "run.help.option.json": "Imprime el registro de ejecución o el plan de comando como JSON",
655
+ "run.help.option.allowUntrustedRoot": "Permite una ejecución desde una raíz con bloqueo de manifiesto ausente o inválido tras revisión manual",
653
656
  "run.help.exit.ok": "El comando se completo con un codigo de salida permitido",
654
657
  "run.help.exit.fail": "El comando no era válido, fue rechazado, agotó el tiempo o falló",
655
658
  "run.label.suggestedIntentSnippet": "Snippet sugerido para el contrato de comandos",
@@ -672,6 +675,8 @@ Lee estos archivos antes de trabajar:
672
675
  "run.error.maxOutputBytes": 'El comando "{intent}" tiene max_output_bytes no válido. {detail}',
673
676
  "run.error.maxOutputBytesDetail": "El límite de salida debe permanecer dentro del máximo permitido.",
674
677
  "run.error.conflictingPreviewModes": "Usa --dry-run o --plan-only, no ambos",
678
+ "run.error.untrustedRootMissing": "Se rechazó ejecutar comandos porque falta {path}. Ejecuta mf init/update para instalar el flujo, o usa --allow-untrusted-root tras revisar AGENTS.md y .mustflow/config/commands.toml.",
679
+ "run.error.untrustedRootInvalid": "Se rechazó ejecutar comandos porque el bloqueo de manifiesto no es válido: {detail}. Restáuralo o regenéralo, o usa --allow-untrusted-root tras revisar AGENTS.md y .mustflow/config/commands.toml.",
675
680
  "run.error.timedOut": 'El comando "{intent}" agotó el tiempo después de {seconds} segundos',
676
681
  "run.error.outputLimitExceeded": 'El comando "{intent}" superó max_output_bytes: {message}',
677
682
  "run.error.startFailed": 'No se pudo iniciar el comando "{intent}": {message}',
@@ -583,6 +583,8 @@ Lisez ces fichiers avant de travailler :
583
583
  "init.error.invalidPreference": "Remplacement de préférence d'initialisation non valide : {value}",
584
584
  "init.error.invalidPreferenceValue": "Valeur non valide pour {key} : {value}",
585
585
  "init.error.unsupportedPreference": "Paramètre de préférence d'initialisation non pris en charge : {key}",
586
+ "init.error.promptInputTooLarge": "L'entrée standard de l'init interactif est trop volumineuse ; {maxBytes} octets maximum sont attendus.",
587
+ "init.error.promptInputTooManyResponses": "L'entrée standard de l'init interactif contient trop de réponses ; {maxResponses} lignes maximum sont attendues.",
586
588
  "init.prompt.locale": "Quelle langue les documents mustflow doivent-ils utiliser ?",
587
589
  "init.prompt.profile": "Quel profil de projet mustflow doit-il utiliser ?",
588
590
  "init.prompt.agentLang": "Quelle langue les agents doivent-ils utiliser pour les rapports finaux ?",
@@ -650,6 +652,7 @@ Lisez ces fichiers avant de travailler :
650
652
  "run.help.option.dryRun": "Imprime un plan de commande sans l'exécuter",
651
653
  "run.help.option.planOnly": "Alias de --dry-run",
652
654
  "run.help.option.json": "Imprime l'enregistrement d'exécution ou le plan de commande en JSON",
655
+ "run.help.option.allowUntrustedRoot": "Autorise une seule exécution depuis une racine sans verrou de manifeste valide après revue manuelle",
653
656
  "run.help.exit.ok": "La commande s'est terminée avec un code de sortie autorisé",
654
657
  "run.help.exit.fail": "La commande était non valide, refusée, expirée ou a échoué",
655
658
  "run.label.suggestedIntentSnippet": "Extrait suggéré de contrat de commande",
@@ -672,6 +675,8 @@ Lisez ces fichiers avant de travailler :
672
675
  "run.error.maxOutputBytes": 'La commande "{intent}" a une valeur max_output_bytes non valide. {detail}',
673
676
  "run.error.maxOutputBytesDetail": "La limite de sortie doit rester dans le maximum autorisé.",
674
677
  "run.error.conflictingPreviewModes": "Utilisez --dry-run ou --plan-only, pas les deux",
678
+ "run.error.untrustedRootMissing": "Exécution refusée car {path} est absent. Lancez mf init/update pour installer le workflow, ou ajoutez --allow-untrusted-root après avoir relu AGENTS.md et .mustflow/config/commands.toml.",
679
+ "run.error.untrustedRootInvalid": "Exécution refusée car le verrou de manifeste est invalide : {detail}. Restaurez-le ou régénérez-le, ou ajoutez --allow-untrusted-root après avoir relu AGENTS.md et .mustflow/config/commands.toml.",
675
680
  "run.error.timedOut": 'La commande "{intent}" a expiré après {seconds} secondes',
676
681
  "run.error.outputLimitExceeded": 'La commande "{intent}" a dépassé max_output_bytes : {message}',
677
682
  "run.error.startFailed": 'Impossible de démarrer la commande "{intent}" : {message}',
@@ -583,6 +583,8 @@ export const hiMessages = {
583
583
  "init.error.invalidPreference": "अमान्य init preference override: {value}",
584
584
  "init.error.invalidPreferenceValue": "{key} के लिए अमान्य मान: {value}",
585
585
  "init.error.unsupportedPreference": "असमर्थित init preference setting: {key}",
586
+ "init.error.promptInputTooLarge": "Interactive init stdin input बहुत बड़ा है; अधिकतम {maxBytes} bytes अपेक्षित हैं।",
587
+ "init.error.promptInputTooManyResponses": "Interactive init stdin input में बहुत अधिक responses हैं; अधिकतम {maxResponses} lines अपेक्षित हैं।",
586
588
  "init.prompt.locale": "mustflow दस्तावेज़ कौन सी भाषा उपयोग करें?",
587
589
  "init.prompt.profile": "mustflow कौन सा project profile उपयोग करे?",
588
590
  "init.prompt.agentLang": "एजेंट final reports के लिए कौन सी भाषा उपयोग करें?",
@@ -650,6 +652,7 @@ export const hiMessages = {
650
652
  "run.help.option.dryRun": "कमांड चलाए बिना उसका plan प्रिंट करें",
651
653
  "run.help.option.planOnly": "--dry-run का alias",
652
654
  "run.help.option.json": "Run record या command plan को JSON के रूप में प्रिंट करें",
655
+ "run.help.option.allowUntrustedRoot": "Manual review के बाद missing या invalid manifest lock वाली root से एक execution allow करें",
653
656
  "run.help.exit.ok": "कमांड अनुमत exit code के साथ पूरी हुई",
654
657
  "run.help.exit.fail": "कमांड अमान्य थी, अस्वीकार हुई, timed out हुई या विफल हुई",
655
658
  "run.label.suggestedIntentSnippet": "Suggested command contract snippet",
@@ -672,6 +675,8 @@ export const hiMessages = {
672
675
  "run.error.maxOutputBytes": 'कमांड "{intent}" में max_output_bytes अमान्य है। {detail}',
673
676
  "run.error.maxOutputBytesDetail": "Output limit अनुमत maximum के अंदर रहनी चाहिए।",
674
677
  "run.error.conflictingPreviewModes": "--dry-run या --plan-only में से एक इस्तेमाल करें, दोनों नहीं",
678
+ "run.error.untrustedRootMissing": "{path} missing है, इसलिए commands execute करने से मना किया गया। Workflow install करने के लिए mf init/update चलाएँ, या AGENTS.md और .mustflow/config/commands.toml review करने के बाद --allow-untrusted-root पास करें।",
679
+ "run.error.untrustedRootInvalid": "Manifest lock invalid है, इसलिए commands execute करने से मना किया गया: {detail}. इसे restore या regenerate करें, या AGENTS.md और .mustflow/config/commands.toml review करने के बाद --allow-untrusted-root पास करें।",
675
680
  "run.error.timedOut": 'कमांड "{intent}" {seconds} सेकंड बाद time out हुई',
676
681
  "run.error.outputLimitExceeded": 'कमांड "{intent}" ने max_output_bytes सीमा पार की: {message}',
677
682
  "run.error.startFailed": 'कमांड "{intent}" शुरू नहीं हो सकी: {message}',
@@ -583,6 +583,8 @@ export const koMessages = {
583
583
  "init.error.invalidPreference": "초기 설정 항목 형식이 올바르지 않습니다: {value}",
584
584
  "init.error.invalidPreferenceValue": "{key}에 사용할 수 없는 값입니다: {value}",
585
585
  "init.error.unsupportedPreference": "지원하지 않는 초기 설정 항목입니다: {key}",
586
+ "init.error.promptInputTooLarge": "대화형 init 표준입력이 너무 큽니다. 최대 {maxBytes}바이트까지만 허용됩니다.",
587
+ "init.error.promptInputTooManyResponses": "대화형 init 표준입력 응답이 너무 많습니다. 최대 {maxResponses}줄까지만 허용됩니다.",
586
588
  "init.prompt.locale": "mustflow 문서는 어떤 언어로 설치할까요?",
587
589
  "init.prompt.profile": "이 저장소에는 어떤 프로젝트 유형을 사용할까요?",
588
590
  "init.prompt.agentLang": "에이전트 최종 응답은 어떤 언어로 받을까요?",
@@ -650,6 +652,7 @@ export const koMessages = {
650
652
  "run.help.option.dryRun": "실행하지 않고 명령 계획을 출력합니다",
651
653
  "run.help.option.planOnly": "--dry-run과 같은 동작입니다",
652
654
  "run.help.option.json": "실행 결과 또는 명령 계획을 JSON으로 출력합니다",
655
+ "run.help.option.allowUntrustedRoot": "잠금 파일이 없거나 올바르지 않은 루트에서 수동 검토 후 이번 실행만 허용합니다",
653
656
  "run.help.exit.ok": "명령이 허용된 종료 코드로 완료되었습니다",
654
657
  "run.help.exit.fail": "명령이 잘못되었거나, 거부되었거나, 시간 초과되었거나, 실패했습니다",
655
658
  "run.label.suggestedIntentSnippet": "제안 명령 계약 조각",
@@ -672,6 +675,8 @@ export const koMessages = {
672
675
  "run.error.maxOutputBytes": '명령 "{intent}"의 max_output_bytes 값이 올바르지 않습니다. {detail}',
673
676
  "run.error.maxOutputBytesDetail": "출력 상한은 허용된 최댓값 안에 있어야 합니다.",
674
677
  "run.error.conflictingPreviewModes": "--dry-run과 --plan-only 중 하나만 사용하세요",
678
+ "run.error.untrustedRootMissing": "{path}이 없어 명령 실행을 거부했습니다. mf init/update로 워크플로우를 설치하거나, AGENTS.md와 .mustflow/config/commands.toml을 검토한 뒤 --allow-untrusted-root를 붙이세요.",
679
+ "run.error.untrustedRootInvalid": "잠금 파일이 올바르지 않아 명령 실행을 거부했습니다: {detail}. 파일을 복구하거나 다시 생성하거나, AGENTS.md와 .mustflow/config/commands.toml을 검토한 뒤 --allow-untrusted-root를 붙이세요.",
675
680
  "run.error.timedOut": '명령 "{intent}"가 {seconds}초 뒤 시간 초과되었습니다',
676
681
  "run.error.outputLimitExceeded": '명령 "{intent}"가 max_output_bytes 제한을 넘었습니다: {message}',
677
682
  "run.error.startFailed": '명령 "{intent}"를 시작하지 못했습니다: {message}',
@@ -583,6 +583,8 @@ export const zhMessages = {
583
583
  "init.error.invalidPreference": "无效的初始化偏好覆盖:{value}",
584
584
  "init.error.invalidPreferenceValue": "{key} 的值无效:{value}",
585
585
  "init.error.unsupportedPreference": "不支持的初始化偏好设置:{key}",
586
+ "init.error.promptInputTooLarge": "交互式 init 的标准输入过大;最多允许 {maxBytes} 字节。",
587
+ "init.error.promptInputTooManyResponses": "交互式 init 的标准输入响应过多;最多允许 {maxResponses} 行。",
586
588
  "init.prompt.locale": "mustflow 文档应使用哪种语言?",
587
589
  "init.prompt.profile": "mustflow 应使用哪个项目配置?",
588
590
  "init.prompt.agentLang": "代理最终报告应使用哪种语言?",
@@ -650,6 +652,7 @@ export const zhMessages = {
650
652
  "run.help.option.dryRun": "输出命令计划但不执行",
651
653
  "run.help.option.planOnly": "--dry-run 的别名",
652
654
  "run.help.option.json": "将运行记录或命令计划输出为 JSON",
655
+ "run.help.option.allowUntrustedRoot": "人工复核后,允许从缺失或无效清单锁的根目录执行一次命令",
653
656
  "run.help.exit.ok": "命令已以允许的退出码完成",
654
657
  "run.help.exit.fail": "命令无效、被拒绝、超时或失败",
655
658
  "run.label.suggestedIntentSnippet": "建议的命令契约片段",
@@ -672,6 +675,8 @@ export const zhMessages = {
672
675
  "run.error.maxOutputBytes": '命令 "{intent}" 的 max_output_bytes 无效。{detail}',
673
676
  "run.error.maxOutputBytesDetail": "输出限制必须保持在允许的最大值内。",
674
677
  "run.error.conflictingPreviewModes": "只能使用 --dry-run 或 --plan-only,不能同时使用",
678
+ "run.error.untrustedRootMissing": "已拒绝执行命令,因为缺少 {path}。请运行 mf init/update 安装工作流,或在检查 AGENTS.md 和 .mustflow/config/commands.toml 后传入 --allow-untrusted-root。",
679
+ "run.error.untrustedRootInvalid": "已拒绝执行命令,因为清单锁无效:{detail}。请恢复或重新生成它,或在检查 AGENTS.md 和 .mustflow/config/commands.toml 后传入 --allow-untrusted-root。",
675
680
  "run.error.timedOut": '命令 "{intent}" 在 {seconds} 秒后超时',
676
681
  "run.error.outputLimitExceeded": '命令 "{intent}" 超过 max_output_bytes:{message}',
677
682
  "run.error.startFailed": '命令 "{intent}" 启动失败:{message}',
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { isRecord, readPositiveInteger, readString, readStringArray, } from './command-contract.js';
@@ -6,7 +6,8 @@ import { readRetentionStore } from '../../core/retention-policy.js';
6
6
  import { toPosixPath } from './filesystem.js';
7
7
  import { readLocalIndexPromptContext } from './local-index.js';
8
8
  import { inspectManifestLock } from './manifest-lock.js';
9
- import { readTomlFile } from './toml.js';
9
+ import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, } from './mustflow-read.js';
10
+ import { readMustflowTomlFile } from './toml.js';
10
11
  const CONTEXT_SCHEMA_VERSION = '1';
11
12
  const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
12
13
  const MUSTFLOW_RELATIVE_PATH = '.mustflow/config/mustflow.toml';
@@ -49,20 +50,14 @@ function safeExists(projectRoot, relativePath) {
49
50
  return existsSync(resolved);
50
51
  }
51
52
  function safeRead(projectRoot, relativePath) {
52
- const resolved = path.resolve(projectRoot, ...relativePath.split('/'));
53
- const root = path.resolve(projectRoot);
54
- const relative = path.relative(root, resolved);
55
- if (relative.startsWith('..') || path.isAbsolute(relative) || !existsSync(resolved)) {
56
- return null;
57
- }
58
- return readFileSync(resolved, 'utf8');
53
+ return readMustflowTextFileIfExists(projectRoot, relativePath);
59
54
  }
60
55
  function readTomlTableIfExists(projectRoot, relativePath) {
61
56
  const filePath = path.join(projectRoot, ...relativePath.split('/'));
62
57
  if (!existsSync(filePath)) {
63
58
  return undefined;
64
59
  }
65
- const parsed = readTomlFile(filePath);
60
+ const parsed = readMustflowTomlFile(projectRoot, relativePath);
66
61
  return isRecord(parsed) ? parsed : undefined;
67
62
  }
68
63
  function readNestedTable(table, key) {
@@ -194,7 +189,7 @@ function readLatestRunContext(projectRoot) {
194
189
  };
195
190
  }
196
191
  try {
197
- const parsed = JSON.parse(readFileSync(latestPath, 'utf8'));
192
+ const parsed = JSON.parse(readMustflowTextFile(projectRoot, LATEST_RUN_RELATIVE_PATH, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
198
193
  if (!isRecord(parsed)) {
199
194
  throw new Error('latest run receipt must contain a JSON object');
200
195
  }
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import path from 'node:path';
3
3
  import { createDashboardCompletionVerdict } from '../../core/completion-verdict.js';
4
+ import { createCorrelationId } from '../../core/correlation-id.js';
4
5
  import { createDashboardEvidenceModel } from '../../core/verification-evidence.js';
5
6
  import { redactSecretLikeText } from '../../core/secret-redaction.js';
6
7
  import { ensureFileTargetInsideWithoutSymlinks, ensureInside, toPosixPath, writeUtf8FileInsideWithoutSymlinks, } from './filesystem.js';
@@ -487,6 +488,7 @@ export function createDashboardExportSnapshot(input) {
487
488
  const snapshot = {
488
489
  schema_version: '1',
489
490
  command: 'dashboard export',
491
+ correlation_id: createCorrelationId('dashboard'),
490
492
  format: input.format,
491
493
  generated_at: new Date().toISOString(),
492
494
  mustflow_root: input.projectRoot,
@@ -0,0 +1,79 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { openPathInFileManager } from './browser-open.js';
4
+ import { updateDashboardPreferences, } from './dashboard-preferences.js';
5
+ import { isReviewerKind, markDocReviewEntry, } from './doc-review-ledger.js';
6
+ const DOC_REVIEW_BULK_PAYLOAD_FIELDS = ['paths', 'documents', 'entries'];
7
+ function readPreferenceUpdatePayload(value) {
8
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
9
+ throw new Error('Request body must be a JSON object.');
10
+ }
11
+ const updates = value.updates;
12
+ if (!Array.isArray(updates)) {
13
+ throw new Error('Request body must include an updates array.');
14
+ }
15
+ return updates.map((entry) => {
16
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
17
+ throw new Error('Each update must be a JSON object.');
18
+ }
19
+ const update = entry;
20
+ if (typeof update.id !== 'string' || update.id.trim().length === 0) {
21
+ throw new Error('Each update must include an id.');
22
+ }
23
+ return { id: update.id, value: update.value };
24
+ });
25
+ }
26
+ function readOptionalStringField(value, key) {
27
+ const field = value[key];
28
+ return typeof field === 'string' && field.trim().length > 0 ? field.trim() : undefined;
29
+ }
30
+ function readRequiredStringField(value, key) {
31
+ const field = readOptionalStringField(value, key);
32
+ if (!field) {
33
+ throw new Error(`${key} is required.`);
34
+ }
35
+ return field;
36
+ }
37
+ function readDocReviewPayload(value) {
38
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
39
+ throw new Error('Request body must be a JSON object.');
40
+ }
41
+ const payload = value;
42
+ for (const field of DOC_REVIEW_BULK_PAYLOAD_FIELDS) {
43
+ if (field in payload) {
44
+ throw new Error('Bulk documentation review updates require a separate confirmed flow.');
45
+ }
46
+ }
47
+ const status = readRequiredStringField(payload, 'status');
48
+ if (status !== 'approved' && status !== 'needs_human' && status !== 'ignored') {
49
+ throw new Error('status must be approved, needs_human, or ignored.');
50
+ }
51
+ const reviewerKind = readRequiredStringField(payload, 'reviewerKind');
52
+ if (!isReviewerKind(reviewerKind)) {
53
+ throw new Error('reviewerKind must be human, llm, tool, or external.');
54
+ }
55
+ return {
56
+ path: readRequiredStringField(payload, 'path'),
57
+ status,
58
+ reviewerKind,
59
+ reviewerId: readRequiredStringField(payload, 'reviewerId'),
60
+ reviewerLabel: readOptionalStringField(payload, 'reviewerLabel'),
61
+ reviewerProvider: readOptionalStringField(payload, 'reviewerProvider'),
62
+ reviewerModel: readOptionalStringField(payload, 'reviewerModel'),
63
+ reviewerCommandIntent: readOptionalStringField(payload, 'reviewerCommandIntent'),
64
+ summary: readOptionalStringField(payload, 'summary'),
65
+ };
66
+ }
67
+ export function updateDashboardPreferencesFromPayload(projectRoot, payload) {
68
+ return updateDashboardPreferences(projectRoot, readPreferenceUpdatePayload(payload));
69
+ }
70
+ export function markDashboardDocReviewFromPayload(projectRoot, payload) {
71
+ markDocReviewEntry(projectRoot, readDocReviewPayload(payload));
72
+ }
73
+ export function openDashboardMustflowFolder(projectRoot) {
74
+ const mustflowPath = path.join(projectRoot, '.mustflow');
75
+ if (!existsSync(mustflowPath)) {
76
+ return 'missing';
77
+ }
78
+ return openPathInFileManager(mustflowPath) ? 'opened' : 'unavailable';
79
+ }
@@ -0,0 +1,25 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { isRecord, readCommandContract, readString } from '../command-contract.js';
4
+ import { normalizeCommandEffects } from '../../../core/command-effects.js';
5
+ export function collectCommandIntents(projectRoot) {
6
+ if (!existsSync(path.join(projectRoot, '.mustflow', 'config', 'commands.toml'))) {
7
+ return [];
8
+ }
9
+ const contract = readCommandContract(projectRoot);
10
+ const intents = [];
11
+ for (const [name, intent] of Object.entries(contract.intents).sort(([left], [right]) => left.localeCompare(right))) {
12
+ if (!isRecord(intent)) {
13
+ continue;
14
+ }
15
+ intents.push({
16
+ name,
17
+ status: readString(intent, 'status') ?? 'unknown',
18
+ lifecycle: readString(intent, 'lifecycle') ?? null,
19
+ runPolicy: readString(intent, 'run_policy') ?? null,
20
+ description: readString(intent, 'description') ?? null,
21
+ effects: normalizeCommandEffects(projectRoot, contract, name),
22
+ });
23
+ }
24
+ return intents;
25
+ }
@@ -0,0 +1,7 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function sha256Text(content) {
3
+ return `sha256:${createHash('sha256').update(content).digest('hex')}`;
4
+ }
5
+ export function sha256Bytes(content) {
6
+ return `sha256:${createHash('sha256').update(content).digest('hex')}`;
7
+ }