@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,505 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const { measureFreshness } = require('./freshness.js');
6
+ const { measureTreeState, sourceVersionFor } = require('./source-version.js');
7
+ const { VALID_EVIDENCE_KINDS, VALID_PROVENANCE } = require('./evidence-ledger.js');
8
+ const { resolveLocator, describeResolution } = require('./locator-resolve.js');
9
+ const { assertNoScoreFields } = require('./evidence-fields.js');
10
+ const { readPlan, resolveNeed, activeAttemptRef, declaredIntentList } = require('./retrieval-record.js');
11
+ const { freshnessEntry, sourceModeLookup } = require('./retrieval-plan.js');
12
+ const { CapabilityRouter } = require('./capability-router.js');
13
+ const { REPO_ROOT } = require('../helper/repo-root.js');
14
+
15
+ function getSourceModeOf(ctx ) {
16
+ if (ctx?.sourceModeOf) return ctx.sourceModeOf;
17
+ const router = new CapabilityRouter({ repoRoot: REPO_ROOT });
18
+ return sourceModeLookup(router.capabilities);
19
+ }
20
+
21
+ function getProviderFreshness(provider , root , ctx ) {
22
+ const cache = ctx?.providerFreshness;
23
+ if (cache && cache.has(provider)) {
24
+ return cache.get(provider);
25
+ }
26
+ const sourceModeOf = getSourceModeOf(ctx);
27
+ const entry = freshnessEntry(provider, root, sourceModeOf);
28
+ if (cache) {
29
+ cache.set(provider, entry);
30
+ }
31
+ return entry;
32
+ }
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+ const CANDIDATE_FIELDS = [
69
+ 'locator',
70
+ 'provider',
71
+ 'capability',
72
+ 'need',
73
+ 'sourceVersion',
74
+ 'rank',
75
+ 'score',
76
+ ];
77
+
78
+ /** Every field an evidence item may carry here. Anything else is refused, never dropped. */
79
+ const EVIDENCE_ITEM_FIELDS = ['kind', 'provenance', 'source', 'locator', 'content', 'taskId', 'establishes', 'observation', 'need', 'absence'];
80
+ const EVIDENCE_SOURCE_FIELDS = ['provider', 'capability'];
81
+ const EVIDENCE_LOCATOR_FIELDS = ['file', 'line', 'symbol', 'uri'];
82
+ const EVIDENCE_OBSERVATION_FIELDS = ['command', 'exitCode'];
83
+
84
+ /** Kinds that record an execution and may carry a typed observation. */
85
+ const OBSERVATION_KINDS = new Set(['test-result', 'runtime-observation']);
86
+
87
+ /** Kinds that are by definition not a direct read of the repository. FR-007's second sentence. */
88
+ const NON_REPOSITORY_KINDS = new Set(['generated-analysis', 'user-statement']);
89
+
90
+ function queryRefRefusal(taskId , repoRoot , slug = null) {
91
+ const plan = readPlan(repoRoot, taskId, slug);
92
+ const list = declaredIntentList(plan);
93
+ return `the runtime stamps it from the need. Re-run with --need <intent> (in a batch, "need": "<intent>"). Declared intents for task '${taskId}': ${list}.`;
94
+ }
95
+
96
+ /** Fields a caller might plausibly send that this boundary refuses by name, with the reason.
97
+ * A named refusal beats the generic unknown-field error: each of these would otherwise be
98
+ * silently discarded by `addEvidence`, and a silent discard reads as a successful write. */
99
+ const EVIDENCE_REFUSED_FIELDS = new Map ([
100
+ ['id', 'evidence ids are minted by the ledger — a caller-chosen id can overwrite an unrelated record'],
101
+ ['freshness', 'freshness is measured here from the working tree at write time, never asserted by the writer'],
102
+ ['supports', 'link evidence to a claim with `claim --action link`, which updates both sides; a list written here would leave the claim unaware of it'],
103
+ ['contradicts', 'link evidence to a claim with `claim --action link`, which updates both sides; a list written here would leave the claim unaware of it'],
104
+ ['stage', 'the evidence record has no stage field — the batch *is* the stage boundary, so a stage name recorded here would be dropped'],
105
+ ['status', "lifecycle status starts 'active' and changes only through `evidence --action supersede` — asserting it here would let a caller declare an item superseded without naming what replaces it"],
106
+ ['queryRef', queryRefRefusal],
107
+ ['attemptRef', (taskId ) => `it is read from the active task attempt. Claim the task with doflow task-attempt --action claim --task ${taskId}, then re-run without attemptRef.`],
108
+ ['result', 'an empty answer is recorded with --absence --need <intent> (in a batch, "absence": true), which also measures the index freshness.'],
109
+ ]);
110
+
111
+ const EVIDENCE_REFUSED_SOURCE_FIELDS = new Map ([
112
+ ['queryRef', queryRefRefusal],
113
+ ['attemptRef', (taskId ) => `it is read from the active task attempt. Claim the task with doflow task-attempt --action claim --task ${taskId}, then re-run without attemptRef.`],
114
+ ]);
115
+
116
+ /**
117
+ * Normalises a locator given either as an object or as a `file[:line]` / URI string.
118
+ * @param {Object|string} raw
119
+ * @returns {Object}
120
+ */
121
+ function parseLocator(raw ) {
122
+ if (typeof raw === 'string') {
123
+ const text = raw.trim();
124
+ if (text === '') throw new Error('locator is empty');
125
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) return { uri: text };
126
+ const withLine = text.match(/^(.+):(\d+)$/);
127
+ if (withLine) return { file: withLine[1], line: Number(withLine[2]) };
128
+ return { file: text };
129
+ }
130
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
131
+ throw new Error("locator must be an object, or a 'path/to/file[:line]' or URI string");
132
+ }
133
+ const locator = {};
134
+ for (const [key, value] of Object.entries(raw)) {
135
+ if (!EVIDENCE_LOCATOR_FIELDS.includes(key)) {
136
+ throw new Error(`unknown locator field '${key}'. Accepted: ${EVIDENCE_LOCATOR_FIELDS.join(', ')}`);
137
+ }
138
+ locator[key] = value;
139
+ }
140
+ if (locator.line !== undefined && (!Number.isInteger(locator.line) || locator.line < 1)) {
141
+ throw new Error(`locator.line must be a positive integer, got '${locator.line}'`);
142
+ }
143
+ for (const key of ['file', 'symbol', 'uri']) {
144
+ if (locator[key] !== undefined && (typeof locator[key] !== 'string' || locator[key].trim() === '')) {
145
+ throw new Error(`locator.${key} must be a non-empty string`);
146
+ }
147
+ }
148
+ if (!locator.file && !locator.uri) throw new Error('a locator needs at least a file or a uri');
149
+ return locator;
150
+ }
151
+
152
+ /**
153
+ * Validates one raw evidence item and returns the record fields the ledger will store.
154
+ *
155
+ * Throws on anything it cannot account for. Every rule here traces to FR-007 or to the
156
+ * vacuous-record family: a record with no statement and no locator asserts nothing, and storing it
157
+ * inflates the evidence count that the readiness gate then reads.
158
+ *
159
+ * @param {Object} raw
160
+ * @param {string} taskId the task the batch is being written to
161
+ * @param {string} taskId the task the batch is being written to
162
+ * @param {string} repoRoot root a relative locator resolves against
163
+ * @param {string|null} [slug] feature slug
164
+ * @returns {{kind:string, provenance:string, source:Object, locator:Object, content:(string|null)}}
165
+ */
166
+ function validateEvidenceItem(raw , taskId , repoRoot , slug = null) {
167
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('an evidence item must be a JSON object');
168
+ assertNoScoreFields(raw, '');
169
+
170
+ for (const key of Object.keys(raw)) {
171
+ if (EVIDENCE_REFUSED_FIELDS.has(key)) {
172
+ const val = EVIDENCE_REFUSED_FIELDS.get(key);
173
+ const reason = typeof val === 'function' ? val(taskId, repoRoot, slug) : val;
174
+ throw new Error(`'${key}' is not accepted here: ${reason}`);
175
+ }
176
+ if (!EVIDENCE_ITEM_FIELDS.includes(key)) {
177
+ throw new Error(`unknown evidence field '${key}'. Accepted: ${EVIDENCE_ITEM_FIELDS.join(', ')}`);
178
+ }
179
+ }
180
+
181
+ if (raw.taskId !== undefined && raw.taskId !== taskId) {
182
+ throw new Error(`item names task '${raw.taskId}' but this write targets '${taskId}' — one write, one task`);
183
+ }
184
+
185
+ const isAbsence = Boolean(raw.absence);
186
+ if (isAbsence) {
187
+ if (raw.need === undefined || raw.need === null || raw.need === '') {
188
+ const plan = readPlan(repoRoot, taskId, slug);
189
+ const list = declaredIntentList(plan);
190
+ throw new Error(`--absence needs --need <intent>: an empty answer means nothing without the query it answered. Declared intents for task '${taskId}': ${list}.`);
191
+ }
192
+ if (raw.locator !== undefined && raw.locator !== null) {
193
+ throw new Error('an absence record carries no locator: it states that the provider found nothing. Drop --locator, or record what was found as an ordinary item with --need <intent>.');
194
+ }
195
+ if (raw.provenance !== 'extracted') {
196
+ throw new Error("an absence record must be 'extracted': it is the provider's answer as you read it. Re-run with --provenance extracted.");
197
+ }
198
+ if (raw.observation !== undefined && raw.observation !== null) {
199
+ throw new Error('an absence record carries no observation: it records a lookup, not an execution. Drop the observation.');
200
+ }
201
+ }
202
+
203
+ const kinds = [...VALID_EVIDENCE_KINDS].sort().join(', ');
204
+ if (typeof raw.kind !== 'string' || raw.kind.trim() === '') throw new Error(`kind is required. Valid: ${kinds}`);
205
+ if (!VALID_EVIDENCE_KINDS.has(raw.kind)) throw new Error(`unknown evidence kind '${raw.kind}'. Valid: ${kinds}`);
206
+
207
+ const provenances = [...VALID_PROVENANCE].sort().join(', ');
208
+ if (typeof raw.provenance !== 'string' || raw.provenance.trim() === '') {
209
+ throw new Error('provenance is required and has no default here — defaulting it would file model '
210
+ + `analysis as repository fact, the one distinction FR-007 makes binding. Valid: ${provenances}`);
211
+ }
212
+ if (!VALID_PROVENANCE.has(raw.provenance)) throw new Error(`unknown provenance '${raw.provenance}'. Valid: ${provenances}`);
213
+ if (NON_REPOSITORY_KINDS.has(raw.kind) && raw.provenance === 'extracted') {
214
+ throw new Error(`kind '${raw.kind}' cannot carry provenance 'extracted': it is not a read of the `
215
+ + "repository, and labelling it as one is exactly how model analysis stops being distinguishable "
216
+ + "from fact. Use 'inferred' (derived by analysis) or 'asserted' (stated by a person).");
217
+ }
218
+
219
+ if (raw.source === undefined) {
220
+ throw new Error('source is required: which provider answered, under which capability. '
221
+ + "It has no default — 'unknown/general' is a plausible-looking answer to a question nobody asked.");
222
+ }
223
+ if (!raw.source || typeof raw.source !== 'object' || Array.isArray(raw.source)) {
224
+ throw new Error('source must be an object with provider and capability');
225
+ }
226
+ assertNoScoreFields(raw.source, 'source');
227
+ for (const key of Object.keys(raw.source)) {
228
+ if (EVIDENCE_REFUSED_SOURCE_FIELDS.has(key)) {
229
+ const val = EVIDENCE_REFUSED_SOURCE_FIELDS.get(key);
230
+ const reason = typeof val === 'function' ? val(taskId, repoRoot, slug) : val;
231
+ throw new Error(`'${key}' is not accepted here: ${reason}`);
232
+ }
233
+ if (!EVIDENCE_SOURCE_FIELDS.includes(key)) {
234
+ throw new Error(`unknown source field '${key}'. Accepted: ${EVIDENCE_SOURCE_FIELDS.join(', ')}`);
235
+ }
236
+ }
237
+ for (const key of EVIDENCE_SOURCE_FIELDS) {
238
+ if (typeof raw.source[key] !== 'string' || raw.source[key].trim() === '') {
239
+ throw new Error(`source.${key} is required and must be a non-empty string`);
240
+ }
241
+ }
242
+
243
+ const locator = (isAbsence || raw.locator === undefined) ? null : parseLocator(raw.locator);
244
+ // An item carrying an observation is anchored by the command it records — that command is what
245
+ // the next worker re-runs to check it — so the locator becomes optional there and only there.
246
+ if (!isAbsence && raw.provenance === 'extracted' && !locator && raw.observation === undefined) {
247
+ throw new Error("provenance 'extracted' requires a locator — a fact read from the repository "
248
+ + 'must name where it was read, or the next worker cannot check it and cannot tell when it goes stale');
249
+ }
250
+
251
+ // A locator that parses is not a locator that resolves (FR-004). Shape validation alone let an
252
+ // item naming line 799 of a 28-line file be recorded as support and counted by the readiness
253
+ // gate. The refusal names what the file actually offers, so the writer can correct it here rather
254
+ // than discover it at the gate.
255
+ if (!isAbsence && raw.provenance === 'extracted' && locator) {
256
+ const resolution = resolveLocator({ locator, repoRoot });
257
+ if (!resolution.resolved) {
258
+ throw new Error(`the locator does not resolve: ${describeResolution(locator, resolution)}. `
259
+ + "An 'extracted' item asserts a read of the repository, so a locator that points at nothing "
260
+ + 'records a fact nobody can check');
261
+ }
262
+ }
263
+
264
+ if (raw.content !== undefined && typeof raw.content !== 'string') throw new Error('content must be a string');
265
+ const content = typeof raw.content === 'string' && raw.content.trim() !== '' ? raw.content : null;
266
+ if (raw.provenance !== 'extracted' && !content) {
267
+ throw new Error(`provenance '${raw.provenance}' requires content — an inferred or asserted item `
268
+ + 'is the statement itself, so a record without one records nothing while still counting as evidence');
269
+ }
270
+
271
+ // Requirement bindings: which readiness-template requirements this item claims to establish.
272
+ // Declared by the writer, judged by the gate — the gate counts an item toward a requirement only
273
+ // when it is named here, so a structural item can no longer satisfy `architecture_mapped` and
274
+ // `blast_radius` at once by category alone (review R1).
275
+ let establishes = [];
276
+ if (raw.establishes !== undefined) {
277
+ const list = typeof raw.establishes === 'string' ? raw.establishes.split(',').map((s ) => s.trim()) : raw.establishes;
278
+ if (!Array.isArray(list) || list.length === 0 || list.some((r) => typeof r !== 'string' || r.trim() === '')) {
279
+ throw new Error("'establishes' must be a requirement id, a comma-separated list of them, or a non-empty array of non-empty strings");
280
+ }
281
+ establishes = list.map((s) => s.trim());
282
+ }
283
+
284
+ // Typed observation: the execution behind a test-result or runtime-observation. Command and exit
285
+ // status are the two facts the gate can actually grade; tree identity and observation time are
286
+ // measured into freshness at this same write. Only an 'extracted' item may carry one — an
287
+ // observation asserts an execution happened, and an inferred item is by definition not one.
288
+ let observation = null;
289
+ if (raw.observation !== undefined) {
290
+ if (!raw.observation || typeof raw.observation !== 'object' || Array.isArray(raw.observation)) {
291
+ throw new Error('observation must be an object with command and exitCode');
292
+ }
293
+ for (const key of Object.keys(raw.observation)) {
294
+ if (!EVIDENCE_OBSERVATION_FIELDS.includes(key)) {
295
+ throw new Error(`unknown observation field '${key}'. Accepted: ${EVIDENCE_OBSERVATION_FIELDS.join(', ')}`);
296
+ }
297
+ }
298
+ if (typeof raw.observation.command !== 'string' || raw.observation.command.trim() === '') {
299
+ throw new Error('observation.command is required: the exact command whose exit status is being recorded');
300
+ }
301
+ if (!Number.isInteger(raw.observation.exitCode) || raw.observation.exitCode < 0) {
302
+ throw new Error(`observation.exitCode must be a non-negative integer, got '${raw.observation.exitCode}'`);
303
+ }
304
+ if (!OBSERVATION_KINDS.has(raw.kind)) {
305
+ throw new Error(`kind '${raw.kind}' cannot carry an observation — only ${[...OBSERVATION_KINDS].join(', ')} record an execution`);
306
+ }
307
+ if (raw.provenance !== 'extracted') {
308
+ throw new Error(`provenance '${raw.provenance}' cannot carry an observation: an observation asserts a command `
309
+ + 'was actually run, which is a read of the world, not an inference or an assertion. '
310
+ + "Either record the execution as 'extracted', or drop the observation.");
311
+ }
312
+ observation = { command: raw.observation.command.trim(), exitCode: raw.observation.exitCode };
313
+ }
314
+ if (raw.kind === 'test-result' && raw.provenance === 'extracted' && !observation) {
315
+ throw new Error("an 'extracted' test-result requires an observation ({command, exitCode}) — a test result "
316
+ + 'without the command and exit status behind it is a claim about an execution nobody can check, '
317
+ + 'and it is exactly how a failed baseline was once accepted as a passing one');
318
+ }
319
+
320
+ let needIntent = null;
321
+ if (raw.need !== undefined) {
322
+ const list = Array.isArray(raw.need) ? raw.need : [raw.need];
323
+ const intents = [];
324
+ for (const entry of list) {
325
+ if (typeof entry !== 'string') {
326
+ throw new Error('need must be an intent string');
327
+ }
328
+ for (const token of entry.split(',')) {
329
+ const trimmed = token.trim();
330
+ if (trimmed !== '') intents.push(trimmed);
331
+ }
332
+ }
333
+ if (intents.length !== 1) {
334
+ throw new Error(`--need takes one intent on evidence --action add, got ${intents.length}. Record one write per need.`);
335
+ }
336
+ needIntent = intents[0];
337
+ }
338
+
339
+ return {
340
+ kind: raw.kind,
341
+ provenance: raw.provenance,
342
+ source: { provider: raw.source.provider, capability: raw.source.capability },
343
+ locator: isAbsence ? {} : (locator || {}),
344
+ content,
345
+ establishes,
346
+ observation,
347
+ ...(needIntent !== null ? { need: needIntent } : {}),
348
+ ...(isAbsence ? { absence: true, result: 'EMPTY' } : {}),
349
+ };
350
+ }
351
+
352
+ function shortHash(hash ) {
353
+ if (!hash) return 'absent';
354
+ const clean = hash.startsWith('sha256:') ? hash.slice('sha256:'.length) : hash;
355
+ return clean.slice(0, 7);
356
+ }
357
+
358
+ function toEvidence(
359
+ input ,
360
+ ctx
361
+ ) {
362
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
363
+ throw new Error("input must be an object with 'candidate' or 'item'");
364
+ }
365
+ if (!ctx || typeof ctx !== 'object') {
366
+ throw new Error('ctx must be an object');
367
+ }
368
+ if (!ctx.taskId || typeof ctx.taskId !== 'string' || ctx.taskId.trim() === '') {
369
+ throw new Error('taskId is required');
370
+ }
371
+
372
+ const taskId = ctx.taskId;
373
+ const root = ctx.root || process.cwd();
374
+ const treeState = ctx.treeState || measureTreeState(root);
375
+
376
+ let rawItem ;
377
+
378
+ if ('candidate' in input && input.candidate !== undefined) {
379
+ const candidate = input.candidate;
380
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
381
+ throw new Error('candidate must be an object');
382
+ }
383
+
384
+ for (const key of Object.keys(candidate)) {
385
+ if (!CANDIDATE_FIELDS.includes(key) && key !== 'absence') {
386
+ throw new Error(
387
+ `unknown candidate field '${key}'. Accepted: ${CANDIDATE_FIELDS.join(', ')}`
388
+ );
389
+ }
390
+ }
391
+
392
+ if (
393
+ candidate.sourceVersion &&
394
+ candidate.sourceVersion.fileHash !== null &&
395
+ candidate.sourceVersion.fileHash !== undefined
396
+ ) {
397
+ let loc = candidate.locator;
398
+ if (typeof loc === 'string') {
399
+ loc = parseLocator(loc);
400
+ }
401
+ const sv = sourceVersionFor(root, loc, treeState);
402
+ const measuredHash = sv.fileHash;
403
+ const searchedHash = candidate.sourceVersion.fileHash;
404
+ if (searchedHash !== measuredHash) {
405
+ const file = loc?.file || 'unknown';
406
+ throw new Error(
407
+ `the file changed between the search and this record (${file}: searched ${shortHash(searchedHash)}, now ${shortHash(measuredHash)}). Re-run the search and record the new result.`
408
+ );
409
+ }
410
+ }
411
+
412
+ const source = {};
413
+ if (candidate.provider !== undefined) source.provider = candidate.provider;
414
+ if (candidate.capability !== undefined) source.capability = candidate.capability;
415
+
416
+ rawItem = {
417
+ kind: ctx.kind,
418
+ provenance: ctx.provenance,
419
+ source,
420
+ locator: candidate.locator,
421
+ content: ctx.content,
422
+ establishes: ctx.establishes,
423
+ };
424
+ const absence = candidate.absence ?? ctx.absence;
425
+ if (absence !== undefined) rawItem.absence = absence;
426
+ if (candidate.need !== undefined) {
427
+ rawItem.need = candidate.need;
428
+ }
429
+ } else if ('item' in input && input.item !== undefined) {
430
+ rawItem = input.item;
431
+ } else {
432
+ throw new Error("toEvidence requires either 'candidate' or 'item'");
433
+ }
434
+
435
+ const validated = validateEvidenceItem(rawItem, taskId, root, ctx.slug);
436
+ const sv = sourceVersionFor(root, validated.locator, treeState);
437
+
438
+ const source = { ...validated.source };
439
+
440
+ if (validated.need) {
441
+ const plan = ctx.plan !== undefined ? ctx.plan : readPlan(root, taskId, ctx.slug);
442
+ const resolved = resolveNeed(plan, validated.need, taskId);
443
+ if (!resolved.ok) {
444
+ throw new Error(resolved.message);
445
+ }
446
+ source.queryRef = resolved.queryRef;
447
+ delete validated.need;
448
+ }
449
+
450
+ const attemptRef = ctx.attemptRef !== undefined
451
+ ? ctx.attemptRef
452
+ : activeAttemptRef(root, taskId, ctx.slug, undefined, ctx.attemptRoot);
453
+ if (attemptRef) {
454
+ source.attemptRef = attemptRef;
455
+ }
456
+
457
+ let freshness ;
458
+ if (validated.absence) {
459
+ delete validated.absence;
460
+ const absenceTreeState = ctx.absenceTreeState || treeState;
461
+ const probe = getProviderFreshness(source.provider, ctx.probeRoot || root, ctx);
462
+ let status = 'UNKNOWN';
463
+ if (probe?.state === 'FRESH' || probe?.state === 'NOT_APPLICABLE') {
464
+ status = 'FRESH';
465
+ } else if (probe?.state === 'STALE') {
466
+ status = 'STALE';
467
+ }
468
+
469
+ if (!absenceTreeState || absenceTreeState.gitCommit === null) {
470
+ status = 'UNKNOWN';
471
+ }
472
+
473
+ freshness = {
474
+ gitCommit: absenceTreeState ? absenceTreeState.gitCommit : null,
475
+ fileHash: null,
476
+ isDirty: null,
477
+ observedAt: new Date().toISOString(),
478
+ status,
479
+ };
480
+ if (absenceTreeState && absenceTreeState.gitCommit !== null && absenceTreeState.fingerprint) {
481
+ freshness.treeFingerprint = absenceTreeState.fingerprint;
482
+ }
483
+ } else {
484
+ freshness = {
485
+ ...measureFreshness(root, validated.locator, sv.gitCommit),
486
+ isDirty: sv.isDirty,
487
+ };
488
+ }
489
+
490
+ return {
491
+ ...validated,
492
+ taskId,
493
+ source,
494
+ freshness,
495
+ };
496
+ }
497
+
498
+ module.exports = {
499
+ CANDIDATE_FIELDS,
500
+ toEvidence,
501
+ parseLocator,
502
+ validateEvidenceItem,
503
+ EVIDENCE_REFUSED_FIELDS,
504
+ EVIDENCE_REFUSED_SOURCE_FIELDS,
505
+ };