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,407 @@
1
+ /**
2
+ * Lifecycle module — Refresh / Update Candidate / Apply Update / Rebind / Removal.
3
+ * See CONTEXT.md: Marketplace Refresh, Update Candidate, Update Plan, Apply Update,
4
+ * Registration Rebind, Registration Removal, Installation Removal, Lifecycle Operation.
5
+ *
6
+ * Marketplace Refresh is a non-mutating inspection: it never writes Bridge State and never
7
+ * acquires the Attempt Fence (checks and Refresh stay available while mutations are barred).
8
+ * It produces either no change or an Update Candidate applied only by a separate
9
+ * Lifecycle Operation bound to a complete Update Plan.
10
+ */
11
+
12
+ import { readBridgeState } from '../bridge-state/store.js';
13
+ import type { BridgeState } from '../bridge-state/types.js';
14
+ import type { Registration, Scope } from '../bridge-state/types.js';
15
+ import type { MarketplaceInspection } from '../installation/inspection.js';
16
+ import { inspectMarketplaceEntries } from '../installation/inspection.js';
17
+ import type { Catalog } from '../registration/catalog.js';
18
+ import {
19
+ CODE,
20
+ RULE,
21
+ blocking,
22
+ sortFindings,
23
+ type ValidationFinding,
24
+ } from '../registration/findings.js';
25
+ import {
26
+ acquireGitSource,
27
+ cleanupAcquisition,
28
+ resolveGitRevision,
29
+ type GitExecutor,
30
+ } from '../registration/git-acquisition.js';
31
+ import { SourceCache } from '../cache/source-cache.js';
32
+ import { normalizeGitLocator } from '../registration/git-locator.js';
33
+ import {
34
+ normalizeGitSelector,
35
+ type GitSelectorInput,
36
+ } from '../registration/git-selector.js';
37
+ import { createReceipt, type AttemptReceipt } from '../registration/receipt.js';
38
+ import {
39
+ buildGitSnapshot,
40
+ buildLocalSnapshot,
41
+ type ValidationSnapshot,
42
+ } from '../registration/snapshot.js';
43
+ import type { SourceKey } from '../registration/source-key.js';
44
+
45
+ export interface LifecycleFlowOptions {
46
+ cwd?: string;
47
+ agentDir?: string;
48
+ /** Host-owned Project Trust decision; refresh is non-mutating so it stays readable without it. */
49
+ projectTrusted?: boolean;
50
+ fenceTimeoutMs?: number;
51
+ /** Injected git executor for tests. */
52
+ executor?: GitExecutor;
53
+ /** Injected Source Cache (defaults to a cache under the given agentDir). */
54
+ cache?: SourceCache;
55
+ }
56
+
57
+ /**
58
+ * A newly validated source state for one Registration that differs from its recorded
59
+ * Validation Snapshot. Applied only by a separate Lifecycle Operation (Apply Update /
60
+ * Registration Rebind) under a complete Update Plan.
61
+ */
62
+ export interface UpdateCandidate {
63
+ scope: Scope;
64
+ registrationId: string;
65
+ /** State Revision observed while this candidate was validated; plans bind exactly this. */
66
+ stateRevision: string;
67
+ /** Recorded Validation Snapshot fingerprint before the refresh. */
68
+ recordedFingerprint?: string;
69
+ recordedResolvedRevision?: string;
70
+ /** Newly validated snapshot with full tree + binds (base-tree fingerprint, as registered). */
71
+ snapshot: ValidationSnapshot;
72
+ marketplaceName: string;
73
+ catalog: Catalog;
74
+ inspection: MarketplaceInspection;
75
+ sourceKey: SourceKey;
76
+ /** Git-only binds of the candidate state. */
77
+ canonicalLocator?: string;
78
+ resolvedRevision?: string;
79
+ selectorCanonical?: string;
80
+ }
81
+
82
+ export type RefreshOutcome =
83
+ | { status: 'no-change'; receipt: AttemptReceipt }
84
+ | { status: 'update-candidate'; candidate: UpdateCandidate; receipt: AttemptReceipt }
85
+ | { status: 'blocked'; findings: ValidationFinding[]; receipt: AttemptReceipt };
86
+
87
+ const OPERATION = 'Marketplace Refresh';
88
+
89
+ function finding(scope: Scope, code: string, rule: string, target: ValidationFinding['target'], pointer: string, outcome: string): ValidationFinding {
90
+ return blocking({ code, rule, target, pointer, outcome, scope, phase: 'validation' });
91
+ }
92
+
93
+ function blocked(scope: Scope, registrationId: string, revision: string, findings: ValidationFinding[], snapshot?: string): RefreshOutcome {
94
+ return {
95
+ status: 'blocked',
96
+ findings: sortFindings(findings),
97
+ receipt: createReceipt({
98
+ operation: OPERATION,
99
+ scope,
100
+ trigger: `refresh ${registrationId}`,
101
+ expectedStateRevision: revision,
102
+ validationSnapshot: snapshot,
103
+ summary: 'Blocked',
104
+ findings,
105
+ }),
106
+ };
107
+ }
108
+
109
+ /** Marketplace name embedded in an inspection Marketplace ID ('<registrationId>/<name>'). */
110
+ export function marketplaceNameOf(registrationId: string, marketplaceId: string | undefined): string {
111
+ return marketplaceId && marketplaceId.startsWith(`${registrationId}/`)
112
+ ? marketplaceId.slice(registrationId.length + 1)
113
+ : '';
114
+ }
115
+
116
+ /** Reconstruct a selector input from a persisted canonical Git Selector. */
117
+ export function selectorInputFromStored(gs: NonNullable<Registration['gitSelector']>): GitSelectorInput {
118
+ switch (gs.kind) {
119
+ case 'default':
120
+ return { kind: 'default' };
121
+ case 'branch':
122
+ return { kind: 'branch', value: gs.canonical.replace(/^refs\/heads\//, '') };
123
+ case 'tag':
124
+ return { kind: 'tag', value: gs.canonical.replace(/^refs\/tags\//, '') };
125
+ case 'commit':
126
+ return { kind: 'commit', value: gs.canonical };
127
+ }
128
+ }
129
+
130
+ async function readStateForRefresh(scope: Scope, opts: LifecycleFlowOptions) {
131
+ return readBridgeState(scope, { cwd: opts.cwd, agentDir: opts.agentDir });
132
+ }
133
+
134
+ /**
135
+ * Explicit, non-mutating inspection of the current Marketplace Source for one Registration.
136
+ * Local sources re-walk the live tree; Git sources re-resolve the selector — a changed
137
+ * Resolved Revision requires new validation (a full-commit selector cannot move, so ref
138
+ * movement alone never produces an Update Candidate). Bridge State is never written.
139
+ */
140
+ export async function refreshRegistration(
141
+ scope: Scope,
142
+ registrationId: string,
143
+ opts: LifecycleFlowOptions = {},
144
+ ): Promise<RefreshOutcome> {
145
+ const read = await readStateForRefresh(scope, opts);
146
+ if (read.status !== 'ok' && read.status !== 'missing') {
147
+ return blocked(
148
+ scope,
149
+ registrationId,
150
+ '?',
151
+ [finding(scope, CODE.PERSISTENCE_INDETERMINATE, 'PERSIST-01', 'attempt', '', read.error ?? 'Bridge State is not readable; neither previous nor target verifiable')],
152
+ );
153
+ }
154
+ const state = read.state!;
155
+ const revision = state.stateRevision;
156
+ const registration = state.registrations.find((item) => item.id === registrationId);
157
+ if (!registration) {
158
+ return blocked(scope, registrationId, revision, [
159
+ finding(scope, CODE.REGISTRATION_NOT_FOUND, RULE.REGISTRATION_NOT_FOUND, 'registration', '', `Registration '${registrationId}' is not in ${scope} Bridge State`),
160
+ ]);
161
+ }
162
+
163
+ if (registration.sourceKind === 'git') {
164
+ return refreshGitRegistration(scope, registration, revision, opts);
165
+ }
166
+ return refreshLocalRegistration(scope, registration, revision, opts);
167
+ }
168
+
169
+ function refreshLocalRegistration(
170
+ scope: Scope,
171
+ registration: Registration,
172
+ revision: string,
173
+ opts: LifecycleFlowOptions,
174
+ ): RefreshOutcome {
175
+ if (!registration.sourceKey || !registration.source) {
176
+ return blocked(scope, registration.id, revision, [
177
+ finding(scope, CODE.SOURCE_REACQUISITION_REQUIRED, RULE.SOURCE_REACQUISITION_REQUIRED, 'registration', '', 'Registration has no retained local Source Key to revalidate'),
178
+ ], registration.validationSnapshot);
179
+ }
180
+
181
+ const snap = buildLocalSnapshot(registration.source, registration.sourceKey, scope);
182
+ if (!snap.ok || !snap.snapshot) {
183
+ return blocked(scope, registration.id, revision, snap.findings, registration.validationSnapshot);
184
+ }
185
+
186
+ if (!registration.validationSnapshot || snap.snapshot.fingerprint !== registration.validationSnapshot) {
187
+ const inspection = inspectMarketplaceEntries(registration, scope, { ignoreRecordedDrift: true });
188
+ const name = marketplaceNameOf(registration.id, inspection.marketplaceId) || registration.marketplaceName || '';
189
+ const candidate: UpdateCandidate = {
190
+ scope,
191
+ registrationId: registration.id,
192
+ stateRevision: revision,
193
+ recordedFingerprint: registration.validationSnapshot,
194
+ recordedResolvedRevision: registration.resolvedRevision,
195
+ snapshot: snap.snapshot,
196
+ marketplaceName: name,
197
+ catalog: { name, entries: inspection.entries.map((item) => item.entry) },
198
+ inspection,
199
+ sourceKey: registration.sourceKey,
200
+ };
201
+ return {
202
+ status: 'update-candidate',
203
+ candidate,
204
+ receipt: createReceipt({
205
+ operation: OPERATION,
206
+ scope,
207
+ trigger: `refresh ${registration.id}`,
208
+ expectedStateRevision: revision,
209
+ validationSnapshot: snap.snapshot.fingerprint,
210
+ summary: 'Completed',
211
+ findings: inspection.findings,
212
+ stateChanged: false,
213
+ }),
214
+ };
215
+ }
216
+
217
+ return {
218
+ status: 'no-change',
219
+ receipt: createReceipt({
220
+ operation: OPERATION,
221
+ scope,
222
+ trigger: `refresh ${registration.id}`,
223
+ expectedStateRevision: revision,
224
+ validationSnapshot: registration.validationSnapshot,
225
+ summary: 'Completed',
226
+ stateChanged: false,
227
+ }),
228
+ };
229
+ }
230
+
231
+ /** Reconstruct the full Source Key for a Registration at an exact Resolved Revision. */
232
+ function sourceKeyAt(registration: Registration, resolvedRevision: string): SourceKey {
233
+ return { ...registration.sourceKey!, resolvedRevision };
234
+ }
235
+
236
+ async function refreshGitRegistration(
237
+ scope: Scope,
238
+ registration: Registration,
239
+ revision: string,
240
+ opts: LifecycleFlowOptions,
241
+ ): Promise<RefreshOutcome> {
242
+ if (!registration.canonicalLocator || !registration.gitSelector || !registration.sourceKey) {
243
+ return blocked(scope, registration.id, revision, [
244
+ finding(scope, CODE.SOURCE_REACQUISITION_REQUIRED, RULE.SOURCE_REACQUISITION_REQUIRED, 'registration', '', 'Registration has no retained Canonical Git Locator / Git Selector to revalidate'),
245
+ ], registration.validationSnapshot);
246
+ }
247
+
248
+ const locRes = normalizeGitLocator(registration.canonicalLocator, scope);
249
+ if (!locRes.ok) return blocked(scope, registration.id, revision, locRes.findings, registration.validationSnapshot);
250
+ const selRes = normalizeGitSelector(selectorInputFromStored(registration.gitSelector), scope);
251
+ if (!selRes.ok) return blocked(scope, registration.id, revision, selRes.findings, registration.validationSnapshot);
252
+ const locator = locRes.locator!;
253
+ const selector = selRes.selector!;
254
+ const cache = opts.cache ?? new SourceCache({ agentDir: opts.agentDir });
255
+
256
+ const noChangeReceipt = () =>
257
+ createReceipt({
258
+ operation: OPERATION,
259
+ scope,
260
+ trigger: `refresh ${registration.id}`,
261
+ expectedStateRevision: revision,
262
+ validationSnapshot: registration.validationSnapshot,
263
+ summary: 'Completed',
264
+ findings: [],
265
+ stateChanged: false,
266
+ });
267
+
268
+ // Cheap resolution first — a hit path never needs a clone.
269
+ const resolution = await resolveGitRevision(locator, selector, scope, { executor: opts.executor });
270
+
271
+ if (!resolution.ok) {
272
+ // Offline / unreachable remote: only an exact fingerprint hit may be reused. The cached
273
+ // tree is re-hashed and must equal the recorded Validation Snapshot exactly — any mismatch
274
+ // is Source Drift (Blocking Finding); a Stale Snapshot is never converted into success.
275
+ const offline = registration.validationSnapshot
276
+ ? await cache.offlineHit(locator.canonicalUrl, selector.canonical, registration.validationSnapshot)
277
+ : null;
278
+ if (offline) {
279
+ const verified = buildGitSnapshot(offline.path, sourceKeyAt(registration, registration.resolvedRevision!), scope, {
280
+ canonicalLocator: locator.canonicalUrl,
281
+ resolvedRevision: registration.resolvedRevision!,
282
+ selectorCanonical: selector.canonical,
283
+ });
284
+ if (verified.ok && verified.snapshot!.fingerprint === registration.validationSnapshot) {
285
+ return { status: 'no-change', receipt: noChangeReceipt() };
286
+ }
287
+ return blocked(scope, registration.id, revision, [
288
+ finding(scope, CODE.SOURCE_DRIFT, RULE.SOURCE_DRIFT, 'registration', '', `Source Drift: cached tree at fingerprint ${String(registration.validationSnapshot).slice(0, 16)}… no longer hashes to the recorded Validation Snapshot; Marketplace Refresh against a reachable source is required`),
289
+ ], registration.validationSnapshot);
290
+ }
291
+ return blocked(scope, registration.id, revision, resolution.findings, registration.validationSnapshot);
292
+ }
293
+
294
+ const resolvedRevision = resolution.sha;
295
+
296
+ if (resolvedRevision === registration.resolvedRevision && registration.validationSnapshot) {
297
+ // Same Resolved Revision: exact-fingerprint cache hit avoids a full acquisition.
298
+ const hit = await cache.hitExact(registration.validationSnapshot);
299
+ if (hit) {
300
+ const verified = buildGitSnapshot(hit.path, sourceKeyAt(registration, resolvedRevision), scope, {
301
+ canonicalLocator: locator.canonicalUrl,
302
+ resolvedRevision,
303
+ selectorCanonical: selector.canonical,
304
+ });
305
+ if (verified.ok && verified.snapshot!.fingerprint === registration.validationSnapshot) {
306
+ return { status: 'no-change', receipt: noChangeReceipt() };
307
+ }
308
+ // Tampered/evicted-in-place entry: fall through to full acquisition below.
309
+ }
310
+ }
311
+
312
+ // Full non-executing acquisition (clone).
313
+ const acq = await acquireGitSource({
314
+ scope,
315
+ locator,
316
+ selector,
317
+ executor: opts.executor,
318
+ });
319
+ if (!acq.ok) return blocked(scope, registration.id, revision, acq.findings, registration.validationSnapshot);
320
+
321
+ try {
322
+ const resolvedRevision = acq.resolvedRevision!;
323
+ const root = acq.acquiredPath!;
324
+ if (resolvedRevision === registration.resolvedRevision) {
325
+ // Cache the re-validated tree under its recorded fingerprint for future exact hits.
326
+ if (registration.validationSnapshot) {
327
+ await cache.storeTree(root, registration.validationSnapshot);
328
+ cache.recordIndex({
329
+ fingerprint: registration.validationSnapshot,
330
+ resolvedRevision,
331
+ canonicalLocator: locator.canonicalUrl,
332
+ selectorCanonical: selector.canonical,
333
+ });
334
+ }
335
+ return {
336
+ status: 'no-change',
337
+ receipt: createReceipt({
338
+ operation: OPERATION,
339
+ scope,
340
+ trigger: `refresh ${registration.id}`,
341
+ expectedStateRevision: revision,
342
+ validationSnapshot: registration.validationSnapshot,
343
+ summary: 'Completed',
344
+ stateChanged: false,
345
+ }),
346
+ };
347
+ }
348
+
349
+ // Resolved Revision changed ⇒ new validation is mandatory before any confirmation.
350
+ const newSourceKey: SourceKey = { ...registration.sourceKey, resolvedRevision };
351
+ const snap = buildGitSnapshot(root, newSourceKey, scope, {
352
+ canonicalLocator: locator.canonicalUrl,
353
+ resolvedRevision,
354
+ selectorCanonical: selector.canonical,
355
+ });
356
+ if (!snap.ok || !snap.snapshot) {
357
+ return blocked(scope, registration.id, revision, snap.findings, registration.validationSnapshot);
358
+ }
359
+ const inspection = inspectMarketplaceEntries(registration, scope, {
360
+ root,
361
+ baseSnapshot: snap.snapshot,
362
+ ignoreRecordedDrift: true,
363
+ });
364
+ const name = marketplaceNameOf(registration.id, inspection.marketplaceId) || registration.marketplaceName || '';
365
+ const candidate: UpdateCandidate = {
366
+ scope,
367
+ registrationId: registration.id,
368
+ stateRevision: revision,
369
+ recordedFingerprint: registration.validationSnapshot,
370
+ recordedResolvedRevision: registration.resolvedRevision,
371
+ snapshot: snap.snapshot,
372
+ marketplaceName: name,
373
+ catalog: { name, entries: inspection.entries.map((item) => item.entry) },
374
+ inspection,
375
+ sourceKey: newSourceKey,
376
+ canonicalLocator: locator.canonicalUrl,
377
+ resolvedRevision,
378
+ selectorCanonical: selector.canonical,
379
+ };
380
+ // Persist the candidate tree in the cache and pin its fingerprint as a pending Update
381
+ // Candidate — pinned entries are never evicted by LRU pruning.
382
+ await cache.storeTree(root, snap.snapshot!.fingerprint);
383
+ cache.recordIndex({
384
+ fingerprint: snap.snapshot!.fingerprint,
385
+ resolvedRevision,
386
+ canonicalLocator: locator.canonicalUrl,
387
+ selectorCanonical: selector.canonical,
388
+ });
389
+ cache.recordPendingUpdate({ scope, registrationId: registration.id, fingerprint: snap.snapshot!.fingerprint });
390
+ return {
391
+ status: 'update-candidate',
392
+ candidate,
393
+ receipt: createReceipt({
394
+ operation: OPERATION,
395
+ scope,
396
+ trigger: `refresh ${registration.id}`,
397
+ expectedStateRevision: revision,
398
+ validationSnapshot: snap.snapshot.fingerprint,
399
+ summary: 'Completed',
400
+ findings: inspection.findings,
401
+ stateChanged: false,
402
+ }),
403
+ };
404
+ } finally {
405
+ cleanupAcquisition(acq.acquiredPath!);
406
+ }
407
+ }