canopycms 0.0.65 → 0.0.66-int.82

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 (60) hide show
  1. package/README.md +1 -3
  2. package/dist/ai/generate.d.ts +13 -0
  3. package/dist/ai/generate.js +9 -3
  4. package/dist/ai/to-plain-text.js +4 -0
  5. package/dist/ai/types.d.ts +20 -1
  6. package/dist/api/admin-branch-health.js +13 -12
  7. package/dist/api/branch.js +2 -2
  8. package/dist/branch-health.js +1 -1
  9. package/dist/branch-metadata-file.d.ts +58 -0
  10. package/dist/branch-metadata-file.js +65 -0
  11. package/dist/branch-metadata.d.ts +2 -26
  12. package/dist/branch-metadata.js +6 -33
  13. package/dist/branch-registry.js +4 -2
  14. package/dist/build/generate-ai-content.js +103 -0
  15. package/dist/cli/cli.js +432 -317
  16. package/dist/cli/generate-ai-content.js +264 -160
  17. package/dist/content-id-index.js +22 -0
  18. package/dist/content-listing.js +3 -0
  19. package/dist/content-reader.d.ts +25 -1
  20. package/dist/content-reader.js +15 -0
  21. package/dist/content-store.d.ts +50 -1
  22. package/dist/content-store.js +102 -4
  23. package/dist/context.d.ts +36 -4
  24. package/dist/context.js +15 -4
  25. package/dist/editor/admin/SystemHealthPanel.js +2 -2
  26. package/dist/entry-schema.d.ts +22 -0
  27. package/dist/git-manager.d.ts +9 -8
  28. package/dist/git-manager.js +35 -9
  29. package/dist/github-service.d.ts +1 -1
  30. package/dist/github-service.js +1 -1
  31. package/dist/paths/branch-name.d.ts +1 -1
  32. package/dist/paths/branch-name.js +1 -1
  33. package/dist/schema/index.d.ts +1 -1
  34. package/dist/schema/index.js +1 -1
  35. package/dist/schema/meta-loader.js +1 -1
  36. package/dist/services.d.ts +22 -0
  37. package/dist/types.d.ts +1 -1
  38. package/dist/url-exclusivity-fixtures.d.ts +76 -0
  39. package/dist/url-exclusivity-fixtures.js +119 -0
  40. package/dist/url-path-resolver.js +8 -4
  41. package/dist/utils/content-write-lock.d.ts +2 -1
  42. package/dist/utils/content-write-lock.js +2 -1
  43. package/dist/utils/error.d.ts +17 -0
  44. package/dist/utils/error.js +17 -0
  45. package/dist/utils/git.d.ts +1 -1
  46. package/dist/utils/git.js +1 -1
  47. package/dist/utils/occ-json-write.js +1 -1
  48. package/dist/worker/cms-worker.d.ts +22 -288
  49. package/dist/worker/cms-worker.js +118 -1995
  50. package/dist/worker/git-sync.d.ts +166 -0
  51. package/dist/worker/git-sync.js +554 -0
  52. package/dist/worker/history-rewrite.d.ts +129 -0
  53. package/dist/worker/history-rewrite.js +216 -0
  54. package/dist/worker/rebase.d.ts +171 -0
  55. package/dist/worker/rebase.js +859 -0
  56. package/dist/worker/task-runner.d.ts +93 -0
  57. package/dist/worker/task-runner.js +529 -0
  58. package/dist/worker/worker-context.d.ts +133 -0
  59. package/dist/worker/worker-context.js +1 -0
  60. package/package.json +1 -1
@@ -0,0 +1,129 @@
1
+ import type { WorkerContext } from './worker-context.js';
2
+ /**
3
+ * [SYNC-H1] The history-rewrite kernel: what happens when the rebase loop
4
+ * rewrites history that was ALREADY published to `remote.git` (and from there
5
+ * to GitHub).
6
+ *
7
+ * Extracted from cms-worker.ts because it is the one thing all three of the
8
+ * worker's big clusters touch, and for opposite-looking reasons:
9
+ *
10
+ * - rebase.ts ARMS a rewrite (mark -> publish -> queue) and completes an
11
+ * interrupted one via `reconcilePendingRewrite`;
12
+ * - task-runner.ts CONSUMES the marker when `pushBranchToGitHub` leases its
13
+ * push, and CLEARS it once GitHub confirms;
14
+ * - git-sync.ts READS it in `reconcileTrackedBranches`, to tell this worker's
15
+ * own in-flight rewrite apart from a genuine cross-deployment collision.
16
+ *
17
+ * Leaving these on the class would have made every cluster's module import
18
+ * every other one. They share no state beyond four resolved paths, so they
19
+ * belong here.
20
+ *
21
+ * THE INVARIANT, since it is spread across those three callers: every force
22
+ * push leases on a SPECIFIC commit this worker knows its own rebase replaced
23
+ * -- the marker, or the pre-rebase tip -- never on "whatever remote.git holds
24
+ * right now". A lease on the current tip is satisfied by a reviewer's direct
25
+ * push to the PR branch and would delete it, silently, from remote.git and
26
+ * then from GitHub.
27
+ */
28
+ export type HistoryRewriteContext = Pick<WorkerContext, 'remoteGitPath' | 'contentBranchesPath' | 'taskDir' | 'taskTimeoutMs'>;
29
+ /**
30
+ * What `remote.git` currently holds for `branchRef`, or null when this
31
+ * branch was never published there (never submitted) or the ref is
32
+ * unreadable.
33
+ *
34
+ * Uses the same explicit `--git-dir` shape as verifyBaseBranchExists()
35
+ * in cms-worker.ts: reading a bare repo that way does not depend on
36
+ * `safe.bareRepository` being permissive, which is why prod code takes
37
+ * this route rather than the config override the test-only `openBareRepo`
38
+ * helper uses.
39
+ */
40
+ export declare function readPublishedSha(ctx: Pick<HistoryRewriteContext, 'remoteGitPath'>, branchRef: string): Promise<string | null>;
41
+ /**
42
+ * Record that this worker rewrote `expectedSha` out of a branch's already
43
+ * published history (see BranchMetadata.historyRewrittenFrom).
44
+ *
45
+ * Set-once: if a marker is already present it is LEFT ALONE. Across two
46
+ * rebases before any GitHub push lands, GitHub still holds the commit the
47
+ * FIRST rebase replaced, so advancing the marker would aim the lease at a
48
+ * commit GitHub never had and permanently wedge the branch.
49
+ *
50
+ * Re-reads metadata rather than trusting the caller's loop-top snapshot:
51
+ * the task loop runs concurrently with syncGit() (see scheduleLoop) and may
52
+ * have cleared the marker while this branch was rebasing.
53
+ */
54
+ export declare function markHistoryRewritten(ctx: Pick<HistoryRewriteContext, 'contentBranchesPath'>, branchPath: string, branchDir: string, expectedSha: string): Promise<void>;
55
+ /**
56
+ * Clear the marker once GitHub is confirmed to hold the rewritten history.
57
+ *
58
+ * Best-effort only in the sense that a failure here destroys nothing: the
59
+ * lease still refuses anything unexpected, and the plain-push fallback only
60
+ * ever fast-forwards. It is NOT harmless. A marker that outlives its
61
+ * episode can wedge the NEXT one -- if the base advances before the stale
62
+ * marker is revisited, the queued push leases a commit GitHub has already
63
+ * moved off, falls back to a plain push of a rebased (non-ancestor)
64
+ * history, and fails permanently with a "something else moved it on
65
+ * GitHub" diagnosis that is false: we did.
66
+ *
67
+ * Tracked, with the concurrent-clear race that can drop a marker
68
+ * mid-arming, in
69
+ * .claude/future-tasks/worker-history-rewrite-marker-races.md.
70
+ */
71
+ export declare function clearHistoryRewrittenMarker(ctx: Pick<HistoryRewriteContext, 'contentBranchesPath'>, branchPath: string, branchDir: string): Promise<void>;
72
+ /**
73
+ * Publish a branch clone's rebased history into `remote.git`, replacing
74
+ * EXACTLY `expectedSha` and nothing else.
75
+ *
76
+ * The lease is the entire safety argument. `--force-with-lease=<ref>:<sha>`
77
+ * refuses unless `remote.git` still stands at `<sha>`, so this can only
78
+ * ever undo the commit our own rebase rewrote away. Callers must never pass
79
+ * "whatever remote.git currently holds" -- see the arming guard in
80
+ * rebase.ts's carryForwardRewrittenHistory for the interleaving where that
81
+ * would silently delete a reviewer's direct push.
82
+ *
83
+ * Returns whether the push landed. A refused lease means a concurrent
84
+ * Lambda push moved the ref; that is logged and retried by the self-heal
85
+ * pass on a later cycle, never thrown.
86
+ */
87
+ export declare function forcePublishToLocalRemote(ctx: Pick<HistoryRewriteContext, 'taskTimeoutMs'>, branchPath: string, branchRef: string, expectedSha: string): Promise<boolean>;
88
+ /**
89
+ * Queue the GitHub hop for a branch whose rewritten history now sits in
90
+ * `remote.git`, so an open PR's head follows the rebase within a cycle
91
+ * instead of waiting for the editor's next submit.
92
+ *
93
+ * Deliberately NOT skipped when a marker is already set: inferring "a task
94
+ * must already be queued" from the marker starves this hop whenever a task
95
+ * was lost, failed permanently, or was never written. Duplicate push tasks
96
+ * are bounded by base-branch advances and are idempotent (a repeat push is
97
+ * a no-op once GitHub holds the tip).
98
+ */
99
+ export declare function enqueueGitHubPush(ctx: Pick<HistoryRewriteContext, 'taskDir'>, branchRef: string): Promise<void>;
100
+ /**
101
+ * Complete a rewrite this worker started but did not finish: get the
102
+ * rebased history into `remote.git` and queued for GitHub.
103
+ *
104
+ * Runs from the rebase loop for any branch carrying a marker, whether or
105
+ * not it is behind base this cycle, so an interrupted publish converges
106
+ * without waiting for the next base-branch advance.
107
+ *
108
+ * Always leases on the MARKER, never on remote.git's current tip -- the
109
+ * marker is the one commit we know our own rebase replaced.
110
+ */
111
+ export declare function reconcilePendingRewrite(ctx: HistoryRewriteContext, options: {
112
+ branchPath: string;
113
+ branchDir: string;
114
+ branchRef: string;
115
+ headSha: string;
116
+ marker: string;
117
+ }): Promise<void>;
118
+ /**
119
+ * Whether this branch carries a pending history rewrite -- the rebase loop
120
+ * rewrote already-published history and the GitHub push has not landed yet
121
+ * (see BranchMetadata.historyRewrittenFrom).
122
+ *
123
+ * `branchName` is a git ref name; branch workspaces are directories named
124
+ * with the sanitized form, hence the conversion. Best-effort: a settings
125
+ * branch (no workspace at all), a missing directory or an unreadable
126
+ * branch.json all mean "no known rewrite", which is the conservative
127
+ * answer -- it keeps the branch in the louder `diverged` bucket.
128
+ */
129
+ export declare function hasPendingHistoryRewrite(ctx: Pick<HistoryRewriteContext, 'contentBranchesPath'>, branchName: string): Promise<boolean>;
@@ -0,0 +1,216 @@
1
+ import path from 'node:path';
2
+ import { simpleGit } from 'simple-git';
3
+ import { BranchMetadataFileManager, getBranchMetadataFileManager } from '../branch-metadata.js';
4
+ import { gitChildEnv } from '../git-manager.js';
5
+ import { sanitizeBranchName } from '../paths/branch-name.js';
6
+ import { getErrorMessage, redactCredentials } from '../utils/error.js';
7
+ import { isStaleLeaseRejection } from '../utils/git.js';
8
+ import { enqueueTask, listTasks } from './task-queue.js';
9
+ import { workerLog, workerLogWarn } from './log.js';
10
+ /**
11
+ * What `remote.git` currently holds for `branchRef`, or null when this
12
+ * branch was never published there (never submitted) or the ref is
13
+ * unreadable.
14
+ *
15
+ * Uses the same explicit `--git-dir` shape as verifyBaseBranchExists()
16
+ * in cms-worker.ts: reading a bare repo that way does not depend on
17
+ * `safe.bareRepository` being permissive, which is why prod code takes
18
+ * this route rather than the config override the test-only `openBareRepo`
19
+ * helper uses.
20
+ */
21
+ export async function readPublishedSha(ctx, branchRef) {
22
+ try {
23
+ const out = await simpleGit().raw([
24
+ '--git-dir',
25
+ ctx.remoteGitPath,
26
+ 'rev-parse',
27
+ '--verify',
28
+ `refs/heads/${branchRef}`,
29
+ ]);
30
+ const sha = out.trim();
31
+ return sha.length > 0 ? sha : null;
32
+ }
33
+ catch {
34
+ // Absent from remote.git: this branch was never submitted, so none of
35
+ // its history was ever published. Not an error.
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * Record that this worker rewrote `expectedSha` out of a branch's already
41
+ * published history (see BranchMetadata.historyRewrittenFrom).
42
+ *
43
+ * Set-once: if a marker is already present it is LEFT ALONE. Across two
44
+ * rebases before any GitHub push lands, GitHub still holds the commit the
45
+ * FIRST rebase replaced, so advancing the marker would aim the lease at a
46
+ * commit GitHub never had and permanently wedge the branch.
47
+ *
48
+ * Re-reads metadata rather than trusting the caller's loop-top snapshot:
49
+ * the task loop runs concurrently with syncGit() (see scheduleLoop) and may
50
+ * have cleared the marker while this branch was rebasing.
51
+ */
52
+ export async function markHistoryRewritten(ctx, branchPath, branchDir, expectedSha) {
53
+ const current = await BranchMetadataFileManager.loadOnly(branchPath);
54
+ if (typeof current?.branch.historyRewrittenFrom === 'string')
55
+ return;
56
+ const meta = getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath);
57
+ await meta.save({ branch: { name: branchDir, historyRewrittenFrom: expectedSha } });
58
+ }
59
+ /**
60
+ * Clear the marker once GitHub is confirmed to hold the rewritten history.
61
+ *
62
+ * Best-effort only in the sense that a failure here destroys nothing: the
63
+ * lease still refuses anything unexpected, and the plain-push fallback only
64
+ * ever fast-forwards. It is NOT harmless. A marker that outlives its
65
+ * episode can wedge the NEXT one -- if the base advances before the stale
66
+ * marker is revisited, the queued push leases a commit GitHub has already
67
+ * moved off, falls back to a plain push of a rebased (non-ancestor)
68
+ * history, and fails permanently with a "something else moved it on
69
+ * GitHub" diagnosis that is false: we did.
70
+ *
71
+ * Tracked, with the concurrent-clear race that can drop a marker
72
+ * mid-arming, in
73
+ * .claude/future-tasks/worker-history-rewrite-marker-races.md.
74
+ */
75
+ export async function clearHistoryRewrittenMarker(ctx, branchPath, branchDir) {
76
+ try {
77
+ const meta = getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath);
78
+ // Explicit undefined clears the key -- save()'s merge only overwrites
79
+ // keys present in the update (same pattern as rebaseFailure).
80
+ await meta.save({ branch: { name: branchDir, historyRewrittenFrom: undefined } });
81
+ }
82
+ catch (err) {
83
+ workerLogWarn(` Failed to clear history-rewrite marker for ${branchDir}: ${getErrorMessage(err)}`);
84
+ }
85
+ }
86
+ /**
87
+ * Publish a branch clone's rebased history into `remote.git`, replacing
88
+ * EXACTLY `expectedSha` and nothing else.
89
+ *
90
+ * The lease is the entire safety argument. `--force-with-lease=<ref>:<sha>`
91
+ * refuses unless `remote.git` still stands at `<sha>`, so this can only
92
+ * ever undo the commit our own rebase rewrote away. Callers must never pass
93
+ * "whatever remote.git currently holds" -- see the arming guard in
94
+ * rebase.ts's carryForwardRewrittenHistory for the interleaving where that
95
+ * would silently delete a reviewer's direct push.
96
+ *
97
+ * Returns whether the push landed. A refused lease means a concurrent
98
+ * Lambda push moved the ref; that is logged and retried by the self-heal
99
+ * pass on a later cycle, never thrown.
100
+ */
101
+ export async function forcePublishToLocalRemote(ctx, branchPath, branchRef, expectedSha) {
102
+ // A dedicated instance rather than the caller's: `.env()` replaces the
103
+ // whole child environment, and the rebase loop's instance must keep its
104
+ // ambient one. gitChildEnv (not gitNetworkChildEnv) because this push
105
+ // targets the local bare repo -- and the locale pin is what keeps
106
+ // isStaleLeaseRejection below from silently becoming a no-op.
107
+ const pushGit = simpleGit({ baseDir: branchPath, timeout: { block: ctx.taskTimeoutMs } });
108
+ pushGit.env(gitChildEnv({}));
109
+ try {
110
+ await pushGit.raw([
111
+ 'push',
112
+ `--force-with-lease=${branchRef}:${expectedSha}`,
113
+ // Real flags must precede --end-of-options; everything after it is
114
+ // positional (see GitManager.push() for the same guard).
115
+ '--end-of-options',
116
+ 'origin',
117
+ `${branchRef}:${branchRef}`,
118
+ ]);
119
+ workerLog(` Published rebased ${branchRef} into remote.git`);
120
+ return true;
121
+ }
122
+ catch (err) {
123
+ const message = redactCredentials(getErrorMessage(err));
124
+ workerLogWarn(isStaleLeaseRejection(message)
125
+ ? ` Did not publish rebased ${branchRef} into remote.git: it moved since this cycle read it (concurrent submit?) -- retrying next cycle`
126
+ : ` Failed to publish rebased ${branchRef} into remote.git: ${message}`);
127
+ return false;
128
+ }
129
+ }
130
+ /**
131
+ * Queue the GitHub hop for a branch whose rewritten history now sits in
132
+ * `remote.git`, so an open PR's head follows the rebase within a cycle
133
+ * instead of waiting for the editor's next submit.
134
+ *
135
+ * Deliberately NOT skipped when a marker is already set: inferring "a task
136
+ * must already be queued" from the marker starves this hop whenever a task
137
+ * was lost, failed permanently, or was never written. Duplicate push tasks
138
+ * are bounded by base-branch advances and are idempotent (a repeat push is
139
+ * a no-op once GitHub holds the tip).
140
+ */
141
+ export async function enqueueGitHubPush(ctx, branchRef) {
142
+ try {
143
+ // Dedupe against tasks actually in flight rather than against the
144
+ // marker: a branch whose GitHub push keeps failing would otherwise
145
+ // gain one task per sync cycle forever.
146
+ for (const status of ['pending', 'processing']) {
147
+ const inFlight = await listTasks(ctx.taskDir, status);
148
+ if (inFlight.some((t) => t.action === 'push-branch' && t.payload.branch === branchRef)) {
149
+ return;
150
+ }
151
+ }
152
+ await enqueueTask(ctx.taskDir, { action: 'push-branch', payload: { branch: branchRef } });
153
+ workerLog(` Queued GitHub push for ${branchRef}`);
154
+ }
155
+ catch (err) {
156
+ workerLogWarn(` Failed to queue GitHub push for ${branchRef}: ${getErrorMessage(err)}`);
157
+ }
158
+ }
159
+ /**
160
+ * Complete a rewrite this worker started but did not finish: get the
161
+ * rebased history into `remote.git` and queued for GitHub.
162
+ *
163
+ * Runs from the rebase loop for any branch carrying a marker, whether or
164
+ * not it is behind base this cycle, so an interrupted publish converges
165
+ * without waiting for the next base-branch advance.
166
+ *
167
+ * Always leases on the MARKER, never on remote.git's current tip -- the
168
+ * marker is the one commit we know our own rebase replaced.
169
+ */
170
+ export async function reconcilePendingRewrite(ctx, options) {
171
+ const { branchPath, branchDir, branchRef, headSha, marker } = options;
172
+ const publishedSha = await readPublishedSha(ctx, branchRef);
173
+ if (publishedSha === null) {
174
+ workerLogWarn(` ${branchDir}: a rewritten history is recorded but ${branchRef} is gone from remote.git -- nothing to publish`);
175
+ return;
176
+ }
177
+ if (publishedSha === headSha) {
178
+ // remote.git already carries the rewrite; only the GitHub hop is left
179
+ // (a crash between the push and the queue, or a task that was lost).
180
+ await enqueueGitHubPush(ctx, branchRef);
181
+ return;
182
+ }
183
+ if (publishedSha === marker) {
184
+ // The publish into remote.git never landed -- a crash right after the
185
+ // marker was written, or a lease refused by a concurrent submit.
186
+ if (await forcePublishToLocalRemote(ctx, branchPath, branchRef, marker)) {
187
+ await enqueueGitHubPush(ctx, branchRef);
188
+ }
189
+ return;
190
+ }
191
+ // Neither this branch's rebased tip nor the commit its rewrite replaced:
192
+ // something else moved remote.git. Never force over that.
193
+ workerLogWarn(` ${branchDir}: remote.git is at ${publishedSha} for ${branchRef}, which is neither the ` +
194
+ `rebased tip nor the commit the rewrite replaced -- left untouched`);
195
+ }
196
+ /**
197
+ * Whether this branch carries a pending history rewrite -- the rebase loop
198
+ * rewrote already-published history and the GitHub push has not landed yet
199
+ * (see BranchMetadata.historyRewrittenFrom).
200
+ *
201
+ * `branchName` is a git ref name; branch workspaces are directories named
202
+ * with the sanitized form, hence the conversion. Best-effort: a settings
203
+ * branch (no workspace at all), a missing directory or an unreadable
204
+ * branch.json all mean "no known rewrite", which is the conservative
205
+ * answer -- it keeps the branch in the louder `diverged` bucket.
206
+ */
207
+ export async function hasPendingHistoryRewrite(ctx, branchName) {
208
+ try {
209
+ const branchPath = path.join(ctx.contentBranchesPath, sanitizeBranchName(branchName));
210
+ const metaFile = await BranchMetadataFileManager.loadOnly(branchPath);
211
+ return typeof metaFile?.branch.historyRewrittenFrom === 'string';
212
+ }
213
+ catch {
214
+ return false;
215
+ }
216
+ }
@@ -0,0 +1,171 @@
1
+ import { type BranchMetadataFile } from '../branch-metadata.js';
2
+ import { type ContentId } from '../paths/types.js';
3
+ import type { WorkerContext } from './worker-context.js';
4
+ /**
5
+ * The rebase loop: the largest and most safety-critical of the four call trees
6
+ * that used to share cms-worker.ts, and the deepest leaf of the git-sync
7
+ * cluster. `syncGit` (git-sync.ts) calls `runRebaseCycle` once per cycle.
8
+ *
9
+ * Every branch workspace that is behind the base branch is rebased onto it,
10
+ * keeping the BRANCH's version of any conflicting file. Three things make this
11
+ * the part of the worker most likely to misbehave under real EFS and spot
12
+ * conditions, and all three are documented at the point they happen rather
13
+ * than here:
14
+ *
15
+ * - it holds the [SYNC-C1] cross-host content-write lock for the whole rebase,
16
+ * so an editor's save cannot be destroyed mid-replay;
17
+ * - it recovers a rebase INTERRUPTED by a crash, OOM or spot interruption,
18
+ * which is lossy in a specific, logged way;
19
+ * - it carries a rewritten history forward into remote.git and GitHub under a
20
+ * lease keyed to the exact commit it replaced ([SYNC-H1], history-rewrite.ts).
21
+ *
22
+ * `pollMergeState` lives here rather than in git-sync.ts because the rebase
23
+ * loop is its only caller: submitted/approved branches are skipped for rebasing
24
+ * but still need their PR's resolution polled, and this loop is the only thing
25
+ * that walks every branch workspace.
26
+ */
27
+ export type RebaseContext = Pick<WorkerContext, 'githubOwner' | 'githubRepo' | 'baseBranch' | 'sanitizedBaseBranch' | 'contentBranchesPath' | 'contentRoot' | 'remoteGitPath' | 'taskDir' | 'taskTimeoutMs' | 'octokit' | 'afterConflictDetectedForTesting' | 'afterRebaseCompletedForTesting'>;
28
+ /**
29
+ * Per-cycle outcome of `rebaseActiveBranches()` (PR-W1). Folded by `syncGit()`
30
+ * into the worker's self-reported status (`WorkerStatusReport.lastGitSync`,
31
+ * see worker-status.ts) alongside a `durationMs` measured around the whole
32
+ * sync cycle.
33
+ */
34
+ export interface RebaseSummary {
35
+ /**
36
+ * Branches that were behind and completed a rebase onto the base branch
37
+ * (successfully, whether or not conflicts were resolved via --theirs).
38
+ * Branches that were already up to date are NOT listed here.
39
+ */
40
+ rebased: string[];
41
+ /** Branches skipped this cycle because their working tree had uncommitted changes. */
42
+ skippedDirty: string[];
43
+ /**
44
+ * [SYNC-C1] Branches skipped this cycle for a content-write-lock reason,
45
+ * either of which is a RETRY rather than a failure:
46
+ *
47
+ * - a content write already held the branch's cross-host content-write lock
48
+ * (utils/content-write-lock.ts) -- the worker yields on contention, since
49
+ * the editor on the other side is a person waiting on a save; or
50
+ * - the lock was LOST mid-rebase (compromised), so the worker stopped before
51
+ * the next destructive git step. Only when the rebase had not completed:
52
+ * a completed rebase must still run its completion path, or it strands the
53
+ * [SYNC-H1] marker on a branch nothing will revisit.
54
+ */
55
+ skippedLocked: string[];
56
+ /** Branches whose rebase attempt failed (fetch error, unexpected rebase error, or MAX_REBASE_ROUNDS exceeded). */
57
+ failed: {
58
+ branch: string;
59
+ error: string;
60
+ }[];
61
+ }
62
+ /**
63
+ * Poll GitHub for a submitted/approved branch's PR resolution.
64
+ *
65
+ * submitted/approved branches sit outside the rebase loop and get no
66
+ * other signal that their PR resolved on GitHub -- nothing pushes a
67
+ * merge/close webhook back into the branch workspace. merged ->
68
+ * auto-archive via buildMergedBranchUpdate (shared with the manual
69
+ * markAsMerged API so both paths produce identical archived-branch
70
+ * metadata). closed-without-merge -> record pullRequestState only; an
71
+ * admin decides the workflow transition from there. Best-effort: any
72
+ * failure here is logged and swallowed, retried next sync cycle.
73
+ */
74
+ export declare function pollMergeState(ctx: RebaseContext, branchDir: string, branchPath: string, metaFile: BranchMetadataFile | null): Promise<void>;
75
+ /**
76
+ * Persist a per-branch rebase failure to branch.json (PR-W2), bounded to
77
+ * roughly one save per failing branch per hour: a branch stuck failing
78
+ * every cycle must not turn into unbounded save-per-cycle x N-failing-
79
+ * branches write amplification -- save() eager-regenerates the branch
80
+ * registry (branch-metadata.ts's invalidateRegistry(), O(branch count) EFS
81
+ * reads), the same concern the `alreadyClean` no-op guard above exists
82
+ * for.
83
+ *
84
+ * Best-effort and non-fatal like every other metadata write in this
85
+ * loop's error paths: a corrupt branch.json, a lock-contention error, or
86
+ * any other save failure here must never abort the per-branch iteration.
87
+ * This matters doubly at the two call sites -- one is inside the outer
88
+ * per-branch catch, with no further catch of its own around this call --
89
+ * so the whole method is wrapped, not just the load.
90
+ */
91
+ export declare function recordRebaseFailure(ctx: RebaseContext, branchPath: string, branchDir: string, message: string): Promise<void>;
92
+ /**
93
+ * Map the file paths a rebase resolved --theirs onto the ContentIds the editor
94
+ * shows as conflicted, deduplicated.
95
+ *
96
+ * IDs rather than paths because they are immutable: a later slug rename must
97
+ * not orphan a conflict marker. Paths that carry no recoverable ID are dropped
98
+ * rather than guessed at.
99
+ *
100
+ * Pure -- the one piece of this loop that can be tested without a git repo.
101
+ */
102
+ export declare function conflictFilesToContentIds(conflictedFiles: readonly string[], contentRoot: string): ContentId[];
103
+ /**
104
+ * What one branch contributed to the cycle summary -- the return type that
105
+ * replaced `continue` when the per-branch body moved into
106
+ * {@link rebaseOneBranch}.
107
+ *
108
+ * The original loop expressed this by pushing to one of four arrays in scope
109
+ * and then `continue`-ing; there were nine such exits and each pushed to a
110
+ * different array (or to none), which is exactly the sort of thing that is
111
+ * easy to get wrong when the body is 640 lines long. Making it a return value
112
+ * means the compiler checks that every path produces one.
113
+ */
114
+ export type BranchRebaseOutcome =
115
+ /** Nothing to do: a skipped status, or already up to date. Not reported. */
116
+ {
117
+ kind: 'none';
118
+ }
119
+ /** Was behind and completed a rebase, with or without conflict resolution. */
120
+ | {
121
+ kind: 'rebased';
122
+ }
123
+ /** Working tree had uncommitted changes. Retried next cycle. */
124
+ | {
125
+ kind: 'skippedDirty';
126
+ }
127
+ /** [SYNC-C1] Content-write lock busy or lost mid-rebase. Retried next cycle. */
128
+ | {
129
+ kind: 'skippedLocked';
130
+ } | {
131
+ kind: 'failed';
132
+ error: string;
133
+ /**
134
+ * True when the rebase itself COMPLETED and the branch was already
135
+ * counted as rebased before the failure. Only reachable from the
136
+ * [SYNC-H1] carry-forward block, which is the sole code that runs after
137
+ * that point and can still throw (via markHistoryRewritten's metadata
138
+ * save).
139
+ *
140
+ * Not a tidying-up detail: the original loop pushed to `rebased` and
141
+ * THEN to `failed` in that interleaving, so the branch appeared in both
142
+ * buckets -- and it genuinely is both, because its history moved and
143
+ * publishing the move failed. Collapsing it to `failed` alone would drop
144
+ * a real rebase from worker-status.json.
145
+ *
146
+ * KNOWN COVERAGE GAP, stated rather than hidden: only the `false`
147
+ * direction is exercised (the fetch-error path in cms-worker.test.ts).
148
+ * Reaching the `true` direction needs `markHistoryRewritten`'s metadata
149
+ * save to fail AFTER a completed rebase, and there is no seam for that --
150
+ * the save sits between two other saves with no hook between them.
151
+ * Adding a production hook purely to reach it would cost more than the
152
+ * gap. The placement was instead verified by hand against the pre-split
153
+ * method: `didRebase = true` sits exactly where `rebased.push` sat, after
154
+ * the conflictStatus save and before the carry-forward, so a failing
155
+ * conflictStatus save still yields `failed` alone.
156
+ */
157
+ rebased?: boolean;
158
+ };
159
+ /**
160
+ * Rebase every branch workspace that is behind the base branch, one cycle.
161
+ *
162
+ * The per-branch work is {@link rebaseOneBranch}; what stays here is the walk
163
+ * over `content-branches/`, the structural pre-filter (entries that are not
164
+ * branch workspaces at all, plus the base branch's own clone), and folding
165
+ * each branch's outcome into the summary.
166
+ *
167
+ * Deliberately sequential rather than concurrent: each branch's rebase holds
168
+ * that branch's cross-host content-write lock and runs a series of git
169
+ * subprocesses against a shared filesystem.
170
+ */
171
+ export declare function runRebaseCycle(ctx: RebaseContext): Promise<RebaseSummary>;