pi-codex-marketplace 0.1.0

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/extensions/pi/git-registration.ts +138 -0
  4. package/extensions/pi/index.ts +293 -0
  5. package/extensions/pi/installation.ts +90 -0
  6. package/extensions/pi/journal.ts +80 -0
  7. package/extensions/pi/lifecycle.ts +285 -0
  8. package/extensions/pi/registration.ts +143 -0
  9. package/extensions/pi/scope-overrides.ts +170 -0
  10. package/package.json +60 -0
  11. package/src/barrier/global-barrier.ts +105 -0
  12. package/src/bridge-state/atomic.ts +237 -0
  13. package/src/bridge-state/index.ts +5 -0
  14. package/src/bridge-state/migrate.ts +261 -0
  15. package/src/bridge-state/paths.ts +75 -0
  16. package/src/bridge-state/repair.ts +185 -0
  17. package/src/bridge-state/schema.ts +70 -0
  18. package/src/bridge-state/store.ts +489 -0
  19. package/src/bridge-state/types.ts +170 -0
  20. package/src/cache/index.ts +2 -0
  21. package/src/cache/paths.ts +42 -0
  22. package/src/cache/source-cache.ts +365 -0
  23. package/src/compatibility/index.ts +1 -0
  24. package/src/compatibility/profile.ts +328 -0
  25. package/src/installation/flow.ts +443 -0
  26. package/src/installation/index.ts +1 -0
  27. package/src/installation/inspection.ts +129 -0
  28. package/src/journal/active-chains.ts +99 -0
  29. package/src/journal/index.ts +3 -0
  30. package/src/journal/journal.ts +215 -0
  31. package/src/journal/types.ts +49 -0
  32. package/src/lifecycle/index.ts +5 -0
  33. package/src/lifecycle/rebind.ts +290 -0
  34. package/src/lifecycle/refresh.ts +407 -0
  35. package/src/lifecycle/removal.ts +457 -0
  36. package/src/lifecycle/update-plan.ts +222 -0
  37. package/src/lifecycle/update.ts +303 -0
  38. package/src/projection/collision.ts +120 -0
  39. package/src/projection/effective-state.ts +182 -0
  40. package/src/projection/index.ts +4 -0
  41. package/src/projection/overrides.ts +230 -0
  42. package/src/projection/project.ts +359 -0
  43. package/src/reconciliation/startup.ts +144 -0
  44. package/src/registration/budget.ts +28 -0
  45. package/src/registration/catalog.ts +224 -0
  46. package/src/registration/contained.ts +140 -0
  47. package/src/registration/fence.ts +86 -0
  48. package/src/registration/findings.ts +188 -0
  49. package/src/registration/flow.ts +619 -0
  50. package/src/registration/git-acquisition.ts +481 -0
  51. package/src/registration/git-flow.ts +654 -0
  52. package/src/registration/git-locator.ts +380 -0
  53. package/src/registration/git-selector.ts +279 -0
  54. package/src/registration/index.ts +16 -0
  55. package/src/registration/receipt.ts +305 -0
  56. package/src/registration/registration.ts +102 -0
  57. package/src/registration/snapshot.ts +382 -0
  58. package/src/registration/source-key.ts +111 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Active Recovery Chains in the Receipt Journal.
3
+ * See CONTEXT.md: Receipt Resolution, Receipt Journal, Active Recovery Chain.
4
+ *
5
+ * Rules:
6
+ * - A receipt enters an active condition if:
7
+ * - summary === 'Pending Application'
8
+ * - summary === 'Persistence Indeterminate' or durableOutcome === 'indeterminate'
9
+ * - summary === 'Persistence Failed' or durableOutcome === 'failed'
10
+ * - has RECEIPT_PERSISTENCE_FAILED finding
11
+ * - Resolved: a later receipt explicitly recovers this receipt (recoversReceiptId === rootReceiptId)
12
+ * and achieves Completed / Completed with diagnostics for the same target state.
13
+ * - Superseded: a later receipt commits a replacement State Revision (durableOutcome === 'committed'
14
+ * and new revision) or explicitly targets superseding.
15
+ * - Failed retry: appends to the chain, but leaves the active condition UNRESOLVED.
16
+ */
17
+
18
+ import { CODE } from '../registration/findings.js';
19
+ import type { AttemptReceipt } from '../registration/receipt.js';
20
+ import type { ActiveCondition, ActiveRecoveryChain } from './types.js';
21
+
22
+ function getActiveCondition(receipt: AttemptReceipt): ActiveCondition | null {
23
+ if (receipt.findings.some((f) => f.code === CODE.RECEIPT_PERSISTENCE_FAILED)) {
24
+ return 'journal-degradation';
25
+ }
26
+ if (receipt.summary === 'Persistence Indeterminate' || receipt.durableOutcome === 'indeterminate') {
27
+ return 'persistence-indeterminate';
28
+ }
29
+ if (receipt.summary === 'Persistence Failed' || receipt.durableOutcome === 'failed') {
30
+ return 'persistence-failed';
31
+ }
32
+ if (receipt.summary === 'Pending Application' || receipt.runtimeOutcome === 'pending-application') {
33
+ return 'pending-application';
34
+ }
35
+ return null;
36
+ }
37
+
38
+ export function findActiveRecoveryChains(
39
+ receipts: AttemptReceipt[],
40
+ ): { activeChains: ActiveRecoveryChain[]; allChains: ActiveRecoveryChain[] } {
41
+ const chains: ActiveRecoveryChain[] = [];
42
+
43
+ for (let i = 0; i < receipts.length; i++) {
44
+ const rcpt = receipts[i];
45
+ const condition = getActiveCondition(rcpt);
46
+
47
+ // If this receipt is a recovery attempt for an existing chain
48
+ if (rcpt.recoversReceiptId) {
49
+ const existingChain = chains.find((c) => c.rootReceiptId === rcpt.recoversReceiptId);
50
+ if (existingChain) {
51
+ existingChain.receipts.push(rcpt);
52
+ if (rcpt.summary === 'Completed' || rcpt.summary === 'Completed with diagnostics') {
53
+ existingChain.resolved = true;
54
+ existingChain.resolvedByReceiptId = rcpt.id;
55
+ }
56
+ continue;
57
+ }
58
+ }
59
+
60
+ // If this receipt opened an active condition and wasn't a recovery attempt
61
+ if (condition) {
62
+ const chain: ActiveRecoveryChain = {
63
+ rootReceiptId: rcpt.id,
64
+ scope: rcpt.scope,
65
+ condition,
66
+ stateRevision: rcpt.observedStateRevision ?? rcpt.targetStateRevision ?? rcpt.expectedStateRevision,
67
+ receipts: [rcpt],
68
+ resolved: false,
69
+ superseded: false,
70
+ };
71
+ chains.push(chain);
72
+ }
73
+
74
+ // Check if this receipt supersedes any earlier un-resolved chains in the same scope
75
+ // A replacement commit supersedes previous un-resolved chains
76
+ if (rcpt.durableOutcome === 'committed' && rcpt.observedStateRevision) {
77
+ for (const chain of chains) {
78
+ if (!chain.resolved && !chain.superseded && chain.scope === rcpt.scope && chain.rootReceiptId !== rcpt.id) {
79
+ // If the commit revision is different/newer than the chain's revision
80
+ if (rcpt.observedStateRevision !== chain.stateRevision) {
81
+ chain.superseded = true;
82
+ chain.supersededByReceiptId = rcpt.id;
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ if (rcpt.supersedesReceiptId) {
89
+ const targetChain = chains.find((c) => c.rootReceiptId === rcpt.supersedesReceiptId);
90
+ if (targetChain && !targetChain.resolved) {
91
+ targetChain.superseded = true;
92
+ targetChain.supersededByReceiptId = rcpt.id;
93
+ }
94
+ }
95
+ }
96
+
97
+ const activeChains = chains.filter((c) => !c.resolved && !c.superseded);
98
+ return { activeChains, allChains: chains };
99
+ }
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './active-chains.js';
3
+ export * from './journal.js';
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Receipt Journal implementation — append-only durable history of Attempt Receipts.
3
+ * See CONTEXT.md: Receipt Journal, Attempt Receipt, Receipt Resolution.
4
+ *
5
+ * File location:
6
+ * - Global: {getAgentDir()}/codex-marketplace/receipts.jsonl
7
+ * - Project: {cwd}/.pi/codex-marketplace/receipts.jsonl
8
+ */
9
+
10
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, openSync, closeSync, fsyncSync } from 'node:fs';
11
+ import { dirname } from 'node:path';
12
+
13
+ import { atomicWriteFile } from '../bridge-state/atomic.js';
14
+ import { getReceiptsJournalPath } from '../bridge-state/paths.js';
15
+ import type { Scope } from '../bridge-state/types.js';
16
+ import { CODE, notice, RULE, type ValidationFinding } from '../registration/findings.js';
17
+ import type { AttemptReceipt } from '../registration/receipt.js';
18
+ import { findActiveRecoveryChains } from './active-chains.js';
19
+ import type {
20
+ JournalAppendResult,
21
+ JournalPruneResult,
22
+ JournalReadResult,
23
+ } from './types.js';
24
+
25
+ export interface JournalOptions {
26
+ cwd?: string;
27
+ agentDir?: string;
28
+ }
29
+
30
+ /** Append an Attempt Receipt to the scope's Receipt Journal with fsync durability. */
31
+ export async function appendReceipt(
32
+ scope: Scope,
33
+ receipt: AttemptReceipt,
34
+ opts: JournalOptions = {},
35
+ ): Promise<JournalAppendResult> {
36
+ const journalPath = getReceiptsJournalPath(scope, opts);
37
+ try {
38
+ mkdirSync(dirname(journalPath), { recursive: true });
39
+ const line = JSON.stringify(receipt) + '\n';
40
+ appendFileSync(journalPath, line, 'utf-8');
41
+
42
+ // fsync file for durability
43
+ try {
44
+ const fd = openSync(journalPath, 'r');
45
+ try {
46
+ fsyncSync(fd);
47
+ } finally {
48
+ closeSync(fd);
49
+ }
50
+ } catch {}
51
+
52
+ return { success: true };
53
+ } catch (e) {
54
+ const msg = e instanceof Error ? e.message : String(e);
55
+ const finding: ValidationFinding = notice({
56
+ code: CODE.RECEIPT_PERSISTENCE_FAILED,
57
+ phase: 'post-commit',
58
+ target: 'attempt',
59
+ scope,
60
+ pointer: journalPath,
61
+ rule: RULE.RECEIPT_PERSISTENCE_FAILED,
62
+ outcome: `Failed to persist Attempt Receipt to journal: ${msg}`,
63
+ });
64
+ return { success: false, error: msg, finding };
65
+ }
66
+ }
67
+
68
+ /** Read the scope's Receipt Journal, parsing line-by-line with tolerance for single-line corruptions. */
69
+ export async function readReceiptJournal(
70
+ scope: Scope,
71
+ opts: JournalOptions = {},
72
+ ): Promise<JournalReadResult> {
73
+ const journalPath = getReceiptsJournalPath(scope, opts);
74
+
75
+ if (!existsSync(journalPath)) {
76
+ return {
77
+ receipts: [],
78
+ activeChains: [],
79
+ allChains: [],
80
+ corruptedLineCount: 0,
81
+ isDegraded: false,
82
+ findings: [],
83
+ };
84
+ }
85
+
86
+ let content: string;
87
+ try {
88
+ content = readFileSync(journalPath, 'utf-8');
89
+ } catch (e) {
90
+ const msg = e instanceof Error ? e.message : String(e);
91
+ return {
92
+ receipts: [],
93
+ activeChains: [],
94
+ allChains: [],
95
+ corruptedLineCount: 1,
96
+ isDegraded: true,
97
+ findings: [
98
+ notice({
99
+ code: CODE.RECEIPT_CORRUPT,
100
+ phase: 'post-commit',
101
+ target: 'attempt',
102
+ scope,
103
+ pointer: journalPath,
104
+ rule: RULE.RECEIPT_CORRUPT,
105
+ outcome: `Failed to read receipts journal: ${msg}`,
106
+ }),
107
+ ],
108
+ error: msg,
109
+ };
110
+ }
111
+
112
+ const lines = content.split('\n');
113
+ const receipts: AttemptReceipt[] = [];
114
+ const findings: ValidationFinding[] = [];
115
+ let corruptedLineCount = 0;
116
+
117
+ for (let i = 0; i < lines.length; i++) {
118
+ const line = lines[i].trim();
119
+ if (line.length === 0) continue;
120
+
121
+ try {
122
+ const parsed = JSON.parse(line);
123
+ if (typeof parsed === 'object' && parsed !== null && typeof parsed.id === 'string' && typeof parsed.summary === 'string') {
124
+ receipts.push(parsed as AttemptReceipt);
125
+ } else {
126
+ corruptedLineCount++;
127
+ findings.push(
128
+ notice({
129
+ code: CODE.RECEIPT_CORRUPT,
130
+ phase: 'post-commit',
131
+ target: 'attempt',
132
+ scope,
133
+ pointer: `${journalPath}:${i + 1}`,
134
+ rule: RULE.RECEIPT_CORRUPT,
135
+ outcome: `Corrupted receipt line ${i + 1}: missing required receipt fields`,
136
+ }),
137
+ );
138
+ }
139
+ } catch (e) {
140
+ corruptedLineCount++;
141
+ findings.push(
142
+ notice({
143
+ code: CODE.RECEIPT_CORRUPT,
144
+ phase: 'post-commit',
145
+ target: 'attempt',
146
+ scope,
147
+ pointer: `${journalPath}:${i + 1}`,
148
+ rule: RULE.RECEIPT_CORRUPT,
149
+ outcome: `Corrupted receipt line ${i + 1}: invalid JSON`,
150
+ }),
151
+ );
152
+ }
153
+ }
154
+
155
+ const { activeChains, allChains } = findActiveRecoveryChains(receipts);
156
+ const isDegraded = corruptedLineCount > 0;
157
+
158
+ return {
159
+ receipts,
160
+ activeChains,
161
+ allChains,
162
+ corruptedLineCount,
163
+ isDegraded,
164
+ findings,
165
+ };
166
+ }
167
+
168
+ /** Prune resolved receipts outside active chains while preserving ALL active recovery chains. */
169
+ export async function pruneReceiptJournal(
170
+ scope: Scope,
171
+ keepCount = 100,
172
+ opts: JournalOptions = {},
173
+ ): Promise<JournalPruneResult> {
174
+ const current = await readReceiptJournal(scope, opts);
175
+ const activeReceiptIds = new Set<string>();
176
+
177
+ for (const chain of current.activeChains) {
178
+ for (const r of chain.receipts) {
179
+ activeReceiptIds.add(r.id);
180
+ }
181
+ }
182
+
183
+ const outsideActive: AttemptReceipt[] = [];
184
+ for (const r of current.receipts) {
185
+ if (!activeReceiptIds.has(r.id)) {
186
+ outsideActive.push(r);
187
+ }
188
+ }
189
+
190
+ // Keep latest N outside active chains
191
+ const retainOutside = outsideActive.slice(-keepCount);
192
+ const retainOutsideIds = new Set(retainOutside.map((r) => r.id));
193
+
194
+ // Build final retained receipts preserving chronological order
195
+ const retained = current.receipts.filter(
196
+ (r) => activeReceiptIds.has(r.id) || retainOutsideIds.has(r.id),
197
+ );
198
+
199
+ const prunedCount = current.receipts.length - retained.length;
200
+ const journalPath = getReceiptsJournalPath(scope, opts);
201
+
202
+ const content = retained.map((r) => JSON.stringify(r)).join('\n') + (retained.length > 0 ? '\n' : '');
203
+ mkdirSync(dirname(journalPath), { recursive: true });
204
+ atomicWriteFile(journalPath, content);
205
+
206
+ return { prunedCount, retainedCount: retained.length };
207
+ }
208
+
209
+ /** Reconstruct or initialize the Receipt Journal from state. */
210
+ export async function reconstructJournalFromState(
211
+ scope: Scope,
212
+ opts: JournalOptions = {},
213
+ ): Promise<JournalReadResult> {
214
+ return readReceiptJournal(scope, opts);
215
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Receipt Journal types.
3
+ * See CONTEXT.md: Receipt Journal, Attempt Receipt, Receipt Resolution.
4
+ */
5
+
6
+ import type { Scope } from '../bridge-state/types.js';
7
+ import type { ValidationFinding } from '../registration/findings.js';
8
+ import type { AttemptReceipt } from '../registration/receipt.js';
9
+
10
+ export type ActiveCondition =
11
+ | 'pending-application'
12
+ | 'persistence-indeterminate'
13
+ | 'persistence-failed'
14
+ | 'journal-degradation';
15
+
16
+ export interface ActiveRecoveryChain {
17
+ /** Root receipt that entered the active condition. */
18
+ rootReceiptId: string;
19
+ scope: Scope;
20
+ condition: ActiveCondition;
21
+ stateRevision: string;
22
+ /** All receipts in this chain (root + follow-up attempts). */
23
+ receipts: AttemptReceipt[];
24
+ resolved: boolean;
25
+ resolvedByReceiptId?: string;
26
+ superseded: boolean;
27
+ supersededByReceiptId?: string;
28
+ }
29
+
30
+ export interface JournalReadResult {
31
+ receipts: AttemptReceipt[];
32
+ activeChains: ActiveRecoveryChain[];
33
+ allChains: ActiveRecoveryChain[];
34
+ corruptedLineCount: number;
35
+ isDegraded: boolean;
36
+ findings: ValidationFinding[];
37
+ error?: string;
38
+ }
39
+
40
+ export interface JournalAppendResult {
41
+ success: boolean;
42
+ error?: string;
43
+ finding?: ValidationFinding;
44
+ }
45
+
46
+ export interface JournalPruneResult {
47
+ prunedCount: number;
48
+ retainedCount: number;
49
+ }
@@ -0,0 +1,5 @@
1
+ export * from './refresh.js';
2
+ export * from './update-plan.js';
3
+ export * from './update.js';
4
+ export * from './rebind.js';
5
+ export * from './removal.js';
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Registration Rebind — explicit replacement of a Marketplace Registration's source locator or
3
+ * Git Selector under the preserved Registration ID.
4
+ * See CONTEXT.md: Registration Rebind, Update Plan, Apply Update, Source Key.
5
+ *
6
+ * The rebind preflight is a non-mutating validation of the REPLACEMENT source: fresh validation,
7
+ * a fresh Registration Confirmation and a complete Update Plan are required before the atomic
8
+ * commit performed by `applyUpdate` (plan kind `rebind`). Prior activation consent never carries
9
+ * over. Duplicate detection runs against every OTHER Registration in the scope.
10
+ */
11
+
12
+ import { readBridgeState } from '../bridge-state/store.js';
13
+ import type { Registration, Scope } from '../bridge-state/types.js';
14
+ import { inspectMarketplaceEntries } from '../installation/inspection.js';
15
+ import {
16
+ CODE,
17
+ RULE,
18
+ blocking,
19
+ sortFindings,
20
+ type ValidationFinding,
21
+ } from '../registration/findings.js';
22
+ import {
23
+ acquireGitSource,
24
+ cleanupAcquisition,
25
+ type GitExecutor,
26
+ } from '../registration/git-acquisition.js';
27
+ import { normalizeGitLocator } from '../registration/git-locator.js';
28
+ import {
29
+ normalizeGitSelector,
30
+ type GitSelectorInput,
31
+ } from '../registration/git-selector.js';
32
+ import { createReceipt, type AttemptReceipt } from '../registration/receipt.js';
33
+ import { findDuplicateRegistration, sourceKeyForLocalRoot } from '../registration/registration.js';
34
+ import { buildGitSnapshot, buildLocalSnapshot } from '../registration/snapshot.js';
35
+ import { gitSourceKey, type SourceKey } from '../registration/source-key.js';
36
+ import type { GitSelectorInput as StoredSelectorInput } from '../registration/git-selector.js';
37
+ import type { LifecycleFlowOptions, UpdateCandidate } from './refresh.js';
38
+ import { marketplaceNameOf, selectorInputFromStored } from './refresh.js';
39
+ import type { RebindSourceAttributes } from './update-plan.js';
40
+
41
+ export type RebindTarget =
42
+ | { kind: 'local'; rootPath: string }
43
+ | { kind: 'git'; locator: string; selector: GitSelectorInput | string };
44
+
45
+ export interface RebindPreflightOk {
46
+ candidate: UpdateCandidate;
47
+ /** Observed State Revision the Update Plan must bind. */
48
+ stateRevision: string;
49
+ rebindSource: RebindSourceAttributes;
50
+ }
51
+
52
+ export type RebindPreflightResult =
53
+ | { ok: true; preflight: RebindPreflightOk }
54
+ | {
55
+ ok: false;
56
+ outcome:
57
+ | { status: 'blocked'; findings: ValidationFinding[]; receipt: AttemptReceipt }
58
+ | { status: 'persistence-failed'; findings: ValidationFinding[]; receipt: AttemptReceipt; isIndeterminate: boolean };
59
+ };
60
+
61
+ const OPERATION = 'Registration Rebind';
62
+
63
+ function blocked(scope: Scope, trigger: string, revision: string, findings: ValidationFinding[], validationSnapshot?: string): RebindPreflightResult {
64
+ return {
65
+ ok: false,
66
+ outcome: {
67
+ status: 'blocked',
68
+ findings: sortFindings(findings),
69
+ receipt: createReceipt({
70
+ operation: OPERATION,
71
+ scope,
72
+ trigger,
73
+ expectedStateRevision: revision,
74
+ validationSnapshot,
75
+ summary: 'Blocked',
76
+ findings,
77
+ }),
78
+ },
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Inspection only reads the Registration's identity (id) when an explicit root + base snapshot
84
+ * are given, so a minimal record suffices without any cast.
85
+ */
86
+ function identityProbe(registrationId: string): Registration {
87
+ return { id: registrationId };
88
+ }
89
+
90
+ /**
91
+ * Validate a replacement Marketplace Source for one existing Registration. Non-mutating: no fence,
92
+ * no writes; consent and atomic application happen through the Update Plan and `applyUpdate`.
93
+ */
94
+ export async function preflightRebind(
95
+ scope: Scope,
96
+ registrationId: string,
97
+ target: RebindTarget,
98
+ opts: LifecycleFlowOptions = {},
99
+ ): Promise<RebindPreflightResult> {
100
+ const read = await readBridgeState(scope, { cwd: opts.cwd, agentDir: opts.agentDir });
101
+ if (read.status !== 'ok' && read.status !== 'missing') {
102
+ const findings: ValidationFinding[] = [
103
+ blocking({
104
+ code: CODE.PERSISTENCE_INDETERMINATE,
105
+ rule: 'PERSIST-01',
106
+ target: 'attempt',
107
+ pointer: '',
108
+ outcome: read.error ?? 'Bridge State is not readable; neither previous nor target verifiable',
109
+ scope,
110
+ phase: 'persistence',
111
+ }),
112
+ ];
113
+ return {
114
+ ok: false,
115
+ outcome: {
116
+ status: 'persistence-failed',
117
+ findings,
118
+ receipt: createReceipt({ operation: OPERATION, scope, trigger: `rebind ${registrationId}`, expectedStateRevision: '?', summary: 'Persistence Indeterminate', findings }),
119
+ isIndeterminate: true,
120
+ },
121
+ };
122
+ }
123
+ const state = read.state!;
124
+ const revision = state.stateRevision;
125
+ const registration = state.registrations.find((item) => item.id === registrationId);
126
+ if (!registration) {
127
+ return blocked(scope, `rebind ${registrationId}`, revision, [
128
+ blocking({ code: CODE.REGISTRATION_NOT_FOUND, rule: RULE.REGISTRATION_NOT_FOUND, target: 'registration', pointer: '', outcome: `Registration '${registrationId}' is not in ${scope} Bridge State`, scope, phase: 'admission' }),
129
+ ]);
130
+ }
131
+
132
+ // Duplicate detection runs against every OTHER Registration — the rebound one keeps its identity.
133
+ const others = state.registrations.filter((item) => item.id !== registrationId);
134
+ if (target.kind === 'local') {
135
+ return rebindToLocal(scope, registrationId, target.rootPath, others, revision, registration.validationSnapshot);
136
+ }
137
+ return rebindToGit(scope, registrationId, target.locator, target.selector, others, revision, registration.validationSnapshot, opts);
138
+ }
139
+
140
+ function rebindToLocal(
141
+ scope: Scope,
142
+ registrationId: string,
143
+ rootPath: string,
144
+ others: Registration[],
145
+ revision: string,
146
+ recordedSnapshot?: string,
147
+ ): RebindPreflightResult {
148
+ const trigger = `rebind ${registrationId}`;
149
+ const keyRes = sourceKeyForLocalRoot(rootPath, scope);
150
+ if (!keyRes.ok) return blocked(scope, trigger, revision, keyRes.findings, recordedSnapshot);
151
+ const sourceKey = keyRes.sourceKey!;
152
+
153
+ const dup = findDuplicateRegistration(scope, sourceKey, others);
154
+ if (dup.duplicate) return blocked(scope, trigger, revision, [dup.finding!], recordedSnapshot);
155
+
156
+ const snap = buildLocalSnapshot(sourceKey.canonicalPath!, sourceKey, scope);
157
+ if (!snap.ok || !snap.snapshot) return blocked(scope, trigger, revision, snap.findings, recordedSnapshot);
158
+
159
+ const inspection = inspectMarketplaceEntries(identityProbe(registrationId), scope, {
160
+ root: sourceKey.canonicalPath!,
161
+ baseSnapshot: snap.snapshot,
162
+ ignoreRecordedDrift: true,
163
+ });
164
+ const name = marketplaceNameOf(registrationId, inspection.marketplaceId);
165
+ if (!inspection.marketplaceId || inspection.findings.some((f) => f.classification === 'blocking')) {
166
+ return blocked(scope, trigger, revision, inspection.findings.length > 0 ? inspection.findings : [
167
+ blocking({ code: CODE.CATALOG_MISSING, rule: RULE.CATALOG_MISSING, target: 'catalog', pointer: '', outcome: 'replacement source has no readable Marketplace Catalog', scope, phase: 'validation' }),
168
+ ], snap.snapshot.fingerprint);
169
+ }
170
+
171
+ const candidate: UpdateCandidate = {
172
+ scope,
173
+ registrationId,
174
+ stateRevision: revision,
175
+ recordedFingerprint: undefined,
176
+ snapshot: snap.snapshot,
177
+ marketplaceName: name,
178
+ catalog: { name, entries: inspection.entries.map((item) => item.entry) },
179
+ inspection,
180
+ sourceKey,
181
+ };
182
+
183
+ return {
184
+ ok: true,
185
+ preflight: {
186
+ candidate,
187
+ stateRevision: revision,
188
+ rebindSource: { sourceKind: 'local', source: sourceKey.canonicalPath!, sourceKey },
189
+ },
190
+ };
191
+ }
192
+
193
+ async function rebindToGit(
194
+ scope: Scope,
195
+ registrationId: string,
196
+ locatorInput: string,
197
+ selectorInput: GitSelectorInput | string,
198
+ others: Registration[],
199
+ revision: string,
200
+ recordedSnapshot: string | undefined,
201
+ opts: LifecycleFlowOptions,
202
+ ): Promise<RebindPreflightResult> {
203
+ const trigger = `rebind ${registrationId}`;
204
+ const locRes = normalizeGitLocator(locatorInput, scope);
205
+ if (!locRes.ok) return blocked(scope, trigger, revision, locRes.findings, recordedSnapshot);
206
+
207
+ let selRes;
208
+ if (typeof selectorInput === 'string') {
209
+ selRes = await import('../registration/git-selector.js').then((m) => m.parseGitSelectorString(selectorInput, scope));
210
+ } else {
211
+ selRes = normalizeGitSelector(selectorInput as StoredSelectorInput, scope);
212
+ }
213
+ if (!selRes.ok) return blocked(scope, trigger, revision, selRes.findings, recordedSnapshot);
214
+ const selector = selRes.selector!;
215
+
216
+ const sourceKey = gitSourceKey(locRes.locator!, selector);
217
+
218
+ const dup = findDuplicateRegistration(scope, sourceKey, others);
219
+ if (dup.duplicate) return blocked(scope, trigger, revision, [dup.finding!]);
220
+
221
+ const acq = await acquireGitSource({
222
+ scope,
223
+ locator: locRes.locator!,
224
+ selector,
225
+ executor: opts.executor,
226
+ });
227
+ if (!acq.ok) return blocked(scope, trigger, revision, acq.findings, recordedSnapshot);
228
+
229
+ try {
230
+ const resolvedRevision = acq.resolvedRevision!;
231
+ const boundKey: SourceKey = {
232
+ ...sourceKey,
233
+ resolvedRevision,
234
+ canonicalUrl: locRes.locator!.canonicalUrl,
235
+ selector: selector.canonical,
236
+ };
237
+ const snap = buildGitSnapshot(acq.acquiredPath!, boundKey, scope, {
238
+ canonicalLocator: locRes.locator!.canonicalUrl,
239
+ resolvedRevision,
240
+ selectorCanonical: selector.canonical,
241
+ });
242
+ if (!snap.ok || !snap.snapshot) return blocked(scope, trigger, revision, snap.findings, recordedSnapshot);
243
+
244
+ const inspection = inspectMarketplaceEntries(identityProbe(registrationId), scope, {
245
+ root: acq.acquiredPath!,
246
+ baseSnapshot: snap.snapshot,
247
+ ignoreRecordedDrift: true,
248
+ });
249
+ const name = marketplaceNameOf(registrationId, inspection.marketplaceId);
250
+ if (!inspection.marketplaceId || inspection.findings.some((f) => f.classification === 'blocking')) {
251
+ return blocked(scope, trigger, revision, inspection.findings, snap.snapshot.fingerprint);
252
+ }
253
+
254
+ const candidate: UpdateCandidate = {
255
+ scope,
256
+ registrationId,
257
+ stateRevision: revision,
258
+ recordedFingerprint: undefined,
259
+ snapshot: snap.snapshot,
260
+ marketplaceName: name,
261
+ catalog: { name, entries: inspection.entries.map((item) => item.entry) },
262
+ inspection,
263
+ sourceKey: boundKey,
264
+ canonicalLocator: locRes.locator!.canonicalUrl,
265
+ resolvedRevision,
266
+ selectorCanonical: selector.canonical,
267
+ };
268
+
269
+ return {
270
+ ok: true,
271
+ preflight: {
272
+ candidate,
273
+ stateRevision: revision,
274
+ rebindSource: {
275
+ sourceKind: 'git',
276
+ source: locRes.locator!.canonicalUrl,
277
+ sourceKey: boundKey,
278
+ canonicalLocator: locRes.locator!.canonicalUrl,
279
+ gitSelector: { kind: selector.kind, canonical: selector.canonical, raw: selector.raw },
280
+ resolvedRevision,
281
+ },
282
+ },
283
+ };
284
+ } finally {
285
+ cleanupAcquisition(acq.acquiredPath!);
286
+ }
287
+ }
288
+
289
+ // Re-exported for callers reconstructing selectors from persisted state.
290
+ export { selectorInputFromStored };