dsh-tower 2.0.3 → 2.1.0-beta.1
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.
- package/CHANGELOG.md +141 -0
- package/COMPATIBILITY.md +38 -0
- package/CONTRIBUTING.md +150 -2
- package/README.en.md +59 -16
- package/README.md +57 -16
- package/SECURITY.md +31 -1
- package/adapters/default.js +10 -0
- package/adapters/store-fs.js +6 -0
- package/adapters/vcs-git.js +4 -0
- package/app/batch.js +79 -0
- package/app/board.js +25 -0
- package/app/checkCompletion.js +76 -0
- package/app/enrichedMissions.js +26 -0
- package/app/findings.js +78 -0
- package/app/gate.js +108 -0
- package/app/init.js +249 -0
- package/app/merge.js +567 -0
- package/app/messages.js +66 -0
- package/app/mission.js +35 -0
- package/app/missionContext.js +14 -0
- package/app/plan.js +42 -0
- package/app/provision.js +193 -0
- package/app/read.js +57 -0
- package/app/rebase.js +302 -0
- package/app/recording.js +72 -0
- package/app/review.js +145 -0
- package/app/roster.js +123 -0
- package/app/spawn.js +125 -0
- package/app/teardown.js +184 -0
- package/app/tower.js +524 -0
- package/cordis.patch.yml +61 -53
- package/docs/README.md +33 -1
- package/docs/architecture/overview.md +114 -14
- package/docs/board.md +72 -0
- package/docs/examples/first-mission.md +2 -1
- package/docs/faq.md +11 -7
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +22 -6
- package/docs/guide/quickstart.md +12 -7
- package/docs/guide/release.md +125 -0
- package/docs/guide/scenarios-and-philosophy.md +86 -69
- package/docs/operations/security.md +65 -14
- package/docs/reference/blockers.md +11 -10
- package/docs/reference/config.generated.md +66 -0
- package/docs/reference/config.md +11 -7
- package/docs/reference/errors.generated.md +39 -0
- package/docs/reference/errors.md +10 -2
- package/docs/reference/events.md +7 -4
- package/docs/reference/testing.md +2 -1
- package/docs/reference/tools.generated.md +33 -0
- package/docs/reference/tools.md +8 -7
- package/docs/zh/README.md +4 -3
- package/docs/zh/faq.md +9 -6
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +17 -5
- package/docs/zh/guide/quickstart.md +13 -8
- package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
- package/docs/zh/operations/security.md +43 -7
- package/docs/zh/reference/blockers.md +10 -10
- package/docs/zh/reference/config.md +11 -7
- package/docs/zh/reference/errors.md +4 -1
- package/docs/zh/reference/events.md +6 -3
- package/docs/zh/reference/testing.md +3 -2
- package/docs/zh/reference/tools.md +8 -7
- package/domain/collab/batch.js +112 -0
- package/domain/collab/messages.js +127 -0
- package/domain/collab/roster.js +158 -0
- package/domain/collab/spawn.js +188 -0
- package/domain/config/schema.js +404 -0
- package/domain/contracts/outputs.js +279 -0
- package/domain/contracts/tool-inputs/collaboration.js +58 -0
- package/domain/contracts/tool-inputs/findings.js +29 -0
- package/domain/contracts/tool-inputs/index.js +94 -0
- package/domain/contracts/tool-inputs/integration.js +37 -0
- package/domain/contracts/tool-inputs/missions.js +94 -0
- package/domain/contracts/tool-inputs/ops.js +75 -0
- package/domain/contracts/tool-inputs/shared.js +25 -0
- package/domain/errors.js +96 -0
- package/domain/gate.js +125 -0
- package/domain/integration/gate.js +176 -0
- package/domain/integration/init.js +225 -0
- package/domain/integration/liveness.js +31 -0
- package/domain/integration/merge.js +377 -0
- package/domain/integration/provision.js +124 -0
- package/domain/integration/rebase.js +236 -0
- package/domain/integration/review.js +52 -0
- package/domain/integration/teardown.js +118 -0
- package/domain/mission.js +68 -0
- package/domain/missions/completion.js +114 -0
- package/domain/missions/findings.js +201 -0
- package/domain/missions/lifecycle.js +185 -0
- package/domain/missions/plan.js +149 -0
- package/domain/missions/review.js +179 -0
- package/domain/planning.js +118 -0
- package/domain/read/enrichedMissions.js +58 -0
- package/domain/read/missionContext.js +53 -0
- package/domain/read/views.js +128 -0
- package/domain/report.js +59 -0
- package/domain/roster.js +40 -0
- package/domain/scope.js +105 -0
- package/domain/state/schema.js +233 -0
- package/domain/state/semantics.js +117 -0
- package/domain/state.js +33 -0
- package/domain/summary.js +114 -0
- package/domain/vocabulary.js +179 -0
- package/index.js +86 -34
- package/lib/briefing.js +176 -0
- package/lib/compatibility.js +108 -0
- package/lib/config.js +54 -302
- package/lib/doctor.js +108 -9
- package/lib/git.js +93 -15
- package/lib/hmr.js +4 -3
- package/lib/leases.js +3 -2
- package/lib/legacy.js +20 -9
- package/lib/modeState.js +39 -0
- package/lib/modeStore.js +35 -33
- package/lib/persistence.js +349 -0
- package/lib/presets.js +35 -0
- package/lib/spawn-prechecks.js +168 -0
- package/lib/spawn-support.js +79 -0
- package/lib/store.js +57 -85
- package/lib/tests.js +46 -11
- package/lib/tool-registry.js +544 -0
- package/lib/workspaceLock.js +46 -22
- package/lib/worktreeOwnership.js +5 -4
- package/locale/en.json +23 -1
- package/locale/zh.json +22 -0
- package/package.json +33 -4
- package/ports/clock.js +6 -0
- package/ports/ids.js +7 -0
- package/ports/index.js +2 -0
- package/ports/store.js +11 -0
- package/ports/vcs.js +11 -0
- package/scripts/architecture.mjs +94 -0
- package/scripts/check-docs.mjs +44 -3
- package/scripts/check-pack.mjs +49 -0
- package/scripts/domain-metrics.mjs +48 -0
- package/scripts/generate-config-reference.mjs +46 -0
- package/scripts/generate-error-reference.mjs +29 -0
- package/scripts/generate-tools-reference.mjs +56 -0
- package/scripts/issue-audit.mjs +259 -0
- package/scripts/lib/cli.mjs +76 -0
- package/scripts/lib/tmp.mjs +20 -0
- package/scripts/mount-smoke/fixtures.mjs +162 -0
- package/scripts/mount-smoke/host.json +4 -0
- package/scripts/mount-smoke/probe.mjs +125 -0
- package/scripts/mount-smoke/surface.mjs +141 -0
- package/scripts/mount-smoke.mjs +270 -0
- package/scripts/pack-manifest.txt +203 -0
- package/scripts/release-check.mjs +303 -0
- package/skills/tower-recovery.md +7 -5
- package/tools/adopt.js +72 -0
- package/tools/batch.js +78 -0
- package/tools/complete.js +53 -0
- package/tools/diff.js +39 -0
- package/tools/doctor.js +50 -0
- package/tools/finding.js +40 -0
- package/tools/findings.js +39 -0
- package/tools/import-legacy.js +42 -0
- package/tools/inbox.js +39 -0
- package/tools/index.js +39 -0
- package/tools/init.js +63 -0
- package/tools/interrupt.js +42 -0
- package/tools/legacy-map.js +44 -0
- package/tools/merge.js +39 -0
- package/tools/mission.js +41 -0
- package/tools/plan.js +39 -0
- package/tools/preflight.js +44 -0
- package/tools/rebase.js +39 -0
- package/tools/recover.js +45 -0
- package/tools/reload.js +52 -0
- package/tools/resource.js +54 -0
- package/tools/review.js +45 -0
- package/tools/roster.js +60 -0
- package/tools/send.js +147 -0
- package/tools/spawn.js +196 -0
- package/tools/status.js +94 -0
- package/tools/teardown.js +42 -0
- package/tools/test.js +61 -0
- package/types/cordis.d.ts +6 -0
- package/types/index.d.ts +339 -0
- package/ui-bundle/README.md +111 -0
- package/ui-bundle/client.js +1980 -0
- package/ui-bundle/cordis.patch.yml +3 -0
- package/ui-bundle/icon.svg +6 -0
- package/ui-bundle/index.js +2 -0
- package/ui-bundle/locale/en.json +6 -0
- package/ui-bundle/locale/zh.json +6 -0
- package/ui-bundle/package.json +35 -0
- package/lib/errors.js +0 -111
- package/lib/tools.js +0 -2250
- package/lib/tower.js +0 -2689
- /package/{lib → domain}/batch.js +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Pure merge decisions: merge-intent / parent / tip validation, commit-stage
|
|
2
|
+
// classification, and already-landed reconciliation decisions. Git reads and
|
|
3
|
+
// writes, the state lock, and persistence stay in app/merge.js.
|
|
4
|
+
// @ts-check
|
|
5
|
+
import { isReviewExempt, latestReview, reviewsForBranch } from './review.js'
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../../types/index.js').Mission} Mission */
|
|
8
|
+
/** @typedef {import('../../types/index.js').Review} Review */
|
|
9
|
+
/** @typedef {import('../../types/index.js').TowerConfig} TowerConfig */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} MergeIntent
|
|
13
|
+
* @property {string} missionId
|
|
14
|
+
* @property {string} baseTip
|
|
15
|
+
* @property {string} branchTip
|
|
16
|
+
* @property {string} [gateTip]
|
|
17
|
+
* @property {number} [startedAt]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} LandedMerge
|
|
22
|
+
* @property {string} commit
|
|
23
|
+
* @property {string[]} parents
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} missionId
|
|
28
|
+
* @returns {{ ok: false, blockers: Array<{ code: string, detail: string }> }}
|
|
29
|
+
*/
|
|
30
|
+
export function missionMissingResult(missionId) {
|
|
31
|
+
return { ok: false, blockers: [{ code: 'mission-missing', detail: `unknown mission "${missionId}"` }] }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} missionId
|
|
36
|
+
* @param {string} detail
|
|
37
|
+
* @returns {{ ok: false, missionId: string, blockers: Array<{ code: string, detail: string }> }}
|
|
38
|
+
*/
|
|
39
|
+
export function staleBaseResult(missionId, detail) {
|
|
40
|
+
return { ok: false, missionId, blockers: [{ code: 'stale-base', detail }] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} missionId
|
|
45
|
+
* @param {string} detail
|
|
46
|
+
* @returns {{ ok: false, missionId: string, blockers: Array<{ code: string, detail: string }> }}
|
|
47
|
+
*/
|
|
48
|
+
export function tipMovedResult(missionId, detail) {
|
|
49
|
+
return { ok: false, missionId, blockers: [{ code: 'tip-moved', detail }] }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {Mission} mission
|
|
54
|
+
* @param {string} baseTip
|
|
55
|
+
* @returns {{ ok: true, alreadyMerged: true, mergedCommit: string | null, baseTip: string }}
|
|
56
|
+
*/
|
|
57
|
+
export function alreadyMergedResult(mission, baseTip) {
|
|
58
|
+
return { ok: true, alreadyMerged: true, mergedCommit: mission.mergedCommit ?? null, baseTip }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The gate captured exact tips; a moved tip invalidates the prepared merge.
|
|
63
|
+
* Base drift keeps the historical `stale-base` code, branch drift `tip-moved`.
|
|
64
|
+
* @param {object} input
|
|
65
|
+
* @param {string} input.missionId
|
|
66
|
+
* @param {string | null} [input.gateBaseTip]
|
|
67
|
+
* @param {string | null} [input.gateBranchTip]
|
|
68
|
+
* @param {string} input.baseTip
|
|
69
|
+
* @param {string} input.branchTip
|
|
70
|
+
* @returns {ReturnType<typeof staleBaseResult> | ReturnType<typeof tipMovedResult> | null}
|
|
71
|
+
*/
|
|
72
|
+
export function gateTipDrift({ missionId, gateBaseTip = null, gateBranchTip = null, baseTip, branchTip }) {
|
|
73
|
+
if (gateBaseTip && gateBaseTip !== baseTip) {
|
|
74
|
+
return staleBaseResult(
|
|
75
|
+
missionId,
|
|
76
|
+
`base moved after gate (${gateBaseTip.slice(0, 7)} -> ${baseTip.slice(0, 7)}); re-run preflight`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
if (gateBranchTip && gateBranchTip !== branchTip) {
|
|
80
|
+
return tipMovedResult(
|
|
81
|
+
missionId,
|
|
82
|
+
`branch moved after gate (${gateBranchTip.slice(0, 7)} -> ${branchTip.slice(0, 7)}); re-run preflight`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {MergeIntent | null | undefined} current
|
|
90
|
+
* @param {MergeIntent} intent
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
export function intentMatches(current, intent) {
|
|
94
|
+
return Boolean(
|
|
95
|
+
current &&
|
|
96
|
+
current.missionId === intent.missionId &&
|
|
97
|
+
current.baseTip === intent.baseTip &&
|
|
98
|
+
current.branchTip === intent.branchTip,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `--no-ff` puts the integrated branch on the second parent.
|
|
104
|
+
* @param {string[] | null | undefined} parents
|
|
105
|
+
* @param {string} baseTip
|
|
106
|
+
* @param {string} branchTip
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
export function isExpectedMergeShape(parents, baseTip, branchTip) {
|
|
110
|
+
return Array.isArray(parents) && parents.length === 2 && parents[0] === baseTip && parents[1] === branchTip
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Only one merge may be in flight at a time.
|
|
115
|
+
* @param {string} missionId
|
|
116
|
+
* @param {MergeIntent} existing
|
|
117
|
+
* @returns {{ ok: false, missionId: string, blockers: Array<{ code: string, detail: string }> }}
|
|
118
|
+
*/
|
|
119
|
+
export function pendingMergeIntentResult(missionId, existing) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
missionId,
|
|
123
|
+
blockers: [
|
|
124
|
+
{
|
|
125
|
+
code: 'merge-intent-pending',
|
|
126
|
+
detail: `a merge intent for ${existing.missionId} is recorded; run tower_merge to reconcile it before starting another merge`,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {object} input
|
|
134
|
+
* @param {string} input.missionId
|
|
135
|
+
* @param {string} input.baseTip
|
|
136
|
+
* @param {string} input.branchTip
|
|
137
|
+
* @param {string} input.gateTip
|
|
138
|
+
* @param {number} input.startedAt
|
|
139
|
+
* @returns {MergeIntent}
|
|
140
|
+
*/
|
|
141
|
+
export function buildMergeIntent({ missionId, baseTip, branchTip, gateTip, startedAt }) {
|
|
142
|
+
return { missionId, baseTip, branchTip, gateTip, startedAt }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A "merged" observation only counts when the parents are the captured tips.
|
|
147
|
+
* @param {string} kind
|
|
148
|
+
* @param {string[]} parents
|
|
149
|
+
* @param {string} baseTip
|
|
150
|
+
* @param {string} branchTip
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
export function classifyCommitOutcome(kind, parents, baseTip, branchTip) {
|
|
154
|
+
return kind === 'merged' && !isExpectedMergeShape(parents, baseTip, branchTip) ? 'merge-shape' : kind
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {boolean} conflict
|
|
159
|
+
* @returns {'merge-conflict' | 'merge-hook-failed'}
|
|
160
|
+
*/
|
|
161
|
+
export function mergeFailureKind(conflict) {
|
|
162
|
+
return conflict ? 'merge-conflict' : 'merge-hook-failed'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {string[]} parents
|
|
167
|
+
* @param {string} baseTip
|
|
168
|
+
* @param {string} branchTip
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
export function mergeShapeDetail(parents, baseTip, branchTip) {
|
|
172
|
+
return `unexpected merge parents [${parents.join(', ')}], expected [${baseTip}, ${branchTip}]`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {string} branchTip
|
|
177
|
+
* @param {string} base
|
|
178
|
+
* @param {string} newTip
|
|
179
|
+
* @returns {string}
|
|
180
|
+
*/
|
|
181
|
+
export function mergeSuccessNote(branchTip, base, newTip) {
|
|
182
|
+
return `merged ${branchTip.slice(0, 7)} into ${base} as ${newTip.slice(0, 7)}`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {string} detail
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
export function mergeConflictNote(detail) {
|
|
190
|
+
return `merge conflict: ${detail.slice(0, 240)}`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {string} detail
|
|
195
|
+
* @param {string} status
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
export function mergeHookFailureNote(detail, status) {
|
|
199
|
+
return `merge command failed before creating a commit (${detail.slice(0, 240)}); mission left ${status} for retry`
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {object} input
|
|
204
|
+
* @param {string} input.missionId
|
|
205
|
+
* @param {string} input.code
|
|
206
|
+
* @param {string} input.detail
|
|
207
|
+
* @param {string[]} [input.warnings]
|
|
208
|
+
* @param {boolean} [input.timeout]
|
|
209
|
+
* @param {string} [input.cleanup]
|
|
210
|
+
* @returns {object}
|
|
211
|
+
*/
|
|
212
|
+
export function mergeBlockedResult({ missionId, code, detail, warnings = [], timeout = false, cleanup }) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
missionId,
|
|
216
|
+
blockers: [
|
|
217
|
+
{
|
|
218
|
+
code,
|
|
219
|
+
detail,
|
|
220
|
+
...(timeout === true ? { timeout: true } : {}),
|
|
221
|
+
...(cleanup === undefined ? {} : { cleanup }),
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Already-landed reconciliation only considers completed/blocked missions with
|
|
230
|
+
* a worktree whose review rules can be satisfied by the snapshot.
|
|
231
|
+
* @param {Mission} mission
|
|
232
|
+
* @param {Review[] | null | undefined} reviews
|
|
233
|
+
* @param {TowerConfig} [config]
|
|
234
|
+
* @returns {{ exempt: boolean, latest: Review | null } | null}
|
|
235
|
+
*/
|
|
236
|
+
export function landedMergeCandidate(mission, reviews, config = {}) {
|
|
237
|
+
if (!['completed', 'blocked'].includes(mission.status)) return null
|
|
238
|
+
if (!mission.worktree) return null
|
|
239
|
+
const branchReviews = reviewsForBranch(reviews, mission.worktree.branch)
|
|
240
|
+
const exempt = isReviewExempt(mission, config)
|
|
241
|
+
if (branchReviews.length === 0 && !exempt) return null
|
|
242
|
+
const latest = latestReview(branchReviews)
|
|
243
|
+
if (latest && !exempt && (latest.status !== 'clean' || latest.mergeVerdict !== 'merge')) return null
|
|
244
|
+
if (!latest && !exempt) return null
|
|
245
|
+
return { exempt, latest }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {object} input
|
|
250
|
+
* @param {string} input.missionId
|
|
251
|
+
* @param {string} input.base
|
|
252
|
+
* @param {string} input.branchTip
|
|
253
|
+
* @param {string} input.reviewedTip
|
|
254
|
+
* @returns {object}
|
|
255
|
+
*/
|
|
256
|
+
export function reviewStaleResult({ missionId, base, branchTip, reviewedTip }) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
missionId,
|
|
260
|
+
blockers: [
|
|
261
|
+
{
|
|
262
|
+
code: 'review-stale',
|
|
263
|
+
detail: `reviewed commit ${reviewedTip.slice(0, 7)} is contained in ${base}, but branch tip ${branchTip.slice(0, 7)} is not; the branch was partially landed or moved — re-review or repair manually`,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Decide what a landed-merge observation means from the two ancestry answers.
|
|
271
|
+
* `skip` means there is nothing safe to reconcile yet.
|
|
272
|
+
* @param {object} input
|
|
273
|
+
* @param {string} input.missionId
|
|
274
|
+
* @param {string} input.base
|
|
275
|
+
* @param {string} input.baseTip
|
|
276
|
+
* @param {string} input.branchTip
|
|
277
|
+
* @param {string | null} input.reviewedTip
|
|
278
|
+
* @param {boolean} input.branchLanded
|
|
279
|
+
* @param {boolean} input.reviewedLanded
|
|
280
|
+
* @param {LandedMerge | null} input.found
|
|
281
|
+
* @param {Review | null} input.latest
|
|
282
|
+
* @returns {{ skip: true } | { reviewStale: object } | { measured: object }}
|
|
283
|
+
*/
|
|
284
|
+
export function landedMergeEvidence({
|
|
285
|
+
missionId,
|
|
286
|
+
base,
|
|
287
|
+
baseTip,
|
|
288
|
+
branchTip,
|
|
289
|
+
reviewedTip,
|
|
290
|
+
branchLanded,
|
|
291
|
+
reviewedLanded,
|
|
292
|
+
found,
|
|
293
|
+
latest,
|
|
294
|
+
}) {
|
|
295
|
+
if (!branchLanded) {
|
|
296
|
+
if (!reviewedLanded) return { skip: true }
|
|
297
|
+
return { reviewStale: reviewStaleResult({ missionId, base, branchTip, reviewedTip: /** @type {string} */ (reviewedTip) }) }
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
measured: {
|
|
301
|
+
baseTip,
|
|
302
|
+
branchTip,
|
|
303
|
+
reviewedTip,
|
|
304
|
+
found,
|
|
305
|
+
mergedCommit: found ? found.commit : branchTip,
|
|
306
|
+
latest,
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @param {object} observed
|
|
313
|
+
* @param {string} observed.mergedCommit
|
|
314
|
+
* @param {string} observed.baseTip
|
|
315
|
+
* @param {string} observed.branchTip
|
|
316
|
+
* @param {LandedMerge | null} observed.found
|
|
317
|
+
* @param {Review | null} observed.latest
|
|
318
|
+
* @param {string | null} observed.reviewedTip
|
|
319
|
+
* @returns {object}
|
|
320
|
+
*/
|
|
321
|
+
export function landedMergeResult(observed) {
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
reconciled: true,
|
|
325
|
+
alreadyLanded: true,
|
|
326
|
+
mergedCommit: observed.mergedCommit,
|
|
327
|
+
baseTip: observed.baseTip,
|
|
328
|
+
branchTip: observed.branchTip,
|
|
329
|
+
parents: observed.found ? observed.found.parents : null,
|
|
330
|
+
review: observed.latest
|
|
331
|
+
? {
|
|
332
|
+
round: observed.latest.round,
|
|
333
|
+
status: observed.latest.status,
|
|
334
|
+
mergeVerdict: observed.latest.mergeVerdict,
|
|
335
|
+
reviewer: observed.latest.reviewer,
|
|
336
|
+
reviewedCommit: observed.reviewedTip,
|
|
337
|
+
}
|
|
338
|
+
: null,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {object} input
|
|
344
|
+
* @param {LandedMerge | null} input.found
|
|
345
|
+
* @param {string} input.mergedCommit
|
|
346
|
+
* @param {string} input.base
|
|
347
|
+
* @returns {string}
|
|
348
|
+
*/
|
|
349
|
+
export function landedMergeNote({ found, mergedCommit, base }) {
|
|
350
|
+
return found
|
|
351
|
+
? `reconciled already-landed merge ${mergedCommit.slice(0, 7)} with parents [${found.parents.map((parent) => parent.slice(0, 7)).join(', ')}]`
|
|
352
|
+
: `reconciled already-landed merge: ${mergedCommit.slice(0, 7)} is contained in ${base} (no merge commit found)`
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @param {object} input
|
|
357
|
+
* @param {string} input.head
|
|
358
|
+
* @param {string} input.baseTip
|
|
359
|
+
* @param {boolean} input.mergeInProgress
|
|
360
|
+
* @param {LandedMerge | null} input.landed
|
|
361
|
+
* @returns {{ landed: LandedMerge | null } | null}
|
|
362
|
+
*/
|
|
363
|
+
export function mergeIntentEvidence({ head, baseTip, mergeInProgress, landed }) {
|
|
364
|
+
if (mergeInProgress) return null
|
|
365
|
+
if (head === baseTip) return { landed: null }
|
|
366
|
+
return landed ? { landed } : null
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* @param {{ landed: LandedMerge | null } | null} evidence
|
|
371
|
+
* @returns {string}
|
|
372
|
+
*/
|
|
373
|
+
export function mergeIntentReconcileNote(evidence) {
|
|
374
|
+
return evidence && evidence.landed
|
|
375
|
+
? `reconciled merge intent; found landed commit ${String(evidence.landed.commit).slice(0, 7)}`
|
|
376
|
+
: 'cleared a merge intent that created no commit'
|
|
377
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Pure provision decisions: branch slug, base-commit choice, worktree root and
|
|
2
|
+
// path resolution, and result/activity projections. Git worktree creation and
|
|
3
|
+
// state persistence stay in app/provision.js.
|
|
4
|
+
// @ts-check
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../../types/index.js').Mission} Mission */
|
|
8
|
+
/** @typedef {import('../../types/index.js').MissionWorktree} MissionWorktree */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {unknown} title
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function slugify(title) {
|
|
15
|
+
const slug = String(title)
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
18
|
+
.replace(/^-+|-+$/g, '')
|
|
19
|
+
.slice(0, 24)
|
|
20
|
+
return slug || 'mission'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Mission} mission
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function worktreeBranch(mission) {
|
|
28
|
+
return `tower/${mission.id.toLowerCase()}-${slugify(mission.title)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pick the base commit from the explicit argument, the requested base ref, or
|
|
33
|
+
* the recorded dirty snapshot. `source: 'base'` means the caller must rev-parse
|
|
34
|
+
* the configured base (the only I/O in the choice).
|
|
35
|
+
* @param {object} input
|
|
36
|
+
* @param {unknown} [input.baseCommit]
|
|
37
|
+
* @param {unknown} [input.includeDirtyBase]
|
|
38
|
+
* @param {string | null} [input.snapshotCommit]
|
|
39
|
+
* @param {boolean} [input.snapshotCommitValid]
|
|
40
|
+
* @returns {{ source: 'explicit' | 'snapshot', commit: string } | { source: 'base' }}
|
|
41
|
+
*/
|
|
42
|
+
export function baseCommitChoice({ baseCommit, includeDirtyBase, snapshotCommit = null, snapshotCommitValid = false }) {
|
|
43
|
+
if (baseCommit) return { source: 'explicit', commit: String(baseCommit) }
|
|
44
|
+
if (includeDirtyBase === false) return { source: 'base' }
|
|
45
|
+
if (snapshotCommit && snapshotCommitValid) return { source: 'snapshot', commit: snapshotCommit }
|
|
46
|
+
return { source: 'base' }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {object} input
|
|
51
|
+
* @param {string | null} [input.worktreeRoot]
|
|
52
|
+
* @param {string | null} [input.configuredRoot]
|
|
53
|
+
* @param {string} input.repoRoot
|
|
54
|
+
* @param {string} input.commonDirHash
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function worktreeRoot({ worktreeRoot = null, configuredRoot = null, repoRoot, commonDirHash }) {
|
|
58
|
+
if (worktreeRoot) return path.resolve(worktreeRoot)
|
|
59
|
+
if (configuredRoot) return path.resolve(configuredRoot)
|
|
60
|
+
return path.join(path.dirname(repoRoot), '.dsh-tower-worktrees', commonDirHash)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} root
|
|
65
|
+
* @param {string} missionId
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function worktreePath(root, missionId) {
|
|
69
|
+
return path.join(root, missionId)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {unknown} missionId
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function hasMissionId(missionId) {
|
|
77
|
+
return typeof missionId === 'string' && missionId.trim().length > 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {object} input
|
|
82
|
+
* @param {string} input.path
|
|
83
|
+
* @param {string} input.branch
|
|
84
|
+
* @param {string} input.baseCommit
|
|
85
|
+
* @param {number} input.createdAt
|
|
86
|
+
* @param {string | null} [input.realPath]
|
|
87
|
+
* @returns {MissionWorktree}
|
|
88
|
+
*/
|
|
89
|
+
export function worktreeRecord({ path: wtPath, branch, baseCommit, createdAt, realPath = null }) {
|
|
90
|
+
return /** @type {MissionWorktree} */ ({
|
|
91
|
+
path: wtPath,
|
|
92
|
+
branch,
|
|
93
|
+
baseCommit,
|
|
94
|
+
createdAt,
|
|
95
|
+
...(realPath === null ? {} : { realPath }),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {Mission} mission
|
|
101
|
+
* @param {MissionWorktree} worktree
|
|
102
|
+
* @returns {{ mission: Mission, worktree: MissionWorktree, created: false }}
|
|
103
|
+
*/
|
|
104
|
+
export function existingWorktreeResult(mission, worktree) {
|
|
105
|
+
return { mission, worktree, created: false }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {Mission} mission
|
|
110
|
+
* @param {MissionWorktree} worktree
|
|
111
|
+
* @returns {{ mission: Mission, worktree: MissionWorktree, created: true }}
|
|
112
|
+
*/
|
|
113
|
+
export function createdWorktreeResult(mission, worktree) {
|
|
114
|
+
return { mission, worktree, created: true }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} branch
|
|
119
|
+
* @param {string} wtPath
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
export function provisionActivityText(branch, wtPath) {
|
|
123
|
+
return `${branch} at ${wtPath}`
|
|
124
|
+
}
|