@telora/daemon 0.22.189 → 0.22.208

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 (189) hide show
  1. package/build-info.json +6 -3
  2. package/dist/backends/index.d.ts.map +1 -1
  3. package/dist/backends/index.js +2 -0
  4. package/dist/backends/index.js.map +1 -1
  5. package/dist/completion/completion-fact-readers.d.ts +35 -0
  6. package/dist/completion/completion-fact-readers.d.ts.map +1 -0
  7. package/dist/completion/completion-fact-readers.js +70 -0
  8. package/dist/completion/completion-fact-readers.js.map +1 -0
  9. package/dist/completion/event-dep-defaults.d.ts +27 -0
  10. package/dist/completion/event-dep-defaults.d.ts.map +1 -0
  11. package/dist/completion/event-dep-defaults.js +30 -0
  12. package/dist/completion/event-dep-defaults.js.map +1 -0
  13. package/dist/completion/event-phases.d.ts.map +1 -1
  14. package/dist/completion/event-phases.js +5 -13
  15. package/dist/completion/event-phases.js.map +1 -1
  16. package/dist/completion/event.d.ts +32 -0
  17. package/dist/completion/event.d.ts.map +1 -1
  18. package/dist/completion/event.js +31 -25
  19. package/dist/completion/event.js.map +1 -1
  20. package/dist/completion/session-hold.d.ts +13 -1
  21. package/dist/completion/session-hold.d.ts.map +1 -1
  22. package/dist/completion/session-hold.js +18 -0
  23. package/dist/completion/session-hold.js.map +1 -1
  24. package/dist/completion/team-phase.d.ts.map +1 -1
  25. package/dist/completion/team-phase.js +1 -0
  26. package/dist/completion/team-phase.js.map +1 -1
  27. package/dist/completion/unclaimed-migration-notice-phase.d.ts +97 -0
  28. package/dist/completion/unclaimed-migration-notice-phase.d.ts.map +1 -0
  29. package/dist/completion/unclaimed-migration-notice-phase.js +270 -0
  30. package/dist/completion/unclaimed-migration-notice-phase.js.map +1 -0
  31. package/dist/directive/directive-executor.d.ts.map +1 -1
  32. package/dist/directive/directive-executor.js +1 -20
  33. package/dist/directive/directive-executor.js.map +1 -1
  34. package/dist/focus/focus-executor.d.ts +1 -0
  35. package/dist/focus/focus-executor.d.ts.map +1 -1
  36. package/dist/focus/focus-executor.js +18 -17
  37. package/dist/focus/focus-executor.js.map +1 -1
  38. package/dist/focus/focus-team-state.js +1 -1
  39. package/dist/focus/listener-spawn.d.ts.map +1 -1
  40. package/dist/focus/listener-spawn.js +5 -20
  41. package/dist/focus/listener-spawn.js.map +1 -1
  42. package/dist/focus/listener-state.d.ts.map +1 -1
  43. package/dist/focus/listener-state.js +1 -0
  44. package/dist/focus/listener-state.js.map +1 -1
  45. package/dist/focus/listener.d.ts +1 -0
  46. package/dist/focus/listener.d.ts.map +1 -1
  47. package/dist/focus/listener.js +9 -0
  48. package/dist/focus/listener.js.map +1 -1
  49. package/dist/focus/migration-claim-pass.d.ts +50 -0
  50. package/dist/focus/migration-claim-pass.d.ts.map +1 -0
  51. package/dist/focus/migration-claim-pass.js +234 -0
  52. package/dist/focus/migration-claim-pass.js.map +1 -0
  53. package/dist/focus/pending-spawn-pass-deps.d.ts.map +1 -1
  54. package/dist/focus/pending-spawn-pass-deps.js +2 -1
  55. package/dist/focus/pending-spawn-pass-deps.js.map +1 -1
  56. package/dist/focus/team-actionable-work.d.ts +21 -0
  57. package/dist/focus/team-actionable-work.d.ts.map +1 -1
  58. package/dist/focus/team-actionable-work.js +3 -0
  59. package/dist/focus/team-actionable-work.js.map +1 -1
  60. package/dist/git/types.d.ts.map +1 -1
  61. package/dist/git/types.js +2 -1
  62. package/dist/git/types.js.map +1 -1
  63. package/dist/loops/llm-client.d.ts.map +1 -1
  64. package/dist/loops/llm-client.js +11 -1
  65. package/dist/loops/llm-client.js.map +1 -1
  66. package/dist/migrations/migration-claim-lifecycle.d.ts +166 -0
  67. package/dist/migrations/migration-claim-lifecycle.d.ts.map +1 -0
  68. package/dist/migrations/migration-claim-lifecycle.js +198 -0
  69. package/dist/migrations/migration-claim-lifecycle.js.map +1 -0
  70. package/dist/migrations/migration-held-set.d.ts +171 -0
  71. package/dist/migrations/migration-held-set.d.ts.map +1 -0
  72. package/dist/migrations/migration-held-set.js +295 -0
  73. package/dist/migrations/migration-held-set.js.map +1 -0
  74. package/dist/migrations/migration-inventory.d.ts +187 -0
  75. package/dist/migrations/migration-inventory.d.ts.map +1 -0
  76. package/dist/migrations/migration-inventory.js +253 -0
  77. package/dist/migrations/migration-inventory.js.map +1 -0
  78. package/dist/migrations/migration-manifest.d.ts +114 -0
  79. package/dist/migrations/migration-manifest.d.ts.map +1 -0
  80. package/dist/migrations/migration-manifest.js +295 -0
  81. package/dist/migrations/migration-manifest.js.map +1 -0
  82. package/dist/prompt/team-prompt-base.js +1 -1
  83. package/dist/prompt-sections/migration-numbering-section.d.ts +58 -0
  84. package/dist/prompt-sections/migration-numbering-section.d.ts.map +1 -0
  85. package/dist/prompt-sections/migration-numbering-section.js +70 -0
  86. package/dist/prompt-sections/migration-numbering-section.js.map +1 -0
  87. package/dist/prompt-sections/migration-numbering-state.d.ts +30 -0
  88. package/dist/prompt-sections/migration-numbering-state.d.ts.map +1 -0
  89. package/dist/prompt-sections/migration-numbering-state.js +24 -0
  90. package/dist/prompt-sections/migration-numbering-state.js.map +1 -0
  91. package/dist/prompt-sections/unclaimed-migration-section.d.ts +38 -0
  92. package/dist/prompt-sections/unclaimed-migration-section.d.ts.map +1 -0
  93. package/dist/prompt-sections/unclaimed-migration-section.js +57 -0
  94. package/dist/prompt-sections/unclaimed-migration-section.js.map +1 -0
  95. package/dist/queries/claim-pass-focuses.d.ts +19 -0
  96. package/dist/queries/claim-pass-focuses.d.ts.map +1 -0
  97. package/dist/queries/claim-pass-focuses.js +72 -0
  98. package/dist/queries/claim-pass-focuses.js.map +1 -0
  99. package/dist/queries/daemon-connection.d.ts.map +1 -1
  100. package/dist/queries/daemon-connection.js +1 -0
  101. package/dist/queries/daemon-connection.js.map +1 -1
  102. package/dist/queries/focus-discovery.d.ts +25 -2
  103. package/dist/queries/focus-discovery.d.ts.map +1 -1
  104. package/dist/queries/focus-discovery.js +16 -9
  105. package/dist/queries/focus-discovery.js.map +1 -1
  106. package/dist/queries/focus-unclaimed-migration.d.ts +146 -0
  107. package/dist/queries/focus-unclaimed-migration.d.ts.map +1 -0
  108. package/dist/queries/focus-unclaimed-migration.js +266 -0
  109. package/dist/queries/focus-unclaimed-migration.js.map +1 -0
  110. package/dist/queries/live-focus-branches.d.ts +32 -0
  111. package/dist/queries/live-focus-branches.d.ts.map +1 -0
  112. package/dist/queries/live-focus-branches.js +66 -0
  113. package/dist/queries/live-focus-branches.js.map +1 -0
  114. package/dist/queries/migration-claims.d.ts +25 -0
  115. package/dist/queries/migration-claims.d.ts.map +1 -0
  116. package/dist/queries/migration-claims.js +79 -0
  117. package/dist/queries/migration-claims.js.map +1 -0
  118. package/dist/queries/schemas.d.ts +2 -0
  119. package/dist/queries/schemas.d.ts.map +1 -1
  120. package/dist/queries/schemas.js +6 -0
  121. package/dist/queries/schemas.js.map +1 -1
  122. package/dist/queries/shared.js +1 -1
  123. package/dist/session/session-lineage.d.ts.map +1 -1
  124. package/dist/session/session-lineage.js +3 -0
  125. package/dist/session/session-lineage.js.map +1 -1
  126. package/dist/shell/heartbeat-metadata.d.ts +48 -2
  127. package/dist/shell/heartbeat-metadata.d.ts.map +1 -1
  128. package/dist/shell/heartbeat-metadata.js +80 -3
  129. package/dist/shell/heartbeat-metadata.js.map +1 -1
  130. package/dist/shell/heartbeat.d.ts +13 -0
  131. package/dist/shell/heartbeat.d.ts.map +1 -1
  132. package/dist/shell/heartbeat.js +31 -10
  133. package/dist/shell/heartbeat.js.map +1 -1
  134. package/dist/shell/mcp-resolution.js +1 -1
  135. package/dist/shell/mcp-resolution.js.map +1 -1
  136. package/dist/shell/migration-held-set-publish.d.ts +170 -0
  137. package/dist/shell/migration-held-set-publish.d.ts.map +1 -0
  138. package/dist/shell/migration-held-set-publish.js +172 -0
  139. package/dist/shell/migration-held-set-publish.js.map +1 -0
  140. package/dist/shell/worker-pidfile.d.ts.map +1 -1
  141. package/dist/shell/worker-pidfile.js +1 -0
  142. package/dist/shell/worker-pidfile.js.map +1 -1
  143. package/dist/spawn/helpers.d.ts.map +1 -1
  144. package/dist/spawn/helpers.js +1 -0
  145. package/dist/spawn/helpers.js.map +1 -1
  146. package/dist/spawn/lead-launch.d.ts.map +1 -1
  147. package/dist/spawn/lead-launch.js +1 -0
  148. package/dist/spawn/lead-launch.js.map +1 -1
  149. package/dist/spawn/lead-process-ownership.d.ts.map +1 -1
  150. package/dist/spawn/lead-process-ownership.js +1 -0
  151. package/dist/spawn/lead-process-ownership.js.map +1 -1
  152. package/dist/spawn/process-group-kill.d.ts.map +1 -1
  153. package/dist/spawn/process-group-kill.js +1 -0
  154. package/dist/spawn/process-group-kill.js.map +1 -1
  155. package/dist/spawn/process-spawn.d.ts +18 -1
  156. package/dist/spawn/process-spawn.d.ts.map +1 -1
  157. package/dist/spawn/process-spawn.js +11 -6
  158. package/dist/spawn/process-spawn.js.map +1 -1
  159. package/dist/spawn/prompt-assembly.d.ts +115 -18
  160. package/dist/spawn/prompt-assembly.d.ts.map +1 -1
  161. package/dist/spawn/prompt-assembly.js +121 -2
  162. package/dist/spawn/prompt-assembly.js.map +1 -1
  163. package/dist/spawn/spawn-and-acknowledge.d.ts +61 -0
  164. package/dist/spawn/spawn-and-acknowledge.d.ts.map +1 -0
  165. package/dist/spawn/spawn-and-acknowledge.js +117 -0
  166. package/dist/spawn/spawn-and-acknowledge.js.map +1 -0
  167. package/dist/spawn/spawn-cooldown.js +1 -1
  168. package/dist/spawn/spawn-decision.d.ts +10 -10
  169. package/dist/spawn/spawn-decision.d.ts.map +1 -1
  170. package/dist/spawn/spawn-decision.js +33 -5
  171. package/dist/spawn/spawn-decision.js.map +1 -1
  172. package/dist/templates/claude-md.d.ts +1 -1
  173. package/dist/templates/claude-md.d.ts.map +1 -1
  174. package/dist/templates/claude-md.js +24 -0
  175. package/dist/templates/claude-md.js.map +1 -1
  176. package/dist/testing/fixtures/index.js +1 -1
  177. package/dist/testing/fixtures/migration-namespace-repo.d.ts +63 -0
  178. package/dist/testing/fixtures/migration-namespace-repo.d.ts.map +1 -0
  179. package/dist/testing/fixtures/migration-namespace-repo.js +164 -0
  180. package/dist/testing/fixtures/migration-namespace-repo.js.map +1 -0
  181. package/dist/testing/fixtures/stubborn-lead-process.d.ts.map +1 -1
  182. package/dist/testing/fixtures/stubborn-lead-process.js +1 -0
  183. package/dist/testing/fixtures/stubborn-lead-process.js.map +1 -1
  184. package/dist/types/index.js +1 -1
  185. package/dist/verify/no-own-delta-verdict.d.ts +16 -2
  186. package/dist/verify/no-own-delta-verdict.d.ts.map +1 -1
  187. package/dist/verify/no-own-delta-verdict.js +18 -2
  188. package/dist/verify/no-own-delta-verdict.js.map +1 -1
  189. package/package.json +2 -2
@@ -0,0 +1,166 @@
1
+ /**
2
+ * A claim follows its branch: what to confirm, what to release, and which files
3
+ * have no claim behind them.
4
+ *
5
+ * THE PURE DECISION, SEPARATED FROM THE PASS
6
+ * ------------------------------------------
7
+ * Everything here is a function of what was READ -- the claims on a focus, what
8
+ * its branch holds, what integration holds, and whether the focus has ended.
9
+ * Nothing here reads or writes anything. The pass (`focus/migration-claim-pass`)
10
+ * does the IO and applies these decisions, which is what lets every rule below
11
+ * be stated as a case rather than traced through a loop.
12
+ *
13
+ * WHY CONFIRM EXISTS
14
+ * ------------------
15
+ * A claim is an intent; the file that lands is the reality. Confirming records
16
+ * that the intent was realised, with the tag the FILE carries -- which may
17
+ * differ from the tag declared at claim time, because a team renames as it
18
+ * writes. Without confirmation there is no way to tell a claim whose work
19
+ * happened from one whose focus simply has not got there yet, and the release
20
+ * pass would have to guess.
21
+ *
22
+ * WHY RELEASE EXISTS, AND WHY IT IS SAFE
23
+ * --------------------------------------
24
+ * An allocator with no release path leaks a number every time work is
25
+ * abandoned, and on a dense sequential scheme a leaked number is leaked
26
+ * forever: every later claimant steps over it. So a claim on a focus that ended
27
+ * WITHOUT its file is released.
28
+ *
29
+ * THE RELEASE RULE, in one sentence: a claim is released only when its focus
30
+ * has ENDED and its file's absence is ESTABLISHED in both places a file can be
31
+ * -- the branch and integration.
32
+ *
33
+ * Each half of that has a failure mode, in opposite directions:
34
+ *
35
+ * - LEAKING. Absence from the branch is established by reading the branch AND
36
+ * by there being no branch at all: a deleted one, or a focus that never got
37
+ * one, wrote no file, which is positive evidence rather than a gap. Reading
38
+ * those as "unknown" means such a focus never releases and its numbers are
39
+ * gone for good.
40
+ * - RE-GRANTING. Absence from INTEGRATION must be established for EVERY
41
+ * claim, not only confirmed ones. A claim can be unconfirmed simply because
42
+ * the confirm pass never ran before the branch merged -- its file is on
43
+ * integration, and releasing it hands a landed migration's number out a
44
+ * second time. That is the cleanup re-creating the failure it cleans up
45
+ * after, so an unreadable integration HOLDS.
46
+ *
47
+ * WHY UN-ARMING RELEASES
48
+ * ----------------------
49
+ * An un-armed focus may be re-armed, so releasing looks premature. The trade is
50
+ * asymmetric: re-claiming costs the next team ONE MCP call, while a
51
+ * held-but-unused number on a dense sequential scheme costs every later
52
+ * claimant, permanently. The cheap side wins.
53
+ *
54
+ * WHY AN UNREADABLE BRANCH CHANGES NOTHING
55
+ * ----------------------------------------
56
+ * Every decision here is driven by evidence of PRESENCE or ABSENCE of a file.
57
+ * A branch that could not be read supplies neither. The destructive actions --
58
+ * releasing a number, clearing a record -- must never fire on an absence of
59
+ * evidence, so an unreadable reading produces an empty decision rather than a
60
+ * decision to release everything.
61
+ *
62
+ * @module migration-claim-lifecycle
63
+ */
64
+ import type { MigrationInventoryResult } from './migration-inventory.js';
65
+ /**
66
+ * What the pass could learn about a focus's branch.
67
+ *
68
+ * `noBranch` is a real and distinct case: a focus that never got a branch
69
+ * NEVER WROTE A FILE ON ONE, which is positive evidence, not a gap. Treating it
70
+ * as unknown (or skipping the focus, as the pass used to) means a claim on such
71
+ * a focus is never released and its number leaks for good.
72
+ */
73
+ export type BranchReading = MigrationInventoryResult | {
74
+ kind: 'noBranch';
75
+ };
76
+ /** One claim, as the lifecycle needs to see it. */
77
+ export interface LifecycleClaim {
78
+ migrationClaimId: string;
79
+ focusId: string | null;
80
+ ordinal: string;
81
+ /** The tag the team DECLARED at claim time. Intent; never changes. */
82
+ tag: string | null;
83
+ confirmedAt: string | null;
84
+ /**
85
+ * The tag the FILE carried at the last confirmation, or null if never
86
+ * confirmed.
87
+ *
88
+ * This is what a re-confirmation must compare against, not `tag`. A team is
89
+ * free to claim under one name and write another -- the file is the reality
90
+ * -- and `tag` is frozen intent that never catches up. Comparing the file to
91
+ * `tag` therefore finds a difference on EVERY pass forever: a write per tick,
92
+ * per product, for the life of the claim.
93
+ */
94
+ confirmedTag: string | null;
95
+ }
96
+ /** Why a focus's claims are being released. */
97
+ export type ReleaseReason = 'focus_completed' | 'focus_cancelled' | 'arm_cleared';
98
+ /** Ordinals an unclaimed-migration record ALREADY names for this focus. */
99
+ export interface RecordedUnclaimedFile {
100
+ ordinal: string;
101
+ /** The file the record named. Needed to carry an unresolved one forward. */
102
+ path: string;
103
+ }
104
+ export type RecordedUnclaimed = readonly RecordedUnclaimedFile[];
105
+ /** The focus's own state, as far as the lifecycle cares. */
106
+ export interface LifecycleFocus {
107
+ focusId: string;
108
+ /** Null while the focus is live; the reason when it has ended. */
109
+ endedBecause: ReleaseReason | null;
110
+ }
111
+ /** A claim to mark confirmed, with the tag the file actually carries. */
112
+ export interface ConfirmDecision {
113
+ migrationClaimId: string;
114
+ ordinal: string;
115
+ confirmedTag: string | null;
116
+ }
117
+ /** A claim to release, with the reason to record. */
118
+ export interface ReleaseDecision {
119
+ migrationClaimId: string;
120
+ ordinal: string;
121
+ reason: ReleaseReason;
122
+ }
123
+ /** A migration file on the branch that no live claim accounts for. */
124
+ export interface UnclaimedFile {
125
+ ordinal: string;
126
+ tag: string;
127
+ path: string;
128
+ }
129
+ /** Everything one pass over one focus decided. */
130
+ export interface LifecycleDecision {
131
+ confirm: ConfirmDecision[];
132
+ release: ReleaseDecision[];
133
+ unclaimed: UnclaimedFile[];
134
+ /**
135
+ * True only when every ordinal an existing record NAMES has been RESOLVED --
136
+ * a live claim now covers it, or the file is gone from the branch AND from
137
+ * integration. False whenever any named ordinal is unresolved or its fate
138
+ * cannot be established, so the record stands.
139
+ *
140
+ * The pass clears the record on this flag and on nothing else. Clearing
141
+ * because the detection went quiet is how a record used to disappear with
142
+ * the file still sitting on integration: the branch merged, its own file read
143
+ * as "inherited", and a record that had never been resolved reported success.
144
+ */
145
+ clearUnclaimed: boolean;
146
+ /**
147
+ * False when the branch could not be read. The pass applies NOTHING on a
148
+ * false reading -- not a confirm, not a release, not a record, not a clear.
149
+ */
150
+ readable: boolean;
151
+ }
152
+ /**
153
+ * Decide what this focus's claims and files call for.
154
+ *
155
+ * @param claims The focus's UNRELEASED claims.
156
+ * @param branch What its branch holds (the reader's result).
157
+ * @param integration What integration holds. Used to keep a confirmed claim
158
+ * whose file has merged and left the branch, and to tell a
159
+ * removed migration from a merged one.
160
+ * @param focus The focus, and whether it has ended.
161
+ * @param recorded Ordinals an existing unclaimed record already names.
162
+ * Once named, an ordinal stays named until the team
163
+ * RESOLVES it; see `decideUnclaimed`.
164
+ */
165
+ export declare function decideClaimLifecycle(claims: readonly LifecycleClaim[], branch: BranchReading, integration: MigrationInventoryResult, focus: LifecycleFocus, recorded?: RecordedUnclaimed): LifecycleDecision;
166
+ //# sourceMappingURL=migration-claim-lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-claim-lifecycle.d.ts","sourceRoot":"","sources":["../../src/migrations/migration-claim-lifecycle.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAEzE;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,wBAAwB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAE5E,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;;;;;;OASG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,+CAA+C;AAC/C,MAAM,MAAM,aAAa,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,aAAa,CAAC;AAElF,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,iBAAiB,GAAG,SAAS,qBAAqB,EAAE,CAAC;AAEjE,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,YAAY,EAAE,aAAa,GAAG,IAAI,CAAC;CACpC;AAED,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAgLD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,wBAAwB,EACrC,KAAK,EAAE,cAAc,EACrB,QAAQ,GAAE,iBAAsB,GAC/B,iBAAiB,CAuBnB"}
@@ -0,0 +1,198 @@
1
+ // PINNED-BY-BEGIN (generated by gen:pinned-by -- do not hand-edit; checked by lint:pinned-by)
2
+ // Pinned by: packages/daemon/src/focus/migration-claim-pass.test.ts,
3
+ // packages/daemon/src/migrations/migration-claim-lifecycle.test.ts,
4
+ // packages/daemon/src/queries/focus-unclaimed-migration.test.ts
5
+ // PINNED-BY-END
6
+ /** The empty decision: what an unreadable branch produces. */
7
+ function nothing(readable) {
8
+ return { confirm: [], release: [], unclaimed: [], clearUnclaimed: false, readable };
9
+ }
10
+ /**
11
+ * The tag a recorded path implies.
12
+ *
13
+ * The record stores the number and the file, not the tag -- one text serves
14
+ * both the human reading it and the lifecycle reasoning over it, and a second
15
+ * stored field is a second thing to keep in step. Reading it back off the
16
+ * filename is exact for every file the declaration's own pattern matched.
17
+ */
18
+ function tagFromRecordedPath(ordinal, path) {
19
+ const base = path.slice(path.lastIndexOf('/') + 1);
20
+ const withoutOrdinal = base.startsWith(`${ordinal}_`) ? base.slice(ordinal.length + 1) : base;
21
+ return withoutOrdinal.replace(/\.[^.]+$/, '');
22
+ }
23
+ /** Confirm every claim whose file is on the branch; release the ended ones. */
24
+ function decideClaims(claims, onBranch, onIntegration, focus, decision) {
25
+ for (const claim of claims) {
26
+ const file = onBranch.get(claim.ordinal);
27
+ if (file) {
28
+ // The file landed. Confirm with the tag the FILE carries -- a team is
29
+ // free to rename as it writes, and the file is the reality.
30
+ //
31
+ // Re-confirm only when what was RECORDED differs from what is there now.
32
+ // Comparing against `tag` (frozen intent) instead would differ forever
33
+ // for any team that renamed as it wrote, rewriting the row every tick.
34
+ if (claim.confirmedAt === null || claim.confirmedTag !== file.tag) {
35
+ decision.confirm.push({
36
+ migrationClaimId: claim.migrationClaimId,
37
+ ordinal: claim.ordinal,
38
+ confirmedTag: file.tag,
39
+ });
40
+ }
41
+ continue;
42
+ }
43
+ // No file on the branch. Only a focus that has ENDED releases anything: a
44
+ // live focus simply has not written it yet.
45
+ if (focus.endedBecause === null)
46
+ continue;
47
+ // RELEASE REQUIRES ESTABLISHED ABSENCE FROM BOTH PLACES A FILE CAN BE.
48
+ // Absence from the branch is established above. Absence from INTEGRATION
49
+ // has to be established too, and for every claim -- not only confirmed
50
+ // ones. A claim can be unconfirmed simply because the confirm pass never
51
+ // ran before the branch merged: its file is on integration, and releasing
52
+ // it would hand a landed migration's number out a second time, which is
53
+ // the cleanup re-creating the failure it cleans up after.
54
+ if (onIntegration === null)
55
+ continue; // cannot prove it is gone -- hold
56
+ if (onIntegration.has(claim.ordinal))
57
+ continue; // it merged; the number stands
58
+ decision.release.push({
59
+ migrationClaimId: claim.migrationClaimId,
60
+ ordinal: claim.ordinal,
61
+ reason: focus.endedBecause,
62
+ });
63
+ }
64
+ }
65
+ /**
66
+ * Migrations on the branch that no claim accounts for, and whether an existing
67
+ * record may be cleared.
68
+ *
69
+ * DETECTING one is judged against the UNRELEASED claims and only against files
70
+ * the branch adds beyond integration -- a migration inherited from integration
71
+ * is someone else's already-landed work, and naming it would put the whole of
72
+ * history in front of the team on every tick.
73
+ *
74
+ * KEEPING one named is a different question, and conflating the two is how the
75
+ * record used to disappear without anyone resolving it. TWO WAYS IT VANISHED:
76
+ * the branch MERGES, so the file reads as "inherited" and the detection goes
77
+ * quiet; or the branch is PRUNED after merging (crash-recovery's merged-orphan
78
+ * cleanup deletes it), so there is no branch to see the file on at all. In both
79
+ * the team neither claimed the number nor removed the file, and in both the
80
+ * file is sitting on integration.
81
+ *
82
+ * So resolution is stated positively and is the ONLY thing that clears a
83
+ * record: a live claim now covers the ordinal, or the file is gone from the
84
+ * branch AND from integration -- really removed, not merely moved. Where
85
+ * integration cannot be read, nothing is resolved, because removal cannot be
86
+ * told from a merge.
87
+ */
88
+ function decideUnclaimed(claims, onBranch, onIntegration, recorded, decision) {
89
+ const claimed = new Set(claims.map((c) => c.ordinal));
90
+ const named = new Set();
91
+ const alreadyRecorded = new Set(recorded.map((e) => e.ordinal));
92
+ for (const migration of onBranch.values()) {
93
+ if (claimed.has(migration.ordinal))
94
+ continue;
95
+ if (!alreadyRecorded.has(migration.ordinal)) {
96
+ // A NEW detection: inherited history is not this team's file.
97
+ if (onIntegration === null)
98
+ continue; // cannot tell inherited from new
99
+ if (onIntegration.has(migration.ordinal))
100
+ continue;
101
+ }
102
+ named.add(migration.ordinal);
103
+ decision.unclaimed.push({
104
+ ordinal: migration.ordinal,
105
+ tag: migration.tag,
106
+ path: migration.path,
107
+ });
108
+ }
109
+ if (recorded.length === 0)
110
+ return;
111
+ // A RECORDED ORDINAL THE BRANCH NO LONGER SHOWS IS NOT AUTOMATICALLY GONE.
112
+ // The common case is that it MERGED: the file is on integration, reads as
113
+ // inherited, and drops out of the walk above. If the caller re-renders the
114
+ // record from `unclaimed` alone, that ordinal is silently dropped from it --
115
+ // so with [0352, 0353] recorded, 0352 merged and 0353 still on the branch,
116
+ // re-recording leaves only 0353, and claiming 0353 then clears the record
117
+ // although 0352 was never claimed or removed. Unresolved state must survive
118
+ // a re-render, so it is carried here, sourced from integration (which is
119
+ // where the file now is).
120
+ for (const entry of recorded) {
121
+ const { ordinal } = entry;
122
+ if (claimed.has(ordinal) || named.has(ordinal))
123
+ continue;
124
+ const merged = onIntegration?.get(ordinal);
125
+ if (merged) {
126
+ // It MERGED. Sourced from integration, which is where the file now is.
127
+ named.add(ordinal);
128
+ decision.unclaimed.push({ ordinal: merged.ordinal, tag: merged.tag, path: merged.path });
129
+ continue;
130
+ }
131
+ if (onIntegration === null) {
132
+ // UNRESOLVABLE, and that is precisely when it must be kept. Integration
133
+ // could not be read, so "this merged" and "this was removed" are the same
134
+ // observation from here. `clearUnclaimed` already refuses to clear the
135
+ // record on this evidence -- but the caller RE-RENDERS the record from
136
+ // `unclaimed`, so an ordinal missing from that list is dropped from the
137
+ // record just the same, quietly, and only for the ordinals an outage made
138
+ // unknowable. Carried forward verbatim instead, from what the record
139
+ // itself said.
140
+ named.add(ordinal);
141
+ decision.unclaimed.push({
142
+ ordinal,
143
+ tag: tagFromRecordedPath(ordinal, entry.path),
144
+ path: entry.path,
145
+ });
146
+ continue;
147
+ }
148
+ // Integration WAS read and does not have it, and the branch does not
149
+ // either: really removed. It leaves the record.
150
+ }
151
+ decision.clearUnclaimed = recorded.every(({ ordinal }) => {
152
+ if (claimed.has(ordinal))
153
+ return true; // the team claimed it
154
+ if (named.has(ordinal))
155
+ return false; // still unresolved, still named
156
+ if (onIntegration === null)
157
+ return false; // removal cannot be told from a merge
158
+ return !onIntegration.has(ordinal); // gone from both: really removed
159
+ });
160
+ }
161
+ /**
162
+ * Decide what this focus's claims and files call for.
163
+ *
164
+ * @param claims The focus's UNRELEASED claims.
165
+ * @param branch What its branch holds (the reader's result).
166
+ * @param integration What integration holds. Used to keep a confirmed claim
167
+ * whose file has merged and left the branch, and to tell a
168
+ * removed migration from a merged one.
169
+ * @param focus The focus, and whether it has ended.
170
+ * @param recorded Ordinals an existing unclaimed record already names.
171
+ * Once named, an ordinal stays named until the team
172
+ * RESOLVES it; see `decideUnclaimed`.
173
+ */
174
+ export function decideClaimLifecycle(claims, branch, integration, focus, recorded = []) {
175
+ // A declaration problem is not a branch fact: nothing can be decided at all.
176
+ if (branch.kind === 'declarationUnreadable')
177
+ return nothing(false);
178
+ if (branch.kind === 'undeclared')
179
+ return nothing(true);
180
+ // A branch that could not be READ proves nothing about what it holds.
181
+ if (branch.kind === 'refUnreadable')
182
+ return nothing(false);
183
+ // The file is absent from the branch either because the branch was read and
184
+ // does not have it, or because there IS no branch -- a deleted one, or a
185
+ // focus that never got one. Both are established absence FROM THE BRANCH;
186
+ // neither is absence from INTEGRATION, which is asked separately.
187
+ const onBranch = branch.kind === 'read'
188
+ ? new Map(branch.inventory.migrations.map((m) => [m.ordinal, m]))
189
+ : new Map();
190
+ const onIntegration = integration.kind === 'read'
191
+ ? new Map(integration.inventory.migrations.map((m) => [m.ordinal, m]))
192
+ : null;
193
+ const decision = nothing(true);
194
+ decideClaims(claims, onBranch, onIntegration, focus, decision);
195
+ decideUnclaimed(claims, onBranch, onIntegration, recorded, decision);
196
+ return decision;
197
+ }
198
+ //# sourceMappingURL=migration-claim-lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-claim-lifecycle.js","sourceRoot":"","sources":["../../src/migrations/migration-claim-lifecycle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * What a product's migration numbers are CURRENTLY held by, across every ref
3
+ * that matters -- integration and every live focus branch.
4
+ *
5
+ * WHY THIS LIVES IN THE DAEMON AND THE GRANT DOES NOT
6
+ * ---------------------------------------------------
7
+ * The two halves of claiming a number sit on opposite sides of a boundary, and
8
+ * neither half can move:
9
+ *
10
+ * - Seeing what is held needs the REPO. It is a git-tree read across several
11
+ * refs, and only the daemon has the repository on disk. The product-api
12
+ * edge function has no git (`sessionStateHandler` says as much: "the server
13
+ * has no git access").
14
+ * - Granting a number needs the DATABASE, atomically. Two teams claiming at
15
+ * once must be separated by a constraint, not by timing, and the constraint
16
+ * is the partial unique index on `product_migration_claims`.
17
+ *
18
+ * So the held set has to cross the gap. The daemon PUBLISHES it -- per product,
19
+ * on the heartbeat, the way `metadata.verificationPostures` is published for
20
+ * the same reason ("posture is workstation-local config the API cannot itself
21
+ * derive; the daemon is the single source") -- and the edge handler grants
22
+ * against the published set plus the live claims it can see itself.
23
+ *
24
+ * THE OTHER SHAPE, AND WHY NOT IT. Serving the whole claim from the daemon was
25
+ * the alternative. It collapses: either the daemon reads-then-inserts, which is
26
+ * the two-worktrees race reborn one altitude up, or it still calls an edge
27
+ * handler to do the atomic insert -- in which case that handler exists anyway
28
+ * and the daemon is an extra hop in front of it. The decision is recorded on
29
+ * the delivery.
30
+ *
31
+ * WHY A STALE SNAPSHOT IS SAFE, AND WHERE IT IS NOT
32
+ * -------------------------------------------------
33
+ * The published set is only ever an INPUT TO THE CANDIDATE. It never decides
34
+ * anything: the unique index decides. A snapshot read a minute ago can make the
35
+ * handler propose a number that a concurrent claim just took -- and the insert
36
+ * fails, and the handler recomputes. A stale snapshot costs a retry; it cannot
37
+ * cost a collision.
38
+ *
39
+ * What it CAN cost is a number granted against branch tips that have since
40
+ * moved -- a migration landed on a sibling branch after the snapshot was taken,
41
+ * and no claim was recorded for it (a team that skipped the rule). That is why
42
+ * every reading carries `tips`: the exact sha each ref was at when it was read.
43
+ * The confirm pass re-checks a claim whose snapshot predates the tips its branch
44
+ * actually had. A snapshot with no recorded staleness is a silent read-then-write
45
+ * race wearing a different hat, so the tips are not optional decoration.
46
+ *
47
+ * WHAT AN UNREADABLE REF DOES
48
+ * ---------------------------
49
+ * It makes the whole reading INCOMPLETE, and the reading says so. A ref that
50
+ * could not be read proves nothing about what it holds, and a held set quietly
51
+ * missing one branch's numbers tells the allocator those numbers are free --
52
+ * failing open on exactly the question being asked. An incomplete reading is
53
+ * published as incomplete and the handler refuses to grant against it.
54
+ *
55
+ * A ref that does not EXIST is different and is not incompleteness: a deleted
56
+ * branch holds nothing. That distinction is the reader's (`migration-inventory`)
57
+ * and is simply carried through here.
58
+ *
59
+ * @module migration-held-set
60
+ */
61
+ import { type MigrationInventoryDeps, type JournalEntry } from './migration-inventory.js';
62
+ import { type MigrationScheme } from './migration-manifest.js';
63
+ /** A ref to read, and the tip it was at when it was read. */
64
+ export interface HeldRefReading {
65
+ /** The ref as asked for (a branch name). */
66
+ ref: string;
67
+ /** The commit the ref was at, or null when it does not exist / was unreadable. */
68
+ tip: string | null;
69
+ /** What happened: read / absent / unreadable. */
70
+ outcome: 'read' | 'absent' | 'unreadable';
71
+ /** Ordinals this ref holds. Empty for absent or unreadable. */
72
+ ordinals: string[];
73
+ /** Why it could not be read, when `outcome` is 'unreadable'. */
74
+ reason?: string;
75
+ }
76
+ /** Every number a product's refs currently hold, and how completely it is known. */
77
+ export interface MigrationHeldSet {
78
+ /** The declared scheme, carried so the grant need not re-read the declaration. */
79
+ scheme: MigrationScheme;
80
+ /**
81
+ * Every ordinal held by any ref that was read, de-duplicated and sorted.
82
+ * String sort IS ordinal sort: both schemes are zero-padded fixed width.
83
+ */
84
+ held: string[];
85
+ /** Per-ref detail, including the tip each reading was taken at. */
86
+ readings: HeldRefReading[];
87
+ /**
88
+ * False when ANY ref was unreadable. An incomplete held set must not be
89
+ * granted against: the numbers of the ref that failed would read as free.
90
+ */
91
+ complete: boolean;
92
+ /** When this reading was taken (epoch ms). */
93
+ readAt: number;
94
+ }
95
+ /** A product that declares no migration numbering, or one whose declaration is broken. */
96
+ export type MigrationHeldSetResult = {
97
+ kind: 'undeclared';
98
+ } | {
99
+ kind: 'declarationUnreadable';
100
+ reason: string;
101
+ } | {
102
+ kind: 'held';
103
+ heldSet: MigrationHeldSet;
104
+ };
105
+ /** Seams: the inventory reader's, plus the clock (so a test can pin `readAt`). */
106
+ export interface MigrationHeldSetDeps extends MigrationInventoryDeps {
107
+ now: () => number;
108
+ }
109
+ /** Real git, real declaration reader, real clock. */
110
+ export declare function defaultMigrationHeldSetDeps(): MigrationHeldSetDeps;
111
+ /**
112
+ * The ordinals a ref's JOURNAL registers, normalised to the product's width.
113
+ *
114
+ * WHY THE JOURNAL COUNTS AT ALL. On a hand-registered product the journal is
115
+ * half the record: a migration is its numbered filename AND its `idx` entry. A
116
+ * ref can register an idx whose file is not visible -- mid-rename, a file
117
+ * removed but the entry left, a journal edited ahead of the file -- and a held
118
+ * set built from filenames alone reports that number free. The allocator then
119
+ * grants it, and the product ends with two migrations at one position, which is
120
+ * the failure this whole mechanism exists to remove.
121
+ *
122
+ * WHY NORMALISATION IS REQUIRED, NOT COSMETIC. The journal records `idx` as a
123
+ * NUMBER (352) while the filename carries it zero-padded (`0352`). Unioning the
124
+ * two raw would put both `352` and `0352` in the held set -- two widths -- and
125
+ * the candidate rule refuses a product whose ordinals are not one fixed width.
126
+ * A naive union would therefore break every claim on exactly the products this
127
+ * fixes. So a journal idx is padded to the width the product uses.
128
+ *
129
+ * WHEN IT CANNOT BE NORMALISED, THE READING IS INCOMPLETE. An idx wider than
130
+ * the width, one that is not a plain number, and -- the case this argument was
131
+ * missing -- ANY idx at all when no width is established. Dropping such an
132
+ * entry silently is the fail-open; guessing a placement is worse; and using the
133
+ * raw value is worst, because it is neither dropped nor refused but published
134
+ * as though it were a number on this product. It is reported instead, and the
135
+ * caller refuses to grant against the reading rather than granting around the
136
+ * part it could not read.
137
+ *
138
+ * @param width The product's ordinal width, or null when none is established.
139
+ * See `resolveOrdinalWidth`.
140
+ */
141
+ export declare function journalOrdinals(journal: readonly JournalEntry[] | undefined, width: number | null): {
142
+ ordinals: string[];
143
+ unnormalisable: string[];
144
+ };
145
+ /**
146
+ * The width this product's ordinals are padded to, across the WHOLE reading.
147
+ *
148
+ * A PROPERTY OF THE PRODUCT, NOT OF ONE REF. It used to be derived per ref,
149
+ * from that ref's own filenames -- so a ref holding only a journal entry had no
150
+ * width, and a ref holding files a moment before the reading had one. The width
151
+ * a number is padded to cannot depend on which branch you happen to be looking
152
+ * at; it is the product's numbering convention, and it is read once for the
153
+ * whole reading.
154
+ *
155
+ * Declaration first (it fixes the width authoritatively -- no inventoried file
156
+ * can disagree with a pattern it had to match), then the files, which must all
157
+ * agree. Null when neither establishes one: a product with no declared width
158
+ * and no files. Every journal entry is then unnormalisable and the reading is
159
+ * incomplete, which is the honest answer -- the alternative was publishing a
160
+ * bare `351` and granting `352` into a four-digit namespace.
161
+ */
162
+ export declare function resolveOrdinalWidth(declaredWidth: number | null, fileOrdinals: readonly string[]): number | null;
163
+ /**
164
+ * Read every ref and fold what they hold into one set.
165
+ *
166
+ * @param repoPath The product's repo root.
167
+ * @param refs Integration plus every live focus branch. Duplicates are fine
168
+ * (a focus whose branch IS integration reads once).
169
+ */
170
+ export declare function computeMigrationHeldSet(repoPath: string, refs: readonly string[], deps?: MigrationHeldSetDeps): MigrationHeldSetResult;
171
+ //# sourceMappingURL=migration-held-set.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-held-set.d.ts","sourceRoot":"","sources":["../../src/migrations/migration-held-set.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AAEH,OAAO,EAGL,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EAElB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAErF,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,iDAAiD;IACjD,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,YAAY,CAAC;IAC1C,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,MAAM,EAAE,eAAe,CAAC;IACxB;;;OAGG;IACH,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,mEAAmE;IACnE,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,0FAA0F;AAC1F,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAEhD,kFAAkF;AAClF,MAAM,WAAW,oBAAqB,SAAQ,sBAAsB;IAClE,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,qDAAqD;AACrD,wBAAgB,2BAA2B,IAAI,oBAAoB,CAElE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,SAAS,EAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAgBlD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,YAAY,EAAE,SAAS,MAAM,EAAE,GAC9B,MAAM,GAAG,IAAI,CAIf;AA2ID;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,IAAI,GAAE,oBAAoD,GACzD,sBAAsB,CA4CxB"}