loopforge 3.9.2 → 4.0.7

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 (285) hide show
  1. package/README.md +236 -112
  2. package/dist/audit.d.ts +73 -15
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js +131 -61
  5. package/dist/audit.js.map +1 -1
  6. package/dist/canonical-state.d.ts +152 -40
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +382 -206
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +41 -12
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-governance.d.ts +0 -2
  14. package/dist/cognitive-governance.d.ts.map +1 -1
  15. package/dist/cognitive-governance.js +0 -4
  16. package/dist/cognitive-governance.js.map +1 -1
  17. package/dist/committed-round.d.ts +105 -141
  18. package/dist/committed-round.d.ts.map +1 -1
  19. package/dist/committed-round.js +249 -372
  20. package/dist/committed-round.js.map +1 -1
  21. package/dist/constraint-lifecycle.d.ts +73 -0
  22. package/dist/constraint-lifecycle.d.ts.map +1 -0
  23. package/dist/constraint-lifecycle.js +174 -0
  24. package/dist/constraint-lifecycle.js.map +1 -0
  25. package/dist/context-boundary.d.ts +83 -0
  26. package/dist/context-boundary.d.ts.map +1 -0
  27. package/dist/context-boundary.js +101 -0
  28. package/dist/context-boundary.js.map +1 -0
  29. package/dist/contract-items.d.ts +12 -8
  30. package/dist/contract-items.d.ts.map +1 -1
  31. package/dist/contract-items.js +31 -26
  32. package/dist/contract-items.js.map +1 -1
  33. package/dist/contract-requirement.d.ts +47 -0
  34. package/dist/contract-requirement.d.ts.map +1 -0
  35. package/dist/contract-requirement.js +51 -0
  36. package/dist/contract-requirement.js.map +1 -0
  37. package/dist/criterion-status.d.ts +35 -0
  38. package/dist/criterion-status.d.ts.map +1 -0
  39. package/dist/criterion-status.js +89 -0
  40. package/dist/criterion-status.js.map +1 -0
  41. package/dist/enforcement-gate.d.ts +32 -67
  42. package/dist/enforcement-gate.d.ts.map +1 -1
  43. package/dist/enforcement-gate.js +203 -439
  44. package/dist/enforcement-gate.js.map +1 -1
  45. package/dist/engine.d.ts +9 -103
  46. package/dist/engine.d.ts.map +1 -1
  47. package/dist/engine.js +28 -887
  48. package/dist/engine.js.map +1 -1
  49. package/dist/errors.d.ts +41 -0
  50. package/dist/errors.d.ts.map +1 -0
  51. package/dist/errors.js +69 -0
  52. package/dist/errors.js.map +1 -0
  53. package/dist/evidence-claims.d.ts +33 -31
  54. package/dist/evidence-claims.d.ts.map +1 -1
  55. package/dist/evidence-claims.js +58 -72
  56. package/dist/evidence-claims.js.map +1 -1
  57. package/dist/evidence-posture-view.d.ts +19 -0
  58. package/dist/evidence-posture-view.d.ts.map +1 -0
  59. package/dist/evidence-posture-view.js +116 -0
  60. package/dist/evidence-posture-view.js.map +1 -0
  61. package/dist/evidence-posture.d.ts +73 -0
  62. package/dist/evidence-posture.d.ts.map +1 -0
  63. package/dist/evidence-posture.js +281 -0
  64. package/dist/evidence-posture.js.map +1 -0
  65. package/dist/evidence-provider.d.ts +55 -37
  66. package/dist/evidence-provider.d.ts.map +1 -1
  67. package/dist/evidence-provider.js +182 -332
  68. package/dist/evidence-provider.js.map +1 -1
  69. package/dist/explain.d.ts +167 -27
  70. package/dist/explain.d.ts.map +1 -1
  71. package/dist/explain.js +373 -58
  72. package/dist/explain.js.map +1 -1
  73. package/dist/field-accounting.d.ts.map +1 -1
  74. package/dist/field-accounting.js +3 -22
  75. package/dist/field-accounting.js.map +1 -1
  76. package/dist/generate-schema.js +13 -8
  77. package/dist/generate-schema.js.map +1 -1
  78. package/dist/git-boundary.d.ts +128 -0
  79. package/dist/git-boundary.d.ts.map +1 -0
  80. package/dist/git-boundary.js +625 -0
  81. package/dist/git-boundary.js.map +1 -0
  82. package/dist/git-facts.d.ts +22 -0
  83. package/dist/git-facts.d.ts.map +1 -0
  84. package/dist/git-facts.js +29 -0
  85. package/dist/git-facts.js.map +1 -0
  86. package/dist/git-motion.d.ts +37 -0
  87. package/dist/git-motion.d.ts.map +1 -0
  88. package/dist/git-motion.js +59 -0
  89. package/dist/git-motion.js.map +1 -0
  90. package/dist/history.d.ts +44 -0
  91. package/dist/history.d.ts.map +1 -0
  92. package/dist/history.js +77 -0
  93. package/dist/history.js.map +1 -0
  94. package/dist/index.d.ts +19 -9
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +12 -4
  97. package/dist/index.js.map +1 -1
  98. package/dist/init.d.ts +16 -0
  99. package/dist/init.d.ts.map +1 -1
  100. package/dist/init.js +80 -14
  101. package/dist/init.js.map +1 -1
  102. package/dist/loop-compiler.d.ts +51 -19
  103. package/dist/loop-compiler.d.ts.map +1 -1
  104. package/dist/loop-compiler.js +423 -507
  105. package/dist/loop-compiler.js.map +1 -1
  106. package/dist/loop-store.d.ts +6 -5
  107. package/dist/loop-store.d.ts.map +1 -1
  108. package/dist/loop-store.js +153 -35
  109. package/dist/loop-store.js.map +1 -1
  110. package/dist/mcp/round-lifecycle.d.ts +285 -81
  111. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  112. package/dist/mcp/round-lifecycle.js +1629 -574
  113. package/dist/mcp/round-lifecycle.js.map +1 -1
  114. package/dist/mcp/server.d.ts +5 -0
  115. package/dist/mcp/server.d.ts.map +1 -1
  116. package/dist/mcp/server.js +61 -19
  117. package/dist/mcp/server.js.map +1 -1
  118. package/dist/mcp/session.d.ts +81 -39
  119. package/dist/mcp/session.d.ts.map +1 -1
  120. package/dist/mcp/session.js +963 -271
  121. package/dist/mcp/session.js.map +1 -1
  122. package/dist/mcp/submission-boundary.d.ts +23 -31
  123. package/dist/mcp/submission-boundary.d.ts.map +1 -1
  124. package/dist/mcp/submission-boundary.js +242 -82
  125. package/dist/mcp/submission-boundary.js.map +1 -1
  126. package/dist/mcp/submission-handle.d.ts +66 -0
  127. package/dist/mcp/submission-handle.d.ts.map +1 -0
  128. package/dist/mcp/submission-handle.js +173 -0
  129. package/dist/mcp/submission-handle.js.map +1 -0
  130. package/dist/mcp/tools.d.ts +17 -7
  131. package/dist/mcp/tools.d.ts.map +1 -1
  132. package/dist/mcp/tools.js +763 -244
  133. package/dist/mcp/tools.js.map +1 -1
  134. package/dist/open-question.d.ts +7 -0
  135. package/dist/open-question.d.ts.map +1 -0
  136. package/dist/open-question.js +14 -0
  137. package/dist/open-question.js.map +1 -0
  138. package/dist/policy-metrics.d.ts +31 -21
  139. package/dist/policy-metrics.d.ts.map +1 -1
  140. package/dist/policy-metrics.js +117 -64
  141. package/dist/policy-metrics.js.map +1 -1
  142. package/dist/policy.d.ts +15 -40
  143. package/dist/policy.d.ts.map +1 -1
  144. package/dist/policy.js +13 -13
  145. package/dist/policy.js.map +1 -1
  146. package/dist/progress.d.ts +45 -0
  147. package/dist/progress.d.ts.map +1 -0
  148. package/dist/progress.js +136 -0
  149. package/dist/progress.js.map +1 -0
  150. package/dist/prompt-assembler.d.ts +28 -9
  151. package/dist/prompt-assembler.d.ts.map +1 -1
  152. package/dist/prompt-assembler.js +592 -603
  153. package/dist/prompt-assembler.js.map +1 -1
  154. package/dist/prompt-coverage.d.ts +39 -0
  155. package/dist/prompt-coverage.d.ts.map +1 -0
  156. package/dist/prompt-coverage.js +80 -0
  157. package/dist/prompt-coverage.js.map +1 -0
  158. package/dist/prompt-freshness.d.ts +25 -0
  159. package/dist/prompt-freshness.d.ts.map +1 -0
  160. package/dist/prompt-freshness.js +88 -0
  161. package/dist/prompt-freshness.js.map +1 -0
  162. package/dist/prompt-policy.d.ts +14 -4
  163. package/dist/prompt-policy.d.ts.map +1 -1
  164. package/dist/prompt-policy.js +29 -10
  165. package/dist/prompt-policy.js.map +1 -1
  166. package/dist/protocol.d.ts +875 -419
  167. package/dist/protocol.d.ts.map +1 -1
  168. package/dist/protocol.js +73 -99
  169. package/dist/protocol.js.map +1 -1
  170. package/dist/recovery-guidance.d.ts +13 -0
  171. package/dist/recovery-guidance.d.ts.map +1 -0
  172. package/dist/recovery-guidance.js +15 -0
  173. package/dist/recovery-guidance.js.map +1 -0
  174. package/dist/recovery.d.ts +68 -0
  175. package/dist/recovery.d.ts.map +1 -0
  176. package/dist/recovery.js +234 -0
  177. package/dist/recovery.js.map +1 -0
  178. package/dist/replay.d.ts +42 -8
  179. package/dist/replay.d.ts.map +1 -1
  180. package/dist/replay.js +311 -84
  181. package/dist/replay.js.map +1 -1
  182. package/dist/result-facts.d.ts +45 -0
  183. package/dist/result-facts.d.ts.map +1 -0
  184. package/dist/result-facts.js +86 -0
  185. package/dist/result-facts.js.map +1 -0
  186. package/dist/round-contract.d.ts +38 -8
  187. package/dist/round-contract.d.ts.map +1 -1
  188. package/dist/round-contract.js +71 -31
  189. package/dist/round-contract.js.map +1 -1
  190. package/dist/round-coordinator.d.ts +45 -91
  191. package/dist/round-coordinator.d.ts.map +1 -1
  192. package/dist/round-coordinator.js +254 -271
  193. package/dist/round-coordinator.js.map +1 -1
  194. package/dist/round-driver.d.ts +56 -16
  195. package/dist/round-driver.d.ts.map +1 -1
  196. package/dist/round-driver.js +225 -39
  197. package/dist/round-driver.js.map +1 -1
  198. package/dist/round-facts.d.ts +33 -52
  199. package/dist/round-facts.d.ts.map +1 -1
  200. package/dist/round-facts.js +54 -157
  201. package/dist/round-facts.js.map +1 -1
  202. package/dist/round-guidance.d.ts +56 -0
  203. package/dist/round-guidance.d.ts.map +1 -0
  204. package/dist/round-guidance.js +161 -0
  205. package/dist/round-guidance.js.map +1 -0
  206. package/dist/round-outcome.d.ts +21 -0
  207. package/dist/round-outcome.d.ts.map +1 -0
  208. package/dist/round-outcome.js +31 -0
  209. package/dist/round-outcome.js.map +1 -0
  210. package/dist/round-transaction.d.ts +168 -21
  211. package/dist/round-transaction.d.ts.map +1 -1
  212. package/dist/round-transaction.js +856 -39
  213. package/dist/round-transaction.js.map +1 -1
  214. package/dist/self-eval.d.ts +40 -60
  215. package/dist/self-eval.d.ts.map +1 -1
  216. package/dist/self-eval.js +455 -384
  217. package/dist/self-eval.js.map +1 -1
  218. package/dist/storage.d.ts +16 -0
  219. package/dist/storage.d.ts.map +1 -1
  220. package/dist/storage.js +54 -4
  221. package/dist/storage.js.map +1 -1
  222. package/dist/subgoal-state.d.ts +2 -3
  223. package/dist/subgoal-state.d.ts.map +1 -1
  224. package/dist/subgoal-state.js +2 -3
  225. package/dist/subgoal-state.js.map +1 -1
  226. package/dist/submission-guidance.d.ts +43 -21
  227. package/dist/submission-guidance.d.ts.map +1 -1
  228. package/dist/submission-guidance.js +368 -262
  229. package/dist/submission-guidance.js.map +1 -1
  230. package/dist/submission-receipt.d.ts +16 -8
  231. package/dist/submission-receipt.d.ts.map +1 -1
  232. package/dist/submission-receipt.js +146 -51
  233. package/dist/submission-receipt.js.map +1 -1
  234. package/dist/token-utils.d.ts +47 -7
  235. package/dist/token-utils.d.ts.map +1 -1
  236. package/dist/token-utils.js +66 -15
  237. package/dist/token-utils.js.map +1 -1
  238. package/dist/transaction-identity.d.ts +9 -0
  239. package/dist/transaction-identity.d.ts.map +1 -0
  240. package/dist/transaction-identity.js +69 -0
  241. package/dist/transaction-identity.js.map +1 -0
  242. package/dist/verification-actions.d.ts +13 -0
  243. package/dist/verification-actions.d.ts.map +1 -0
  244. package/dist/verification-actions.js +44 -0
  245. package/dist/verification-actions.js.map +1 -0
  246. package/dist/verification-gate.d.ts +26 -46
  247. package/dist/verification-gate.d.ts.map +1 -1
  248. package/dist/verification-gate.js +218 -427
  249. package/dist/verification-gate.js.map +1 -1
  250. package/dist/version.d.ts +1 -1
  251. package/dist/version.js +1 -1
  252. package/dist/working-memory.d.ts +187 -0
  253. package/dist/working-memory.d.ts.map +1 -0
  254. package/dist/working-memory.js +767 -0
  255. package/dist/working-memory.js.map +1 -0
  256. package/dist/workspace-checkpoint.d.ts +88 -0
  257. package/dist/workspace-checkpoint.d.ts.map +1 -0
  258. package/dist/workspace-checkpoint.js +326 -0
  259. package/dist/workspace-checkpoint.js.map +1 -0
  260. package/dist/workspace-restore.d.ts +68 -0
  261. package/dist/workspace-restore.d.ts.map +1 -0
  262. package/dist/workspace-restore.js +604 -0
  263. package/dist/workspace-restore.js.map +1 -0
  264. package/dist/workspace.d.ts +0 -9
  265. package/dist/workspace.d.ts.map +1 -1
  266. package/dist/workspace.js +0 -17
  267. package/dist/workspace.js.map +1 -1
  268. package/loop_policy.json +5 -19
  269. package/package.json +2 -2
  270. package/skills/forge/SKILL.md +212 -0
  271. package/skills/forge/references/evidence-and-contracts.md +204 -0
  272. package/skills/forge/references/recovery-and-views.md +188 -0
  273. package/dist/cognitive-facts.d.ts +0 -30
  274. package/dist/cognitive-facts.d.ts.map +0 -1
  275. package/dist/cognitive-facts.js +0 -113
  276. package/dist/cognitive-facts.js.map +0 -1
  277. package/dist/loop-extras-parser.d.ts +0 -81
  278. package/dist/loop-extras-parser.d.ts.map +0 -1
  279. package/dist/loop-extras-parser.js +0 -213
  280. package/dist/loop-extras-parser.js.map +0 -1
  281. package/dist/loop-projection.d.ts +0 -6
  282. package/dist/loop-projection.d.ts.map +0 -1
  283. package/dist/loop-projection.js +0 -28
  284. package/dist/loop-projection.js.map +0 -1
  285. package/skills/perception/SKILL.md +0 -370
@@ -4,12 +4,16 @@
4
4
  * place that interprets their transaction envelope or the compiler's merged
5
5
  * lineage representation. Consumers receive one stable, read-only view.
6
6
  */
7
- import { GIT_OBSERVATION_SCHEMA_VERSION, PROMPT_ARTIFACT_SCHEMA_VERSION, } from "./protocol.js";
8
- import { ROUND_TRANSACTION_SCHEMA_VERSION, deriveRoundObservationDelta, parseRoundTransactionSnapshot, transactionSchemaVersionOf, } from "./round-transaction.js";
9
- import { claimedMetCriteria, claimedRemainingCriteria, effectiveOutcome, parseRoundContract } from "./self-eval.js";
7
+ import { observationVersionIssue } from "./evidence-provider.js";
8
+ import { promptContextPairIssue } from "./context-boundary.js";
9
+ import { PROMPT_ARTIFACT_SCHEMA_VERSION, isPromptContextMode, isPromptContextReason, makeLoopRoundResult, } from "./protocol.js";
10
+ import { ROUND_TRANSACTION_SCHEMA_VERSION, deriveRoundObservationDelta, isProcessResult, parseRoundTransactionSnapshot, rootBranchId, transactionSchemaVersionOf, } from "./round-transaction.js";
11
+ import { effectiveOutcome, parseRoundContract, } from "./self-eval.js";
10
12
  import { entryRound, isRecord } from "./token-utils.js";
13
+ import { transactionIdentityIssue } from "./transaction-identity.js";
11
14
  function committedAction(value) {
12
- return value === "continue" || value === "stop" || value === "backtrack"
15
+ return value === "continue" || value === "stop" ||
16
+ value === "recovery" || value === "backtrack"
13
17
  ? value
14
18
  : null;
15
19
  }
@@ -19,56 +23,11 @@ function snapshots(value) {
19
23
  function flags(value) {
20
24
  return Array.isArray(value) ? value : [];
21
25
  }
22
- function roundOutcome(value) {
23
- return value === "success" || value === "partial" ||
24
- value === "failed" || value === "blocked"
25
- ? value
26
- : null;
27
- }
28
26
  function strings(value) {
29
27
  return Array.isArray(value)
30
28
  ? value.filter((item) => typeof item === "string")
31
29
  : [];
32
30
  }
33
- function numbers(value) {
34
- return Array.isArray(value)
35
- ? value.filter((item) => typeof item === "number")
36
- : [];
37
- }
38
- function stringMap(value) {
39
- if (!isRecord(value))
40
- return {};
41
- const out = {};
42
- for (const [key, entry] of Object.entries(value)) {
43
- if (typeof entry === "string")
44
- out[key] = entry;
45
- }
46
- return out;
47
- }
48
- /** v3.8.1: decode a rollback directive's facts from either carrier —
49
- * `result.*` on a durable :feedback entry, `lineage.*` on a hydrated merged
50
- * entry. Returns null when no restore point is recorded, which is also the
51
- * signal that the record cannot serve as recovery guidance. */
52
- export function decodeBacktrackRecord(source) {
53
- const value = isRecord(source) ? source : {};
54
- const target = typeof value.backtrackTarget === "number" ? value.backtrackTarget : null;
55
- if (target === null)
56
- return null;
57
- return {
58
- target,
59
- triggerRule: typeof value.backtrackTriggerRule === "string"
60
- ? value.backtrackTriggerRule
61
- : "",
62
- failedRounds: numbers(value.backtrackFailedRounds),
63
- approaches: strings(value.backtrackApproaches),
64
- wrongAssumptions: strings(value.backtrackWrongAssumptions),
65
- skippedFiles: strings(value.backtrackSkippedFiles),
66
- skippedFingerprints: stringMap(value.backtrackSkippedFingerprints),
67
- targetGitHead: typeof value.backtrackTargetGitHead === "string"
68
- ? value.backtrackTargetGitHead
69
- : undefined,
70
- };
71
- }
72
31
  /** Normalize a raw subgoal_updates value into typed entries. Decode stays
73
32
  * tolerant of any shape (parser caps apply at ingestion; this is the
74
33
  * read-model view of what was committed). */
@@ -114,7 +73,7 @@ function evidenceIncomplete(after, deltas) {
114
73
  * feedback entry and the engine's hydrated merged entry call it, so there is
115
74
  * no second evaluation/history interpretation. */
116
75
  function viewFromSnapshot(input) {
117
- const { snapshot, result, lineage } = input;
76
+ const { snapshot, result } = input;
118
77
  if (snapshot.phase !== "committed")
119
78
  return null;
120
79
  const action = committedAction(result.action);
@@ -129,12 +88,27 @@ function viewFromSnapshot(input) {
129
88
  const beforeEvidence = snapshots(snapshot.beforeEvidence);
130
89
  const afterEvidence = snapshots(snapshot.afterEvidence);
131
90
  const observationDelta = deriveRoundObservationDelta(beforeEvidence, afterEvidence);
91
+ const contractProposal = evaluation?.round_contract
92
+ ? parseRoundContract(evaluation.round_contract) ?? null
93
+ : null;
132
94
  return {
133
95
  source: input.source,
134
96
  sourceEntry: input.sourceEntry,
135
97
  loopId,
136
98
  round,
137
99
  roundId: snapshot.roundId,
100
+ transactionId: snapshot.transactionId,
101
+ branchId: snapshot.branchId,
102
+ kind: snapshot.kind,
103
+ origin: snapshot.kind === "rollback"
104
+ ? (snapshot.recovery?.origin ?? "machine")
105
+ : "evaluation",
106
+ recovery: snapshot.recovery ?? null,
107
+ beforeCheckpoint: snapshot.beforeCheckpoint ?? null,
108
+ checkpoints: snapshot.checkpoints ?? [],
109
+ restore: snapshot.restore ?? null,
110
+ operatorAcceptance: snapshot.operatorAcceptance ?? null,
111
+ compiled: snapshot.compiled ?? null,
138
112
  sequence: input.sequence,
139
113
  attempt: snapshot.attempt,
140
114
  promptArtifact: snapshot.promptArtifact ?? null,
@@ -142,12 +116,16 @@ function viewFromSnapshot(input) {
142
116
  executionReport: evaluation?.execution_report ?? null,
143
117
  verificationFlags: flags(result.verificationFlags),
144
118
  result,
119
+ evidencePosture: result.evidencePosture ?? null,
145
120
  action,
146
- success: result.roundSuccess === true,
147
- outcome,
148
- contractProposal: evaluation?.round_contract
149
- ? parseRoundContract(evaluation.round_contract) ?? null
150
- : null,
121
+ agentSuccessClaim: result.agentSuccessClaim === true,
122
+ agentOutcome: result.agentOutcome ?? outcome,
123
+ ...(evaluation && typeof evaluation.objective_refinement === "string" &&
124
+ evaluation.objective_refinement.trim().length > 0
125
+ ? { objectiveRefinement: evaluation.objective_refinement }
126
+ : {}),
127
+ contractProposal,
128
+ contractRedirect: contractProposal?.supersedes ?? null,
151
129
  contractBinding: snapshot.contractBinding ?? null,
152
130
  beforeEvidence,
153
131
  afterEvidence,
@@ -158,13 +136,17 @@ function viewFromSnapshot(input) {
158
136
  outputSummary: evaluation?.output_summary,
159
137
  constraintViolations: strings(evaluation?.constraint_violations),
160
138
  discoveredConstraints: strings(evaluation?.discovered_constraints),
161
- activeConstraints: strings(lineage?.constraints_active),
139
+ activeConstraints: snapshot.compiled
140
+ ? [...snapshot.compiled.constraintsActive]
141
+ : [],
142
+ compiledRetirements: snapshot.compiled
143
+ ? [...snapshot.compiled.constraintsRetired]
144
+ : [],
162
145
  retractedConstraints: strings(evaluation?.retracted_constraints),
146
+ openQuestions: strings(evaluation?.open_questions),
147
+ resolvedOpenQuestionIds: strings(evaluation?.resolved_open_question_ids),
163
148
  emergedSubtasks: strings(evaluation?.emerged_subtasks),
164
149
  subgoalUpdates: subGoalUpdates(evaluation?.subgoal_updates),
165
- backtrack: action === "backtrack"
166
- ? decodeBacktrackRecord(result)
167
- : null,
168
150
  };
169
151
  }
170
152
  export function decodeCommittedRound(entry) {
@@ -181,66 +163,107 @@ export function decodeCommittedRound(entry) {
181
163
  // legacyTransactionRounds() reports the loss explicitly; it is never
182
164
  // silently decoded into a partial view.
183
165
  const snapshot = parseRoundTransactionSnapshot(transaction.snapshot);
184
- if (!snapshot)
166
+ if (!snapshot || snapshot.kind !== "evaluation")
167
+ return null;
168
+ const identityIssue = transactionIdentityIssue(entry);
169
+ if (identityIssue)
185
170
  return null;
186
171
  const resultValue = isRecord(transaction.result)
187
172
  ? transaction.result
188
173
  : snapshot.result;
189
- if (!isRecord(resultValue))
174
+ // v4.0.3/v4.0.6: a current-schema (9) committed result must carry a
175
+ // well-formed Evidence Posture and a well-formed Agent outcome. A missing or
176
+ // corrupt posture/result is a HARD break, never re-derived at read time.
177
+ if (!isRecord(resultValue) || !isProcessResult(resultValue))
190
178
  return null;
191
179
  return viewFromSnapshot({
192
180
  source: "feedback",
193
181
  sourceEntry: entry,
194
- lineage,
195
182
  snapshot,
196
183
  result: resultValue,
197
184
  sequence: typeof entry.sequence === "number" ? entry.sequence : undefined,
198
185
  });
199
186
  }
200
- /** Decode an in-memory lineage entry after the engine merged its committed
201
- * feedback. The merged entry MUST carry the embedded transaction snapshot;
202
- * the old raw top-level evaluation reconstruction is gone, so a partial
203
- * lineage entry cannot become a second history. */
204
- export function decodeMergedRound(entry) {
205
- if (!isRecord(entry))
187
+ /** v3.9.6: decode a committed rollback directive event. It is a typed event
188
+ * in the round document, never a normal feedback round. */
189
+ export function decodeRollbackEvent(entry) {
190
+ if (entry.task_type !== "rollback_directive")
206
191
  return null;
207
- const raw = entry;
208
- const lineage = isRecord(raw.loop_lineage) ? raw.loop_lineage : {};
209
- const action = committedAction(lineage.committed_action ?? raw.committed_action);
210
- if (!action)
211
- return null;
212
- const transaction = isRecord(lineage.round_transaction)
192
+ const lineage = isRecord(entry.loop_lineage) ? entry.loop_lineage : null;
193
+ const transaction = lineage && isRecord(lineage.round_transaction)
213
194
  ? lineage.round_transaction
214
- : isRecord(raw.round_transaction)
215
- ? raw.round_transaction
216
- : null;
195
+ : null;
217
196
  if (!transaction)
218
197
  return null;
219
198
  const snapshot = parseRoundTransactionSnapshot(transaction.snapshot);
220
- if (!snapshot)
199
+ if (!snapshot || snapshot.kind !== "rollback" || !snapshot.recovery)
200
+ return null;
201
+ const identityIssue = transactionIdentityIssue(entry);
202
+ if (identityIssue)
221
203
  return null;
222
204
  const resultValue = isRecord(transaction.result)
223
205
  ? transaction.result
224
206
  : snapshot.result;
225
- if (!isRecord(resultValue))
207
+ if (!isRecord(resultValue) || !isProcessResult(resultValue))
208
+ return null;
209
+ const result = resultValue;
210
+ if (result.action !== "recovery" && result.action !== "backtrack")
226
211
  return null;
227
212
  return viewFromSnapshot({
228
- source: "merged",
229
- sourceEntry: raw,
230
- lineage,
213
+ source: "rollback",
214
+ sourceEntry: entry,
231
215
  snapshot,
232
- result: resultValue,
233
- sequence: typeof lineage.sequence === "number" ? lineage.sequence : undefined,
216
+ result,
217
+ sequence: typeof entry.sequence === "number" ? entry.sequence : undefined,
234
218
  });
235
219
  }
236
220
  /** Normalize committed history: rollback directives disappear, later records
237
221
  * replace earlier records for the same logical round, and output is ascending. */
238
- /** Decode a committed round from either representation: an engine-hydrated
239
- * merged lineage entry first (decodeMergedRound), then a durable :feedback
240
- * entry. Compile-side callers used to inline this chain as a private
241
- * `committedView` helper. */
242
- export function decodeRound(entry) {
243
- return decodeMergedRound(entry) ?? decodeCommittedRound(entry);
222
+ /** v4.0.1: decode either an ordinary committed round or a rollback event.
223
+ * There is no merged-lineage representation any more: a committed round is
224
+ * exactly its durable :feedback envelope, and a rollback is its event. */
225
+ export function decodeCommittedRecord(entry) {
226
+ return decodeCommittedRound(entry) ??
227
+ decodeRollbackEvent(entry);
228
+ }
229
+ /** THE committed-view → compiler-request projection.
230
+ *
231
+ * The compiler once received these fields from the caller's mutable
232
+ * `last_round_result`, which existed on the live path and was absent on
233
+ * resume/unpause/backtrack compiles. This projection lets one committed
234
+ * fact source feed every consumer. Optional declarations are read from the
235
+ * decoded view where it exposes them. */
236
+ export function roundResultFromCommittedRound(view) {
237
+ const evaluation = view.evaluation;
238
+ return makeLoopRoundResult({
239
+ round: view.round,
240
+ agentSuccessClaim: view.agentSuccessClaim,
241
+ output_summary: evaluation?.output_summary ?? "",
242
+ constraint_violations: [...(evaluation?.constraint_violations ?? [])],
243
+ discovered_constraints: [...(view.discoveredConstraints ?? [])],
244
+ emerged_subtasks: [...(view.emergedSubtasks ?? [])],
245
+ subgoal_updates: [...(view.subgoalUpdates ?? [])],
246
+ execution_report: view.executionReport ?? undefined,
247
+ retracted_constraints: [...(view.retractedConstraints ?? [])],
248
+ wrong_assumptions: [...(evaluation?.wrong_assumptions ?? [])],
249
+ open_questions: [...(evaluation?.open_questions ?? [])],
250
+ resolved_open_question_ids: [...(evaluation?.resolved_open_question_ids ?? [])],
251
+ worker_results: [...(evaluation?.worker_results ?? [])],
252
+ subtask_boundary: evaluation?.subtask_boundary === true,
253
+ subtask_boundary_label: evaluation?.subtask_boundary_label ?? "",
254
+ agentOutcome: evaluation
255
+ ? effectiveOutcome(evaluation)
256
+ : view.agentOutcome ?? undefined,
257
+ blocker: evaluation?.blocker,
258
+ retroactiveClaims: [...(evaluation?.retroactiveClaims ?? [])],
259
+ no_change_reason: evaluation?.no_change_reason,
260
+ });
261
+ }
262
+ /** The ONE pending-directive lookup for a redo round. The directive's
263
+ * `target.redo_round` is the branch fence, so a deep restore and a
264
+ * same-round redo match the same way. */
265
+ export function pendingBacktrackRecord(entries, redoRound) {
266
+ return pendingRollbackDirective(entries, redoRound);
244
267
  }
245
268
  /** Lineage sub-object of an entry (`loop_lineage`; v3.7: the legacy
246
269
  * top-level `lineage` alias was removed), or {} when absent. */
@@ -250,188 +273,114 @@ export function entryLineage(entry) {
250
273
  const value = entry.loop_lineage;
251
274
  return isRecord(value) ? value : {};
252
275
  }
253
- /** Extract execution_report from an entry, handling both direct-field and
254
- * lineage-nested shapes. The decoded committed view wins when the entry is
255
- * a committed round. Returns null when absent. Moved from loop-compiler so
256
- * field-shape interpretation lives in this module only. */
257
- export function entryExecutionReport(entry) {
258
- const view = decodeRound(entry);
259
- if (view)
260
- return view.executionReport;
261
- const record = isRecord(entry) ? entry : {};
262
- const direct = record.execution_report;
263
- if (isRecord(direct))
264
- return direct;
265
- const nested = entryLineage(entry).execution_report;
266
- return isRecord(nested) ? nested : null;
267
- }
268
- /** Read the criteria the agent claimed met from an entry's execution_report. */
269
- export function entryCriteriaMet(entry) {
270
- const view = decodeRound(entry);
271
- if (view)
272
- return claimedMetCriteria(view.executionReport);
273
- const ev = entryExecutionReport(entry);
274
- if (!ev)
275
- return [];
276
- return rawCriterionClaims(ev, "met");
277
- }
278
- /** Read the criteria the agent declared outstanding from an entry. */
279
- export function entryCriteriaRemaining(entry) {
280
- const view = decodeRound(entry);
281
- if (view)
282
- return claimedRemainingCriteria(view.executionReport);
283
- const ev = entryExecutionReport(entry);
284
- if (!ev)
285
- return [];
286
- return rawCriterionClaims(ev, "remaining");
287
- }
288
- /** Shape-tolerant reader for a raw entry's criterion claims (the durable
289
- * shape, before the typed parse). */
290
- function rawCriterionClaims(report, outcome) {
291
- const claims = report.criterion_claims;
292
- if (!Array.isArray(claims))
293
- return [];
294
- return claims
295
- .filter(isRecord)
296
- .filter((claim) => claim.outcome === outcome && typeof claim.criterion_id === "string")
297
- .map((claim) => claim.criterion_id);
298
- }
299
- /** Read constraints_active from an entry's lineage. */
300
- export function entryActiveConstraints(entry) {
301
- const view = decodeRound(entry);
302
- if (view)
303
- return view.activeConstraints ?? [];
304
- const arr = entryLineage(entry).constraints_active;
305
- return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
306
- }
307
- /** Read retracted_constraints from an entry (direct field, else nested in
308
- * the lineage). */
309
- export function entryRetractedConstraints(entry) {
310
- const view = decodeRound(entry);
311
- if (view)
312
- return view.retractedConstraints ?? [];
313
- const record = isRecord(entry) ? entry : {};
314
- const direct = record.retracted_constraints;
315
- if (Array.isArray(direct))
316
- return direct.filter((v) => typeof v === "string");
317
- const nested = entryLineage(entry).retracted_constraints;
318
- if (Array.isArray(nested))
319
- return nested.filter((v) => typeof v === "string");
320
- return [];
321
- }
322
- /** Read emerged_subtasks from an entry (handles direct + lineage nesting). */
323
- export function entryEmergedSubtasks(entry) {
324
- const view = decodeRound(entry);
325
- if (view)
326
- return view.emergedSubtasks ?? [];
327
- const record = isRecord(entry) ? entry : {};
328
- const direct = record.emerged_subtasks;
329
- if (Array.isArray(direct))
330
- return direct.filter((v) => typeof v === "string");
331
- const nested = entryLineage(entry).emerged_subtasks;
332
- if (Array.isArray(nested))
333
- return nested.filter((v) => typeof v === "string");
334
- return [];
335
- }
336
- /** Read subgoal_updates from an entry (handles direct + lineage nesting).
337
- * v3.7.1: committed status transitions are replayed in round order by the
338
- * compiler — every derivation replays ALL committed rounds, not just the
339
- * last one. */
340
- export function entrySubGoalUpdates(entry) {
341
- const view = decodeRound(entry);
342
- if (view)
343
- return view.subgoalUpdates ?? [];
344
- const record = isRecord(entry) ? entry : {};
345
- const direct = record.subgoal_updates;
346
- if (Array.isArray(direct))
347
- return subGoalUpdates(direct);
348
- const nested = entryLineage(entry).subgoal_updates;
349
- if (Array.isArray(nested))
350
- return subGoalUpdates(nested);
351
- return [];
352
- }
353
- /** Read constraint_violations from an entry (entry-level, stored from the
354
- * previous round's last_round_result at persist time — distinct from the
355
- * evaluation-level violations on the committed view). Moved from
356
- * verification-gate so lineage-shape reads live in this module only. */
357
- export function entryViolations(entry) {
358
- const record = isRecord(entry) ? entry : {};
359
- const viols = record.constraint_violations;
360
- if (Array.isArray(viols))
361
- return viols.filter((v) => typeof v === "string");
362
- return [];
363
- }
364
- export function historyRounds(views, beforeRound = Number.POSITIVE_INFINITY) {
276
+ /** Resolve the current branch from already-decoded committed views. This is
277
+ * the branch walker itself; `resolveBranch` is only the entry-decoding
278
+ * wrapper, so a caller that already holds the views (the history loader)
279
+ * never decodes the same entries twice. */
280
+ export function resolveBranchViews(records) {
281
+ const kindOf = (view) => view.kind ?? "evaluation";
282
+ const first = records.find((view) => kindOf(view) === "evaluation") ?? records[0];
283
+ if (!first) {
284
+ return { loopId: "", branchId: "", rounds: [], directives: [], pendingDirective: null };
285
+ }
286
+ const loopId = first.loopId;
287
+ const branchOf = (view) => view.branchId ?? rootBranchId(loopId);
288
+ let branchId = rootBranchId(loopId);
289
+ let rounds = records
290
+ .filter((view) => kindOf(view) === "evaluation" && branchOf(view) === branchId)
291
+ .sort((a, b) => a.round - b.round);
292
+ const directives = [];
293
+ const seen = new Set();
294
+ for (;;) {
295
+ const candidates = records
296
+ .filter((view) => kindOf(view) === "rollback" &&
297
+ branchOf(view) === branchId &&
298
+ view.recovery !== null && view.recovery !== undefined)
299
+ .sort((a, b) => a.round - b.round ||
300
+ (a.transactionId ?? "").localeCompare(b.transactionId ?? ""));
301
+ if (candidates.length === 0)
302
+ break;
303
+ // A branch closes exactly once. If corrupt data has more, the latest one
304
+ // is the effective directive; the others remain visible in the raw log.
305
+ const directive = candidates[candidates.length - 1];
306
+ const directiveId = directive.transactionId ?? directive.roundId;
307
+ if (seen.has(directiveId))
308
+ break;
309
+ seen.add(directiveId);
310
+ const recovery = directive.recovery;
311
+ // v3.9.7: the fence is the explicit redo round, not a nominal target
312
+ // round. A before/mid checkpoint's redo round is its owning round.
313
+ rounds = rounds.filter((view) => view.round < recovery.target.redo_round);
314
+ const child = recovery.transaction_id;
315
+ const childRounds = records
316
+ .filter((view) => kindOf(view) === "evaluation" && branchOf(view) === child)
317
+ .sort((a, b) => a.round - b.round);
318
+ rounds = [...rounds, ...childRounds];
319
+ directives.push(directive);
320
+ branchId = child;
321
+ }
365
322
  const byRound = new Map();
366
- for (const view of views) {
367
- if (!view || view.action === "backtrack" || view.round >= beforeRound)
368
- continue;
323
+ for (const view of rounds)
369
324
  byRound.set(view.round, view);
325
+ rounds = [...byRound.values()].sort((a, b) => a.round - b.round);
326
+ let pendingDirective = null;
327
+ const latest = directives[directives.length - 1];
328
+ if (latest && latest.recovery) {
329
+ const child = latest.recovery.transaction_id;
330
+ const hasChildRound = records.some((view) => kindOf(view) === "evaluation" && branchOf(view) === child);
331
+ if (!hasChildRound)
332
+ pendingDirective = latest;
333
+ }
334
+ return { loopId, branchId, rounds, directives, pendingDirective };
335
+ }
336
+ /** Resolve the current branch from committed records in the raw entry view.
337
+ * Decoding happens once here; the walker itself is `resolveBranchViews`. */
338
+ export function resolveBranch(entries) {
339
+ return resolveBranchViews(entries
340
+ .map(decodeCommittedRecord)
341
+ .filter((view) => view !== null));
342
+ }
343
+ /** v3.9.6: current-branch evaluation rounds. */
344
+ export function branchRounds(entries) {
345
+ return resolveBranch(entries).rounds;
346
+ }
347
+ /** v3.9.6: rollback directive chain. */
348
+ export function branchDirectives(entries) {
349
+ return resolveBranch(entries).directives;
350
+ }
351
+ /** v3.9.6: the pending directive controlling a redo round. */
352
+ export function pendingRollbackDirective(entries, redoRound) {
353
+ const pending = resolveBranch(entries).pendingDirective;
354
+ if (!pending?.recovery)
355
+ return null;
356
+ if (redoRound !== undefined && pending.recovery.target.redo_round !== redoRound) {
357
+ return null;
370
358
  }
371
- return [...byRound.values()].sort((a, b) => a.round - b.round);
359
+ return pending;
360
+ }
361
+ /** v3.9.6: machine deadlock guard. Agent-origin directives never consume it. */
362
+ export function hasCommittedMachineBacktrack(entries, currentRound) {
363
+ return branchDirectives(entries).some((view) => {
364
+ const recovery = view.recovery;
365
+ return view.origin === "machine" &&
366
+ recovery !== null && recovery !== undefined &&
367
+ recovery.target.redo_round <= currentRound &&
368
+ view.round >= currentRound;
369
+ });
372
370
  }
373
- /** v3.8.1: the ONE committed-history window every derivation reads.
374
- *
375
- * `decodeRound` (hydrated merged lineage first, then the durable feedback
376
- * entry) plus the rollback handling in `historyRounds` — so the compile path,
377
- * the projection, the live coordinator, audit and explain can no longer
378
- * disagree about which rounds are this branch's history, nor about how their
379
- * envelope was interpreted.
380
- *
381
- * `currentRound` bounds the window exactly as before (default: unbounded). It
382
- * is NOT a rollback fence: the redo re-commits the round numbers the abandoned
383
- * rounds occupied, so abandonment is temporal, not numeric, and cannot be
384
- * expressed as a bound here. */
371
+ /** v3.9.6: live history window = current branch below the round being
372
+ * compiled. `currentRound` is the round being prepared/submitted. */
385
373
  export function derivationRounds(entries, currentRound = Number.POSITIVE_INFINITY) {
386
- return historyRounds(entries.map(decodeRound), currentRound);
387
- }
388
- /** v3.8.1: the loop's durable round frontier — the session document's
389
- * `current_round`. Null when no session document is in the entry set, so
390
- * callers fail OPEN rather than inventing a bound. */
391
- export function persistedFrontier(entries) {
392
- for (const entry of entries) {
393
- if (!isRecord(entry) || entry.task_type !== "session_state")
394
- continue;
395
- const round = entryLineage(entry).current_round;
396
- return typeof round === "number" && Number.isFinite(round) && round >= 1
397
- ? round
398
- : null;
399
- }
400
- return null;
374
+ return branchRounds(entries).filter((view) => view.round < currentRound);
401
375
  }
402
- /** v3.8.1: THE window for a READ-ONLY view of a loop — audit, explain, replay,
403
- * the projection, the metrics replay. Same decoder and same rollback handling
404
- * as `derivationRounds`, plus the bound those callers cannot compute
405
- * themselves.
406
- *
407
- * The live paths bound their window at the round being compiled, and that
408
- * bound is what keeps a backtracked branch out of this branch's history. A
409
- * read-only view has no compile round, so it reads the SAME frontier from the
410
- * durable session document and applies it only while a committed rollback
411
- * directive is still in effect — a directive stops being history the moment
412
- * the redo commit replaces its round's record, which is exactly when the
413
- * abandoned rounds have been re-committed and must count again.
414
- *
415
- * With no directive in effect the window stays UNBOUNDED on purpose: after a
416
- * stop, terminate or max_rounds the session's `current_round` sits ON the
417
- * round it just committed, so bounding there would silently drop the loop's
418
- * final round. An unobservable frontier fails open the same way — a read-only
419
- * view never invents a fence, it only applies the one the runtime recorded. */
376
+ /** v3.9.6: read-only history = current branch rounds. No session frontier,
377
+ * no numeric fence, no second cursor. */
420
378
  export function readOnlyRounds(entries) {
421
- const views = entries.map(decodeRound);
422
- const rollbackPending = views.some((view) => view?.action === "backtrack");
423
- if (!rollbackPending)
424
- return historyRounds(views);
425
- return historyRounds(views, persistedFrontier(entries) ?? Number.POSITIVE_INFINITY);
379
+ return branchRounds(entries);
426
380
  }
427
- /** The machine-evidence set that best represents a committed round: the
428
- * after-phase observations (captured post-execution), else the pre-round
429
- * baseline. The derived delta is never evidence — it is a change-set view
430
- * over these facts, not a second observation collection. */
431
- export function machineEvidenceForRound(view) {
432
- if (view.afterEvidence.length > 0)
433
- return view.afterEvidence;
434
- return view.beforeEvidence;
381
+ /** v3.9.6: read-only rollback events = committed directive chain. */
382
+ export function readOnlyRollbacks(entries) {
383
+ return branchDirectives(entries);
435
384
  }
436
385
  export function legacyTransactionRounds(entries) {
437
386
  const out = [];
@@ -459,12 +408,42 @@ export function legacyTransactionRounds(entries) {
459
408
  // silently, which is the one thing this function exists to prevent.
460
409
  const snapshot = envelope.snapshot;
461
410
  if (isRecord(snapshot.promptArtifact)) {
462
- const artifactVersion = snapshot.promptArtifact.schemaVersion;
411
+ const artifact = snapshot.promptArtifact;
412
+ const artifactVersion = artifact.schemaVersion;
463
413
  if (typeof artifactVersion === "number" &&
464
414
  artifactVersion !== PROMPT_ARTIFACT_SCHEMA_VERSION) {
465
415
  out.push({ round, schemaVersion: artifactVersion, envelope: "prompt_artifact" });
466
416
  continue;
467
417
  }
418
+ // v4.0.5: the same runtime whitelists the decoder uses. A malformed
419
+ // optional invocation instruction is a legacy artifact, not accepted
420
+ // silently.
421
+ const validInstruction = artifact.currentUserInstruction === undefined ||
422
+ (typeof artifact.currentUserInstruction === "string" &&
423
+ artifact.currentUserInstruction.trim().length > 0);
424
+ const validContextFields = (artifact.level === "l0" || artifact.level === "l1" || artifact.level === "l2") &&
425
+ isPromptContextMode(artifact.contextMode) &&
426
+ isPromptContextReason(artifact.contextReason) &&
427
+ validInstruction &&
428
+ typeof artifact.attempt === "number" &&
429
+ typeof artifact.round === "number";
430
+ const contextIssue = validContextFields
431
+ ? promptContextPairIssue({
432
+ level: artifact.level,
433
+ mode: artifact.contextMode,
434
+ reason: artifact.contextReason,
435
+ attempt: artifact.attempt,
436
+ round: artifact.round,
437
+ })
438
+ : "missing or malformed context boundary fields";
439
+ if (contextIssue !== null) {
440
+ out.push({
441
+ round,
442
+ schemaVersion: typeof artifactVersion === "number" ? artifactVersion : -1,
443
+ envelope: "prompt_context",
444
+ });
445
+ continue;
446
+ }
468
447
  }
469
448
  // v3.9.1: an observation schema mismatch is also a hard break. The
470
449
  // transaction parser drops the whole round on it; report the loss here
@@ -474,16 +453,16 @@ export function legacyTransactionRounds(entries) {
474
453
  ...(Array.isArray(snapshot.afterEvidence) ? snapshot.afterEvidence : []),
475
454
  ];
476
455
  for (const item of observations) {
477
- if (!isRecord(item) || typeof item.kind !== "string") {
456
+ if (!isRecord(item)) {
478
457
  out.push({ round, schemaVersion: -1, envelope: "observation" });
479
458
  break;
480
459
  }
481
- const expected = item.kind === "git" ? GIT_OBSERVATION_SCHEMA_VERSION : 1;
482
- const observationVersion = item.schemaVersion;
483
- if (typeof observationVersion !== "number" || observationVersion !== expected) {
460
+ // v3.9.9: the SAME version judgment the transaction parser uses.
461
+ const issue = observationVersionIssue(item.kind, item.schemaVersion, item.data, item.status);
462
+ if (issue) {
484
463
  out.push({
485
464
  round,
486
- schemaVersion: typeof observationVersion === "number" ? observationVersion : -1,
465
+ schemaVersion: issue.schemaVersion,
487
466
  envelope: "observation",
488
467
  });
489
468
  break;
@@ -492,106 +471,4 @@ export function legacyTransactionRounds(entries) {
492
471
  }
493
472
  return out.sort((a, b) => a.round - b.round);
494
473
  }
495
- /** The ONE definition of machine Git motion for a single round.
496
- *
497
- * Motion is a non-empty file list, a HEAD that moved across the round, or a
498
- * HEAD that landed on the rollback's restore point — restoring the workspace
499
- * IS that round's work, and would otherwise read as a round that did
500
- * nothing. A clean worktree is not the same thing as no progress:
501
- * commit-per-slice workflows leave no file delta even though HEAD advanced.
502
- *
503
- * Observability stays the caller's concern: this answers "did this round
504
- * move", never "could the round be observed". The restore-point arm compares
505
- * short hashes, the rule `checkBacktrackGitHeadRestore` uses; the
506
- * before/after arm compares the observations themselves. */
507
- export function gitMotionSignal(input) {
508
- if (input.changedFiles !== null && input.changedFiles.length > 0)
509
- return true;
510
- const { headBefore, headAfter, restoreTargetHead } = input;
511
- if (headAfter && headBefore && headAfter !== headBefore)
512
- return true;
513
- if (headAfter && restoreTargetHead &&
514
- headAfter.slice(0, 12) === restoreTargetHead.slice(0, 12))
515
- return true;
516
- if (input.changedFiles === null)
517
- return null;
518
- // An observed empty change set is no motion only when both HEADs are known;
519
- // otherwise a commit-only slice could still have moved HEAD.
520
- if (!headBefore || !headAfter)
521
- return null;
522
- return false;
523
- }
524
- /** Git HEAD as a machine observation exposes it, or null when it is silent. */
525
- export function observedGitHead(observation) {
526
- return observation && isRecord(observation.data) &&
527
- typeof observation.data.head === "string" && observation.data.head
528
- ? observation.data.head
529
- : null;
530
- }
531
- function observedGitObservation(observation) {
532
- return observation && observation.kind === "git" && observation.status === "observed"
533
- ? observation
534
- : null;
535
- }
536
- function observedGitIn(observations) {
537
- return observations.find((observation) => observation.kind === "git" && observation.status === "observed") ?? null;
538
- }
539
- /** The in-flight round's own motion, from its derived Git delta. A Git
540
- * boundary that cannot be compared yields `null` unless HEAD movement or the
541
- * restore point independently proves motion. */
542
- export function inFlightGitMotion(deltas, restoreTargetHead) {
543
- const git = deltas.find((delta) => delta.kind === "git");
544
- if (!git)
545
- return null;
546
- const before = observedGitObservation(git.before);
547
- const after = observedGitObservation(git.after);
548
- return gitMotionSignal({
549
- changedFiles: git.changedFiles,
550
- headBefore: observedGitHead(before ?? undefined),
551
- headAfter: observedGitHead(after ?? undefined),
552
- restoreTargetHead: restoreTargetHead ?? null,
553
- });
554
- }
555
- /** Machine-observed git motion for the most recent contiguous window.
556
- * A clean worktree is not the same thing as no progress: commit-per-slice
557
- * workflows leave no file delta even though HEAD advanced. Unknown motion in
558
- * any round makes the window undecidable rather than falsely empty. */
559
- export function machineGitMotionSeries(rounds, currentRound, lookback) {
560
- if (!Number.isInteger(lookback) || lookback < 1 || currentRound <= lookback)
561
- return null;
562
- const byRound = new Map();
563
- for (const round of rounds) {
564
- if (round.round < 1 || round.round >= currentRound)
565
- continue;
566
- byRound.set(round.round, round);
567
- }
568
- const recent = [];
569
- for (let number = currentRound - lookback; number < currentRound; number++) {
570
- const round = byRound.get(number);
571
- if (!round)
572
- return null;
573
- if (!round.observationDelta.some((delta) => delta.kind === "git"))
574
- return null;
575
- recent.push(round);
576
- }
577
- const motion = [];
578
- for (const round of recent) {
579
- const git = round.observationDelta.find((delta) => delta.kind === "git");
580
- // The round's baseline: its own before observation, else the previous
581
- // round's after observation. Older Git observations do not carry a HEAD
582
- // field; their change set is still a valid motion signal.
583
- const baseline = observedGitObservation(git.before)
584
- ?? observedGitIn(byRound.get(round.round - 1)?.afterEvidence ?? []);
585
- const after = observedGitObservation(git.after);
586
- const signal = gitMotionSignal({
587
- changedFiles: git.changedFiles,
588
- headBefore: observedGitHead(baseline ?? undefined),
589
- headAfter: observedGitHead(after ?? undefined),
590
- });
591
- if (signal === null)
592
- return null;
593
- motion.push(signal);
594
- }
595
- return motion;
596
- }
597
474
  //# sourceMappingURL=committed-round.js.map