@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.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.
Files changed (236) hide show
  1. package/bin/doflow.js +2 -2
  2. package/core/.antigravity-plugin/plugin.json +0 -1
  3. package/core/.claude-plugin/plugin.json +1 -1
  4. package/core/.codex-plugin/plugin.json +1 -1
  5. package/core/.plugin/plugin.json +1 -1
  6. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  7. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  8. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  9. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  10. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  11. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  12. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  13. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  14. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  15. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  16. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  17. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  18. package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
  19. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  20. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
  21. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
  22. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
  23. package/core/harnesses/gemini/hooks/session-start.sh +5 -3
  24. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
  25. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
  26. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
  27. package/core/harnesses/kiro/hooks/session-start.sh +11 -5
  28. package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
  29. package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
  30. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
  31. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  32. package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
  33. package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
  34. package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
  35. package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
  36. package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
  37. package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
  38. package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
  39. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
  40. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
  41. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
  42. package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
  43. package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
  44. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
  45. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
  46. package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
  47. package/core/registry/assets.json +19 -0
  48. package/core/registry/contracts.json +2 -2
  49. package/core/registry/harnesses.json +196 -10
  50. package/core/registry/lifecycle.json +2626 -7
  51. package/core/registry/storage.json +110 -83
  52. package/core/registry/verification.json +1 -1
  53. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
  54. package/core/shared/scripts/doflow/bin/doflow-run +10 -6
  55. package/core/shared/skills/do-design/SKILL.md +6 -6
  56. package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
  57. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
  58. package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
  59. package/core/shared/skills/do-implement/SKILL.md +3 -2
  60. package/core/shared/skills/do-plan/SKILL.md +7 -8
  61. package/package.json +4 -2
  62. package/src/adapters/antigravity/{index.ts → index.js} +141 -105
  63. package/src/adapters/claude/{index.ts → index.js} +63 -49
  64. package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
  65. package/src/adapters/codex/{agents.ts → agents.js} +24 -24
  66. package/src/adapters/codex/{config.ts → config.js} +49 -49
  67. package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
  68. package/src/adapters/codex/{index.ts → index.js} +148 -105
  69. package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
  70. package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
  71. package/src/adapters/copilot/{index.ts → index.js} +42 -42
  72. package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
  73. package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
  74. package/src/adapters/gemini/{index.ts → index.js} +96 -57
  75. package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
  76. package/src/adapters/{index.ts → index.js} +13 -13
  77. package/src/adapters/kiro/{index.ts → index.js} +49 -35
  78. package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
  79. package/src/adapters/opencode/{index.ts → index.js} +52 -52
  80. package/src/adapters/pi/hooks.js +119 -0
  81. package/src/adapters/pi/{index.ts → index.js} +64 -39
  82. package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
  83. package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
  84. package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
  85. package/src/cli/commands/{install.ts → install.js} +25 -25
  86. package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
  87. package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
  88. package/src/cli/commands/{remove.ts → remove.js} +10 -10
  89. package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
  90. package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
  91. package/src/cli/commands/{status.ts → status.js} +24 -24
  92. package/src/cli/commands/{tools.ts → tools.js} +16 -16
  93. package/src/cli/commands/{update.ts → update.js} +20 -20
  94. package/src/cli/{index.ts → index.js} +43 -31
  95. package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
  96. package/src/cli/{shared.ts → shared.js} +40 -40
  97. package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
  98. package/src/helper/{git.ts → git.js} +2 -2
  99. package/src/helper/{guardrails.ts → guardrails.js} +10 -10
  100. package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
  101. package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
  102. package/src/helper/{prompt.ts → prompt.js} +5 -5
  103. package/src/helper/{repo-root.ts → repo-root.js} +2 -2
  104. package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
  105. package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
  106. package/src/helper/{toml.ts → toml.js} +48 -48
  107. package/src/install/{backup-set.ts → backup-set.js} +16 -16
  108. package/src/install/{backup.ts → backup.js} +65 -65
  109. package/src/install/{context.ts → context.js} +15 -15
  110. package/src/install/{manifest.ts → manifest.js} +27 -27
  111. package/src/install/{mcp.ts → mcp.js} +30 -30
  112. package/src/install/{paths.ts → paths.js} +15 -15
  113. package/src/install/{targets.ts → targets.js} +10 -10
  114. package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
  115. package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
  116. package/src/lifecycle/coverage.js +57 -0
  117. package/src/lifecycle/hook-step.js +61 -0
  118. package/src/lifecycle/{index.ts → index.js} +160 -146
  119. package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
  120. package/src/lifecycle/{policies.ts → policies.js} +11 -11
  121. package/src/lifecycle/{view.ts → view.js} +28 -28
  122. package/src/registry/{index.ts → index.js} +195 -66
  123. package/src/release/build-package.js +91 -0
  124. package/src/release/changelog-section.js +70 -0
  125. package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
  126. package/src/runtime/advice/{cli.ts → cli.js} +31 -31
  127. package/src/runtime/advice/{client.ts → client.js} +44 -44
  128. package/src/runtime/advice/{config.ts → config.js} +16 -16
  129. package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
  130. package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
  131. package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
  132. package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
  133. package/src/runtime/advice/{record.ts → record.js} +19 -19
  134. package/src/runtime/advice/{registry.ts → registry.js} +16 -16
  135. package/src/runtime/advice/{strength.ts → strength.js} +7 -7
  136. package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
  137. package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
  138. package/src/runtime/candidate.js +505 -0
  139. package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
  140. package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
  141. package/src/runtime/{claims.ts → claims.js} +44 -35
  142. package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
  143. package/src/runtime/{cli.ts → cli.js} +59 -292
  144. package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
  145. package/src/runtime/context/budget.js +203 -0
  146. package/src/runtime/context/compose.js +1367 -0
  147. package/src/runtime/context/delivery-store.js +102 -0
  148. package/src/runtime/context/mandatory.js +594 -0
  149. package/src/runtime/context/pointer.js +142 -0
  150. package/src/runtime/context-pack.js +1277 -0
  151. package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
  152. package/src/runtime/evidence-fields.js +39 -0
  153. package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
  154. package/src/runtime/failure/{capture.ts → capture.js} +29 -29
  155. package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
  156. package/src/runtime/failure/{cli.ts → cli.js} +30 -30
  157. package/src/runtime/failure/{home.ts → home.js} +6 -6
  158. package/src/runtime/failure/{store.ts → store.js} +32 -32
  159. package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
  160. package/src/runtime/{freshness.ts → freshness.js} +59 -22
  161. package/src/runtime/{health.ts → health.js} +72 -72
  162. package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
  163. package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
  164. package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
  165. package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
  166. package/src/runtime/inventory/{index.ts → index.js} +36 -36
  167. package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
  168. package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
  169. package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
  170. package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
  171. package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
  172. package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
  173. package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
  174. package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
  175. package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
  176. package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
  177. package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
  178. package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
  179. package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
  180. package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
  181. package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
  182. package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
  183. package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
  184. package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
  185. package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
  186. package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
  187. package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
  188. package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
  189. package/src/runtime/{mask.ts → mask.js} +22 -22
  190. package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
  191. package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
  192. package/src/runtime/{model-router.ts → model-router.js} +20 -20
  193. package/src/runtime/{outcome.ts → outcome.js} +27 -27
  194. package/src/runtime/{reach.ts → reach.js} +21 -21
  195. package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
  196. package/src/runtime/{readiness.ts → readiness.js} +113 -43
  197. package/src/runtime/{recovery.ts → recovery.js} +15 -15
  198. package/src/runtime/{research-request.ts → research-request.js} +34 -34
  199. package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
  200. package/src/runtime/retrieval-record.js +223 -0
  201. package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
  202. package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
  203. package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
  204. package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
  205. package/src/runtime/source-version.js +289 -0
  206. package/src/runtime/storage/{cli.ts → cli.js} +9 -9
  207. package/src/runtime/storage/{engine.ts → engine.js} +37 -37
  208. package/src/runtime/storage/{registry.ts → registry.js} +19 -19
  209. package/src/runtime/storage/{roots.ts → roots.js} +8 -8
  210. package/src/runtime/storage/{rules.ts → rules.js} +26 -26
  211. package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
  212. package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
  213. package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
  214. package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
  215. package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
  216. package/src/runtime/{task-state.ts → task-state.js} +86 -21
  217. package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
  218. package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
  219. package/src/runtime/trace/{render.ts → render.js} +7 -7
  220. package/src/runtime/trace/{views.ts → views.js} +40 -40
  221. package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
  222. package/src/runtime/verification/{engine.ts → engine.js} +143 -143
  223. package/src/runtime/verification/{registry.ts → registry.js} +6 -6
  224. package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
  225. package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
  226. package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
  227. package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
  228. package/src/runtime/{worktree.ts → worktree.js} +31 -31
  229. package/src/state/{index.ts → index.js} +26 -26
  230. package/src/state/{lockfile.ts → lockfile.js} +20 -20
  231. package/src/state/{run-lock.ts → run-lock.js} +44 -44
  232. package/core/.antigravity-plugin/hooks.json +0 -39
  233. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  234. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  235. package/core/harnesses/shared/hooks/hooks.json +0 -47
  236. package/src/runtime/context-pack.ts +0 -492
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const { boundedAcquire, updateTaskState } = require('../task-state.js');
8
+ const { taskStoreDir } = require('../task-scope.js');
9
+
10
+ const MAX_DELIVERIES = 5;
11
+ const ACTUAL_UNMEASURED = 'no harness reports token usage to an external CLI; profile tokenTelemetry unverified';
12
+
13
+ function assertValidDeliveryEntry(entry ) {
14
+ if (!entry || typeof entry !== 'object') throw new Error('Delivery entry must be an object');
15
+ if (!entry.tokens || !entry.tokens.estimated) throw new Error('Delivery entry requires tokens.estimated');
16
+ if (typeof entry.tokens.estimated.value !== 'number' || entry.tokens.estimated.basis !== 'estimated:bytes/4') {
17
+ throw new Error('Delivery entry tokens.estimated must use estimated:bytes/4');
18
+ }
19
+ if (entry.tokens.actual?.basis?.startsWith('estimated:')) {
20
+ throw new Error('Delivery entry tokens.actual cannot use an estimated basis');
21
+ }
22
+ for (const expansion of entry.expansions || []) {
23
+ if (typeof expansion.reason !== 'string' || expansion.reason.trim() === '') {
24
+ throw new Error('Delivery expansion reason must not be empty');
25
+ }
26
+ }
27
+ }
28
+
29
+ function makeTokenRecord(bytes ) {
30
+ return {
31
+ estimated: { value: Math.ceil(bytes / 4), basis: 'estimated:bytes/4' },
32
+ actual: { unmeasured: ACTUAL_UNMEASURED },
33
+ };
34
+ }
35
+
36
+ function createDeliveryEntry({ pack, form, harness = null, sources = {}, expansions = [], retention = null } ) {
37
+ const items = pack.deliveryAllItems || pack.deliveryItems || [];
38
+ const digests = Object.fromEntries(items.map((item ) => [item.id, item.digest]));
39
+ const mandatoryIds = items.filter((item ) => item.cls === 'mandatory').map((item ) => item.id);
40
+ const harnessId = harness || 'unstated';
41
+ const bytes = Number(pack.size?.totalBytes || 0);
42
+ const entry = {
43
+ key: `${pack.delivery.packDigest}.${form}.${harnessId}`,
44
+ packDigest: pack.delivery.packDigest,
45
+ form,
46
+ harness,
47
+ firstAt: new Date().toISOString(),
48
+ mode: pack.mode || 'full',
49
+ budget: { maxBytes: pack.budget.maxBytes, source: pack.budget.source },
50
+ since: pack.delivery.since || null,
51
+ deltaRefused: pack.delivery.deltaRefused || null,
52
+ retention: retention || { status: 'unknown', basis: 'contextProfile.retention is unverified' },
53
+ items: digests,
54
+ mandatoryIds,
55
+ sources,
56
+ pointers: pack.pointers || [],
57
+ bytes,
58
+ tokens: makeTokenRecord(bytes),
59
+ omissions: (pack.deliveryOmissions || []).map((item ) => ({ ...item })),
60
+ expansions: [...expansions],
61
+ };
62
+ assertValidDeliveryEntry(entry);
63
+ return entry;
64
+ }
65
+
66
+ function readDeliveryEntry({ projectRoot, taskId, slug = null, packDigest, fsImpl = fs } ) {
67
+ const dir = taskStoreDir({ projectRoot, store: 'delivery', taskId, slug });
68
+ const file = path.join(dir, `${taskId}.json`);
69
+ try {
70
+ const disk = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
71
+ return (Array.isArray(disk.deliveries) ? disk.deliveries : []).find((entry ) => entry.packDigest === packDigest) || null;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ /** Persist a delivery under the same bounded lock used by task state. */
78
+ function recordDelivery({ projectRoot, taskId, slug = null, entry, fsImpl = fs, acquire = boundedAcquire, supersedesKey = null } ) {
79
+ assertValidDeliveryEntry(entry);
80
+ if (typeof taskId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(taskId) || taskId.includes('..')) {
81
+ throw new Error(`Invalid task id '${taskId}'`);
82
+ }
83
+ const dir = taskStoreDir({ projectRoot, store: 'delivery', taskId, slug });
84
+ const file = path.join(dir, `${taskId}.json`);
85
+ const written = updateTaskState({
86
+ fsImpl,
87
+ file,
88
+ acquire: (impl , target ) => acquire(impl, target, { retries: 3, waitMs: 5 }),
89
+ build(disk ) {
90
+ const deliveries = Array.isArray(disk?.deliveries) ? disk.deliveries : [];
91
+ if (deliveries.some((existing ) => existing.key === entry.key)) {
92
+ return null;
93
+ }
94
+ const current = supersedesKey ? deliveries.filter((existing ) => existing.key !== supersedesKey) : deliveries;
95
+ return { taskId, deliveries: [...current, entry].slice(-MAX_DELIVERIES) };
96
+ },
97
+ });
98
+ if (written === null) return null;
99
+ return { key: entry.key, recorded: true, reason: null };
100
+ }
101
+
102
+ module.exports = { ACTUAL_UNMEASURED, assertValidDeliveryEntry, createDeliveryEntry, makeTokenRecord, readDeliveryEntry, recordDelivery };
@@ -0,0 +1,594 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const crypto = require('node:crypto');
6
+ const {
7
+ section,
8
+ row,
9
+ joinWrapped,
10
+ parseRequirement,
11
+ } = require('../scaffold/artifacts.js');
12
+
13
+ const VALID_MANDATORY_KEYS = new Set([
14
+ 'objective',
15
+ 'acceptanceCriteria',
16
+ 'constraints',
17
+ 'verificationRequirements',
18
+ 'scopeExcluded',
19
+ 'blockingGaps',
20
+ ]);
21
+
22
+ /**
23
+ * Extracts mandatory fields and provenance from requirement.md text per IC-010.
24
+ * Pure function: text in, structure out. Never throws.
25
+ */
26
+ function extractMandatory({
27
+ text = null,
28
+ path: filePath = 'requirement.md',
29
+ maturity = null,
30
+ featureError = null,
31
+ taskStories = [],
32
+ } = {}) {
33
+ const fields = {
34
+ objective: null,
35
+ acceptanceCriteria: [],
36
+ constraints: [],
37
+ verificationRequirements: [],
38
+ scopeExcluded: [],
39
+ blockingGaps: [],
40
+ };
41
+
42
+ const provenance = {};
43
+
44
+ const fieldKeys = Object.keys(fields);
45
+
46
+ // Failure cases: feature error or artifact absent
47
+ if (featureError) {
48
+ const errorMsg = typeof featureError === 'object' && featureError !== null
49
+ ? (featureError.message || featureError.error)
50
+ : String(featureError);
51
+ for (const k of fieldKeys) {
52
+ provenance[k] = {
53
+ supplied: false,
54
+ source: 'none',
55
+ locator: null,
56
+ digest: null,
57
+ maturity: null,
58
+ reason: `no-active-feature: ${errorMsg}`,
59
+ gaps: [],
60
+ };
61
+ }
62
+ return { fields, provenance };
63
+ }
64
+
65
+ if (text === null || text === undefined) {
66
+ for (const k of fieldKeys) {
67
+ provenance[k] = {
68
+ supplied: false,
69
+ source: 'none',
70
+ locator: null,
71
+ digest: null,
72
+ maturity: null,
73
+ reason: 'artifact-absent',
74
+ gaps: [],
75
+ };
76
+ }
77
+ return { fields, provenance };
78
+ }
79
+
80
+ const rawDigest = crypto.createHash('sha256').update(Buffer.from(text, 'utf8')).digest('hex');
81
+ const digest = `sha256:${rawDigest}`;
82
+
83
+ const parsedReq = parseRequirement(text);
84
+
85
+ let resolvedMaturity = maturity ?? null;
86
+ if (resolvedMaturity === null) {
87
+ const maturityMatch = text.match(/\*\*Maturity:\*\*\s*([^·\r\n]+)/i);
88
+ resolvedMaturity = maturityMatch ? maturityMatch[1].trim() : null;
89
+ }
90
+
91
+ // 1. Objective: ## 1. Summary
92
+ const summaryBody = section(text, /Summary/i);
93
+ if (summaryBody === null) {
94
+ provenance.objective = {
95
+ supplied: false,
96
+ source: 'none',
97
+ locator: null,
98
+ digest,
99
+ maturity: resolvedMaturity,
100
+ reason: 'artifact-malformed: Summary missing',
101
+ gaps: [],
102
+ };
103
+ } else {
104
+ const trimmed = summaryBody.trim();
105
+ if (trimmed === '') {
106
+ provenance.objective = {
107
+ supplied: false,
108
+ source: 'none',
109
+ locator: null,
110
+ digest,
111
+ maturity: resolvedMaturity,
112
+ reason: 'artifact-malformed: Summary holds no items',
113
+ gaps: [],
114
+ };
115
+ } else if (/^none\b/i.test(trimmed)) {
116
+ fields.objective = null;
117
+ provenance.objective = {
118
+ supplied: true,
119
+ source: 'requirement',
120
+ locator: `${filePath}#1-summary`,
121
+ digest,
122
+ maturity: resolvedMaturity,
123
+ reason: 'none-in-artifact',
124
+ gaps: [],
125
+ };
126
+ } else {
127
+ // First paragraph, joined to one line
128
+ const firstPara = trimmed.split(/\n\s*\n/)[0].replace(/\n+/g, ' ').trim();
129
+ fields.objective = firstPara;
130
+ provenance.objective = {
131
+ supplied: true,
132
+ source: 'requirement',
133
+ locator: `${filePath}#1-summary`,
134
+ digest,
135
+ maturity: resolvedMaturity,
136
+ reason: null,
137
+ gaps: [],
138
+ };
139
+ }
140
+ }
141
+
142
+ // 2. Acceptance Criteria & Verification Requirements: ## 6. Acceptance Criteria
143
+ const acceptanceBody = section(text, /Acceptance Criteria/i);
144
+ if (acceptanceBody === null) {
145
+ const acGaps = (parsedReq?.gaps || []).filter((g ) => g.what === 'acceptance criteria');
146
+ provenance.acceptanceCriteria = {
147
+ supplied: false,
148
+ source: 'none',
149
+ locator: null,
150
+ digest,
151
+ maturity: resolvedMaturity,
152
+ reason: 'artifact-malformed: Acceptance Criteria missing',
153
+ gaps: acGaps,
154
+ };
155
+ provenance.verificationRequirements = {
156
+ supplied: false,
157
+ source: 'none',
158
+ locator: null,
159
+ digest,
160
+ maturity: resolvedMaturity,
161
+ reason: 'artifact-malformed: Acceptance Criteria missing',
162
+ gaps: acGaps,
163
+ };
164
+ } else {
165
+ const lines = joinWrapped(acceptanceBody);
166
+ const allCriteriaLines = [];
167
+ for (const l of lines) {
168
+ const match = l.match(/^-\s*\[[ xX]\]\s*(.+?)\s*$/);
169
+ if (match) allCriteriaLines.push(match[1]);
170
+ }
171
+
172
+ if (allCriteriaLines.length === 0) {
173
+ const acGaps = (parsedReq?.gaps || []).filter((g ) => g.what === 'acceptance criteria');
174
+ provenance.acceptanceCriteria = {
175
+ supplied: false,
176
+ source: 'none',
177
+ locator: null,
178
+ digest,
179
+ maturity: resolvedMaturity,
180
+ reason: 'artifact-malformed: Acceptance Criteria holds no items',
181
+ gaps: acGaps,
182
+ };
183
+ provenance.verificationRequirements = {
184
+ supplied: false,
185
+ source: 'none',
186
+ locator: null,
187
+ digest,
188
+ maturity: resolvedMaturity,
189
+ reason: 'artifact-malformed: Acceptance Criteria holds no items',
190
+ gaps: acGaps,
191
+ };
192
+ } else {
193
+ const scenarioLines = allCriteriaLines.filter((l) => /^\*\*Scenario:/i.test(l));
194
+ const nonScenarioLines = allCriteriaLines.filter((l) => !/^\*\*Scenario:/i.test(l));
195
+
196
+ let acSourceLines ;
197
+ let vrSourceLines ;
198
+
199
+ if (scenarioLines.length > 0) {
200
+ acSourceLines = scenarioLines;
201
+ vrSourceLines = nonScenarioLines;
202
+ } else {
203
+ acSourceLines = allCriteriaLines;
204
+ vrSourceLines = allCriteriaLines;
205
+ }
206
+
207
+ // Assign ids in document order, then order items for the task's stories.
208
+ const identifiedAcLines = acSourceLines.map((stmt, idx) => ({
209
+ stmt,
210
+ id: `ac:${idx + 1}`,
211
+ }));
212
+ let sortedAcLines = [...identifiedAcLines];
213
+ if (Array.isArray(taskStories) && taskStories.length > 0) {
214
+ sortedAcLines.sort((a, b) => {
215
+ const aMatch = taskStories.some((s) => a.stmt.includes(s));
216
+ const bMatch = taskStories.some((s) => b.stmt.includes(s));
217
+ return Number(bMatch) - Number(aMatch);
218
+ });
219
+ }
220
+
221
+ fields.acceptanceCriteria = sortedAcLines.map(({ stmt, id }) => {
222
+ const ids = [...new Set(stmt.match(/N?FR-\d+/g) || [])].sort();
223
+ const stories = stmt.match(/US\d+/g) || [];
224
+ return {
225
+ id,
226
+ text: stmt,
227
+ requirements: ids,
228
+ stories,
229
+ };
230
+ });
231
+
232
+ provenance.acceptanceCriteria = {
233
+ supplied: true,
234
+ source: 'requirement',
235
+ locator: `${filePath}#6-acceptance-criteria`,
236
+ digest,
237
+ maturity: resolvedMaturity,
238
+ reason: null,
239
+ gaps: [],
240
+ };
241
+
242
+ fields.verificationRequirements = vrSourceLines.map((stmt, idx) => ({
243
+ id: `vr:${idx + 1}`,
244
+ text: stmt,
245
+ }));
246
+
247
+ provenance.verificationRequirements = {
248
+ supplied: true,
249
+ source: 'requirement',
250
+ locator: `${filePath}#6-acceptance-criteria`,
251
+ digest,
252
+ maturity: resolvedMaturity,
253
+ reason: vrSourceLines.length === 0 ? 'none-in-artifact' : null,
254
+ gaps: [],
255
+ };
256
+ }
257
+ }
258
+
259
+ // 3. Constraints: ## 4. Non-Functional Requirements index
260
+ const nfrBody = section(text, /Non-Functional Requirements/i);
261
+ if (nfrBody === null) {
262
+ provenance.constraints = {
263
+ supplied: false,
264
+ source: 'none',
265
+ locator: null,
266
+ digest,
267
+ maturity: resolvedMaturity,
268
+ reason: 'artifact-malformed: Non-Functional Requirements missing',
269
+ gaps: [],
270
+ };
271
+ } else {
272
+ const trimmedNfr = nfrBody.trim();
273
+ const rows = [];
274
+ for (const l of trimmedNfr.split('\n')) {
275
+ const cells = row(l);
276
+ if (cells && cells.length >= 2) rows.push(cells);
277
+ }
278
+
279
+ const nfrRows = rows.filter((cells) => /^NFR-\d+$/i.test(cells[0].replace(/`/g, '').trim()));
280
+
281
+ if (nfrRows.length === 0) {
282
+ if (/^none\b/i.test(trimmedNfr)) {
283
+ provenance.constraints = {
284
+ supplied: true,
285
+ source: 'requirement',
286
+ locator: `${filePath}#4-non-functional-requirements`,
287
+ digest,
288
+ maturity: resolvedMaturity,
289
+ reason: 'none-in-artifact',
290
+ gaps: [],
291
+ };
292
+ } else {
293
+ provenance.constraints = {
294
+ supplied: false,
295
+ source: 'none',
296
+ locator: null,
297
+ digest,
298
+ maturity: resolvedMaturity,
299
+ reason: 'artifact-malformed: Non-Functional Requirements holds no items',
300
+ gaps: [],
301
+ };
302
+ }
303
+ } else {
304
+ // Exclude Superseded rows
305
+ const activeRows = nfrRows.filter((cells) => !cells.some((c) => /superseded/i.test(c)));
306
+ fields.constraints = activeRows.map((cells) => {
307
+ const id = cells[0].replace(/`/g, '').trim();
308
+ const constraintText = cells[1];
309
+ const kind = cells[2] || 'constraint';
310
+ return {
311
+ id,
312
+ text: constraintText,
313
+ kind,
314
+ };
315
+ });
316
+
317
+ provenance.constraints = {
318
+ supplied: true,
319
+ source: 'requirement',
320
+ locator: `${filePath}#4-non-functional-requirements`,
321
+ digest,
322
+ maturity: resolvedMaturity,
323
+ reason: null,
324
+ gaps: [],
325
+ };
326
+ }
327
+ }
328
+
329
+ // 4. Out of Scope: ## 5. Out of Scope
330
+ const scopeBody = section(text, /Out of Scope/i);
331
+ if (scopeBody === null) {
332
+ provenance.scopeExcluded = {
333
+ supplied: false,
334
+ source: 'none',
335
+ locator: null,
336
+ digest,
337
+ maturity: resolvedMaturity,
338
+ reason: 'artifact-malformed: Out of Scope missing',
339
+ gaps: [],
340
+ };
341
+ } else {
342
+ const trimmedScope = scopeBody.trim();
343
+ if (/^none\b/i.test(trimmedScope)) {
344
+ fields.scopeExcluded = [];
345
+ provenance.scopeExcluded = {
346
+ supplied: true,
347
+ source: 'requirement',
348
+ locator: `${filePath}#5-out-of-scope`,
349
+ digest,
350
+ maturity: resolvedMaturity,
351
+ reason: 'none-in-artifact',
352
+ gaps: [],
353
+ };
354
+ } else {
355
+ const bullets = trimmedScope
356
+ .split('\n')
357
+ .filter((l ) => /^[-*]\s+/.test(l.trim()))
358
+ .map((l ) => l.trim().replace(/^[-*]\s+/, ''));
359
+
360
+ if (bullets.length === 0) {
361
+ provenance.scopeExcluded = {
362
+ supplied: false,
363
+ source: 'none',
364
+ locator: null,
365
+ digest,
366
+ maturity: resolvedMaturity,
367
+ reason: 'artifact-malformed: Out of Scope holds no items',
368
+ gaps: [],
369
+ };
370
+ } else {
371
+ fields.scopeExcluded = bullets.map((b , idx ) => {
372
+ // The bold lead of each bullet, plus its owner clause when present
373
+ const leadMatch = b.match(/\*\*(.+?)\*\*/);
374
+ const ownerMatch = b.match(/\(owner:\s*([^)]+)\)|—\s*owner\s+([^\s(;]+)(?:\s*\([^)]*\))?/i);
375
+ let lead = leadMatch ? leadMatch[1] : b;
376
+ if (ownerMatch) lead += ` (owner: ${(ownerMatch[1] || ownerMatch[2]).trim()})`;
377
+ return {
378
+ id: `scope:${idx + 1}`,
379
+ text: lead,
380
+ };
381
+ });
382
+
383
+ provenance.scopeExcluded = {
384
+ supplied: true,
385
+ source: 'requirement',
386
+ locator: `${filePath}#5-out-of-scope`,
387
+ digest,
388
+ maturity: resolvedMaturity,
389
+ reason: null,
390
+ gaps: [],
391
+ };
392
+ }
393
+ }
394
+ }
395
+
396
+ // 5. Blocking Gaps: ## 7. Open Questions
397
+ const gapsBody = section(text, /Open Questions/i);
398
+ if (gapsBody === null) {
399
+ provenance.blockingGaps = {
400
+ supplied: false,
401
+ source: 'none',
402
+ locator: null,
403
+ digest,
404
+ maturity: resolvedMaturity,
405
+ reason: 'artifact-malformed: Open Questions missing',
406
+ gaps: [],
407
+ };
408
+ } else {
409
+ const trimmedGaps = gapsBody.trim();
410
+ if (/^none\b/i.test(trimmedGaps)) {
411
+ fields.blockingGaps = [];
412
+ provenance.blockingGaps = {
413
+ supplied: true,
414
+ source: 'requirement',
415
+ locator: `${filePath}#7-open-questions`,
416
+ digest,
417
+ maturity: resolvedMaturity,
418
+ reason: 'none-in-artifact',
419
+ gaps: [],
420
+ };
421
+ } else {
422
+ const bullets = trimmedGaps
423
+ .split('\n')
424
+ .filter((l ) => /^[-*]\s+/.test(l.trim()))
425
+ .map((l ) => l.trim().replace(/^[-*]\s+/, ''));
426
+
427
+ if (bullets.length === 0) {
428
+ provenance.blockingGaps = {
429
+ supplied: false,
430
+ source: 'none',
431
+ locator: null,
432
+ digest,
433
+ maturity: resolvedMaturity,
434
+ reason: 'artifact-malformed: Open Questions holds no items',
435
+ gaps: [],
436
+ };
437
+ } else {
438
+ fields.blockingGaps = bullets.map((textLine , idx ) => ({
439
+ id: `gap:${idx + 1}`,
440
+ text: textLine,
441
+ source: 'requirement',
442
+ }));
443
+
444
+ provenance.blockingGaps = {
445
+ supplied: true,
446
+ source: 'requirement',
447
+ locator: `${filePath}#7-open-questions`,
448
+ digest,
449
+ maturity: resolvedMaturity,
450
+ reason: null,
451
+ gaps: [],
452
+ };
453
+ }
454
+ }
455
+ }
456
+
457
+ return { fields, provenance };
458
+ }
459
+
460
+ /**
461
+ * Parses caller-supplied --mandatory-file JSON per IC-011.
462
+ * Validates keys, types, and produces caller items with c<n> ids.
463
+ */
464
+ function parseMandatoryFile(inputContent , inputPath = 'stdin') {
465
+ let parsed ;
466
+ try {
467
+ parsed = JSON.parse(inputContent);
468
+ } catch (err ) {
469
+ const error = new Error(`--mandatory-file is not valid JSON: ${err.message}`);
470
+ error.code = 'USAGE_ERROR';
471
+ throw error;
472
+ }
473
+
474
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
475
+ const error = new Error('--mandatory-file must be a JSON object');
476
+ error.code = 'USAGE_ERROR';
477
+ throw error;
478
+ }
479
+
480
+ for (const key of Object.keys(parsed)) {
481
+ if (!VALID_MANDATORY_KEYS.has(key)) {
482
+ const error = new Error(`--mandatory-file contains unknown key '${key}'. Valid keys: ${[...VALID_MANDATORY_KEYS].join(', ')}`);
483
+ error.code = 'USAGE_ERROR';
484
+ throw error;
485
+ }
486
+ }
487
+
488
+ const digest = `sha256:${crypto.createHash('sha256').update(Buffer.from(inputContent, 'utf8')).digest('hex')}`;
489
+ const locator = inputPath === '-' ? 'stdin' : inputPath;
490
+
491
+ const fields = {};
492
+ const provenance = {};
493
+
494
+ if ('objective' in parsed) {
495
+ if (typeof parsed.objective !== 'string') {
496
+ const error = new Error('--mandatory-file: objective must be a string');
497
+ error.code = 'USAGE_ERROR';
498
+ throw error;
499
+ }
500
+ fields.objective = parsed.objective;
501
+ provenance.objective = {
502
+ supplied: true,
503
+ source: 'caller',
504
+ locator,
505
+ digest,
506
+ maturity: null,
507
+ reason: null,
508
+ gaps: [],
509
+ };
510
+ }
511
+
512
+ const listConfigs = {
513
+ acceptanceCriteria: {
514
+ prefix: 'ac',
515
+ buildItem: (text, id) => ({ id, text, requirements: [], stories: [] }),
516
+ },
517
+ constraints: {
518
+ prefix: 'nfr',
519
+ buildItem: (text, id) => ({ id, text, kind: 'constraint' }),
520
+ },
521
+ verificationRequirements: {
522
+ prefix: 'vr',
523
+ buildItem: (text, id) => ({ id, text }),
524
+ },
525
+ scopeExcluded: {
526
+ prefix: 'scope',
527
+ buildItem: (text, id) => ({ id, text }),
528
+ },
529
+ blockingGaps: {
530
+ prefix: 'gap',
531
+ buildItem: (text, id) => ({ id, text, source: 'caller' }),
532
+ },
533
+ };
534
+
535
+ for (const [key, config] of Object.entries(listConfigs)) {
536
+ if (key in parsed) {
537
+ const val = parsed[key];
538
+ if (!Array.isArray(val) || val.some((item) => typeof item !== 'string')) {
539
+ const error = new Error(`--mandatory-file: ${key} must be an array of strings`);
540
+ error.code = 'USAGE_ERROR';
541
+ throw error;
542
+ }
543
+ if (val.length === 0) {
544
+ fields[key] = [];
545
+ provenance[key] = {
546
+ supplied: true,
547
+ source: 'caller',
548
+ locator,
549
+ digest,
550
+ maturity: null,
551
+ reason: 'caller-supplied-empty',
552
+ gaps: [],
553
+ };
554
+ } else {
555
+ fields[key] = val.map((text, idx) => config.buildItem(text, `${config.prefix}:c${idx + 1}`));
556
+ provenance[key] = {
557
+ supplied: true,
558
+ source: 'caller',
559
+ locator,
560
+ digest,
561
+ maturity: null,
562
+ reason: null,
563
+ gaps: [],
564
+ };
565
+ }
566
+ }
567
+ }
568
+
569
+ return { fields, provenance };
570
+ }
571
+
572
+ /**
573
+ * Replaces derived fields whole with caller overrides (IC-011).
574
+ * Never merges lists.
575
+ */
576
+ function applyMandatoryOverride(derived , override ) {
577
+ if (!override) return derived;
578
+ const fields = { ...derived.fields };
579
+ const provenance = { ...derived.provenance };
580
+
581
+ for (const [key, val] of Object.entries(override.fields)) {
582
+ fields[key] = val;
583
+ provenance[key] = override.provenance[key];
584
+ }
585
+
586
+ return { fields, provenance };
587
+ }
588
+
589
+ module.exports = {
590
+ VALID_MANDATORY_KEYS,
591
+ extractMandatory,
592
+ parseMandatoryFile,
593
+ applyMandatoryOverride,
594
+ };