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,305 @@
1
+ /**
2
+ * Attempt Receipt — redacted, immutable, non-authoritative record of one Bridge-managed attempt.
3
+ * See CONTEXT.md: Attempt Receipt, Attempt Summary, Validation Finding, Recovery Action.
4
+ *
5
+ * Relates expected / target / observed State Revisions with the applicable Validation Snapshot,
6
+ * outcomes, findings. Received object is frozen (immutable) and secret-bearing input is redacted.
7
+ */
8
+
9
+ import { randomUUID } from 'node:crypto';
10
+
11
+ import type { Scope } from '../bridge-state/types.js';
12
+ import { CODE, hasBlocking, sortFindings, type ValidationFinding } from './findings.js';
13
+ import { redactSource } from './source-key.js';
14
+
15
+ /** Closed set of 8 Attempt Summary values (CONTEXT.md). */
16
+ export type AttemptSummary =
17
+ | 'Completed'
18
+ | 'Completed with diagnostics'
19
+ | 'Declined'
20
+ | 'Blocked'
21
+ | 'Rejected as Stale'
22
+ | 'Persistence Failed'
23
+ | 'Persistence Indeterminate'
24
+ | 'Pending Application';
25
+
26
+ /** Closed set of 9 Recovery Actions (CONTEXT.md & Issue #9). */
27
+ export type RecoveryAction =
28
+ | 'Retry'
29
+ | 'Revalidate'
30
+ | 'Refresh'
31
+ | 'Rebind'
32
+ | 'Retry Application'
33
+ | 'Disable'
34
+ | 'Remove'
35
+ | 'Repair State'
36
+ | 'Inspect';
37
+
38
+ export type ReceiptKind =
39
+ | 'Lifecycle Operation'
40
+ | 'Marketplace Refresh'
41
+ | 'Runtime Application'
42
+ | 'Reconciliation'
43
+ | 'State Repair';
44
+
45
+ export type DurableOutcome = 'committed' | 'unchanged' | 'failed' | 'indeterminate';
46
+ export type RuntimeOutcome = 'applied' | 'pending-application' | 'none';
47
+
48
+ export interface AttemptReceipt {
49
+ /** Opaque receipt id (rcpt_<uuid>). */
50
+ id: string;
51
+ kind: ReceiptKind;
52
+ /** Operation name (e.g. "Marketplace Registration", "Plugin Installation", etc.) */
53
+ operation: string;
54
+ scope: Scope;
55
+ /** Redacted trigger description. */
56
+ trigger: string;
57
+ /** ISO timestamp when attempt started. */
58
+ startedAt: string;
59
+ /** ISO timestamp when attempt completed. */
60
+ completedAt: string;
61
+ /** State Revision expected before preflight. */
62
+ expectedStateRevision: string;
63
+ /** State Revision targeted to commit. */
64
+ targetStateRevision?: string;
65
+ /** State Revision observed after commit/verify. */
66
+ observedStateRevision?: string;
67
+ /** Bound Validation Snapshot fingerprint. */
68
+ validationSnapshot?: string;
69
+ /** Array of snapshot fingerprints involved. */
70
+ snapshotFingerprints?: string[];
71
+ /** Three-orthogonal axis 1: Durable persistence outcome. */
72
+ durableOutcome: DurableOutcome;
73
+ /** Three-orthogonal axis 2: Findings. Redacted and sorted. */
74
+ findings: ValidationFinding[];
75
+ /** Three-orthogonal axis 3: Runtime reload/participation outcome. */
76
+ runtimeOutcome: RuntimeOutcome;
77
+ /** User-visible closed Attempt Summary. */
78
+ summary: AttemptSummary;
79
+ /** Closed Recovery Actions eligible under current state. */
80
+ recoveryActions: RecoveryAction[];
81
+ /** Whether durable state was modified. */
82
+ stateChanged: boolean;
83
+ /** Active recovery chain link: receipt id this attempt seeks to recover. */
84
+ recoversReceiptId?: string;
85
+ /** Receipt id this attempt supersedes with a replacement commit. */
86
+ supersedesReceiptId?: string;
87
+ /** ISO timestamp of receipt creation. */
88
+ createdAt: string;
89
+ }
90
+
91
+ export interface ReceiptOptions {
92
+ id?: string;
93
+ kind?: ReceiptKind;
94
+ operation: string;
95
+ scope: Scope;
96
+ trigger: string;
97
+ startedAt?: string;
98
+ completedAt?: string;
99
+ expectedStateRevision: string;
100
+ targetStateRevision?: string;
101
+ observedStateRevision?: string;
102
+ validationSnapshot?: string;
103
+ snapshotFingerprints?: string[];
104
+ durableOutcome?: DurableOutcome;
105
+ runtimeOutcome?: RuntimeOutcome;
106
+ findings?: ValidationFinding[];
107
+ summary?: AttemptSummary;
108
+ recoveryActions?: RecoveryAction[];
109
+ stateChanged?: boolean;
110
+ declined?: boolean;
111
+ recoversReceiptId?: string;
112
+ supersedesReceiptId?: string;
113
+ }
114
+
115
+ /** Derive closed Attempt Summary from the three orthogonal outcomes. */
116
+ export function deriveAttemptSummary(
117
+ durable: DurableOutcome,
118
+ findings: ValidationFinding[],
119
+ runtime: RuntimeOutcome,
120
+ opts: { declined?: boolean } = {},
121
+ ): AttemptSummary {
122
+ if (durable === 'indeterminate') return 'Persistence Indeterminate';
123
+ if (durable === 'failed') return 'Persistence Failed';
124
+ if (opts.declined) return 'Declined';
125
+
126
+ const hasStale = findings.some(
127
+ (f) => f.code === CODE.REJECTED_AS_STALE || f.rule === 'STALE-01' || f.rule === 'STALE-02',
128
+ );
129
+ if (hasStale && durable !== 'committed') return 'Rejected as Stale';
130
+
131
+ if (durable !== 'committed' && hasBlocking(findings)) return 'Blocked';
132
+
133
+ if (durable === 'committed') {
134
+ if (runtime === 'pending-application') return 'Pending Application';
135
+ const hasDiagnostics = findings.some((f) => f.classification === 'warning' || f.classification === 'notice');
136
+ return hasDiagnostics ? 'Completed with diagnostics' : 'Completed';
137
+ }
138
+
139
+ // If durable is unchanged and no blocking findings
140
+ if (runtime === 'applied') return 'Completed';
141
+ if (runtime === 'pending-application') return 'Pending Application';
142
+ return 'Blocked';
143
+ }
144
+
145
+ /** Derive closed Recovery Actions from the Attempt Summary and findings. */
146
+ export function deriveRecoveryActions(
147
+ summary: AttemptSummary,
148
+ findings: ValidationFinding[],
149
+ _context: { canRebind?: boolean; isInstalled?: boolean } = {},
150
+ ): RecoveryAction[] {
151
+ switch (summary) {
152
+ case 'Persistence Indeterminate':
153
+ return ['Repair State', 'Inspect'];
154
+ case 'Persistence Failed':
155
+ return ['Retry'];
156
+ case 'Rejected as Stale':
157
+ return ['Revalidate'];
158
+ case 'Pending Application':
159
+ return ['Retry Application'];
160
+ case 'Declined':
161
+ return [];
162
+ case 'Completed':
163
+ return [];
164
+ case 'Completed with diagnostics': {
165
+ const actions: RecoveryAction[] = [];
166
+ if (findings.some((f) => f.code === CODE.RUNTIME_SKILL_COLLISION)) {
167
+ actions.push('Inspect', 'Disable', 'Remove');
168
+ } else {
169
+ actions.push('Inspect');
170
+ }
171
+ return actions;
172
+ }
173
+ case 'Blocked': {
174
+ // Check finding types
175
+ if (findings.some((f) => f.code === CODE.SOURCE_DRIFT)) {
176
+ return ['Refresh'];
177
+ }
178
+ if (findings.some((f) => f.code.startsWith('GIT_LOCATOR_') || f.code.startsWith('GIT_SELECTOR_'))) {
179
+ return ['Rebind', 'Inspect'];
180
+ }
181
+ if (findings.some((f) => f.code === CODE.ATTEMPT_IN_PROGRESS || f.code === CODE.GLOBAL_PENDING_BARRIER)) {
182
+ return ['Inspect'];
183
+ }
184
+ if (findings.some((f) => f.code === CODE.PROJECT_TRUST_DENIED)) {
185
+ return ['Inspect'];
186
+ }
187
+ if (findings.some((f) => f.code === CODE.CATALOG_MISSING || f.code === CODE.CATALOG_MALFORMED)) {
188
+ return ['Inspect'];
189
+ }
190
+ if (findings.some((f) => f.code === CODE.BUDGET_EXCEEDED || f.code === CODE.PATH_CONTAINMENT_VIOLATION)) {
191
+ return ['Inspect'];
192
+ }
193
+ return ['Inspect'];
194
+ }
195
+ }
196
+ }
197
+
198
+ /** Format a Three-Orthogonal Diagnostic Report string. */
199
+ export function formatThreeOrthogonalReport(receipt: AttemptReceipt): string {
200
+ const lines: string[] = [];
201
+
202
+ // Persistence Partition
203
+ let persistenceStr = '';
204
+ if (receipt.durableOutcome === 'committed') {
205
+ persistenceStr = `已提交 (State Revision: ${receipt.observedStateRevision ?? receipt.targetStateRevision ?? '?'})`;
206
+ } else if (receipt.durableOutcome === 'failed') {
207
+ persistenceStr = `Persistence Failed (維持先前 State Revision ${receipt.expectedStateRevision})`;
208
+ } else if (receipt.durableOutcome === 'indeterminate') {
209
+ persistenceStr = `Persistence Indeterminate (狀態損毀或無法驗證,fail-closed)`;
210
+ } else {
211
+ persistenceStr = `未變更 (維持 State Revision ${receipt.expectedStateRevision})`;
212
+ }
213
+ lines.push(`【持久化 / Persistence】: ${persistenceStr}`);
214
+
215
+ // Findings Partition
216
+ const blockingCount = receipt.findings.filter((f) => f.classification === 'blocking').length;
217
+ const warningCount = receipt.findings.filter((f) => f.classification === 'warning').length;
218
+ const noticeCount = receipt.findings.filter((f) => f.classification === 'notice').length;
219
+ lines.push(`【診斷 / Findings】: ${blockingCount} Blocking · ${warningCount} Warning · ${noticeCount} Notice`);
220
+ for (const f of receipt.findings) {
221
+ const ptr = f.pointer ? ` @${f.pointer}` : '';
222
+ lines.push(` - [${f.classification.toUpperCase()}] ${f.code} (${f.rule}): ${f.outcome}${ptr}`);
223
+ }
224
+
225
+ // Runtime Partition
226
+ let runtimeStr = '';
227
+ if (receipt.runtimeOutcome === 'applied') {
228
+ runtimeStr = 'Applied (宿主驗證生效)';
229
+ } else if (receipt.runtimeOutcome === 'pending-application') {
230
+ runtimeStr = 'Pending Application (待重載/套用)';
231
+ } else {
232
+ runtimeStr = 'None (無運行時變更)';
233
+ }
234
+ lines.push(`【運行時 / Runtime】: ${runtimeStr}`);
235
+
236
+ // Summary & Recovery Actions
237
+ lines.push(`\nAttempt Summary: ${receipt.summary}`);
238
+ if (receipt.recoveryActions.length > 0) {
239
+ lines.push(`Recovery Actions: ${receipt.recoveryActions.join(', ')}`);
240
+ } else {
241
+ lines.push(`Recovery Actions: none required`);
242
+ }
243
+
244
+ return lines.join('\n');
245
+ }
246
+
247
+ /** Create an immutable, redacted Attempt Receipt. */
248
+ export function createReceipt(opts: ReceiptOptions): AttemptReceipt {
249
+ const startedAt = opts.startedAt ?? new Date().toISOString();
250
+ const completedAt = opts.completedAt ?? new Date().toISOString();
251
+ const sortedFindings = sortFindings([...(opts.findings ?? [])]);
252
+
253
+ let durable: DurableOutcome;
254
+ if (opts.durableOutcome) {
255
+ durable = opts.durableOutcome;
256
+ } else if (opts.summary === 'Persistence Indeterminate') {
257
+ durable = 'indeterminate';
258
+ } else if (opts.summary === 'Persistence Failed') {
259
+ durable = 'failed';
260
+ } else if (opts.summary === 'Completed' || opts.summary === 'Completed with diagnostics' || opts.stateChanged) {
261
+ durable = 'committed';
262
+ } else {
263
+ durable = 'unchanged';
264
+ }
265
+
266
+ let runtime: RuntimeOutcome;
267
+ if (opts.runtimeOutcome) {
268
+ runtime = opts.runtimeOutcome;
269
+ } else if (opts.summary === 'Pending Application') {
270
+ runtime = 'pending-application';
271
+ } else if (durable === 'committed') {
272
+ runtime = 'applied';
273
+ } else {
274
+ runtime = 'none';
275
+ }
276
+
277
+ const summary = opts.summary ?? deriveAttemptSummary(durable, sortedFindings, runtime, { declined: opts.declined });
278
+ const recoveryActions = opts.recoveryActions ?? deriveRecoveryActions(summary, sortedFindings);
279
+
280
+ const receipt: AttemptReceipt = {
281
+ id: opts.id ?? `rcpt_${randomUUID()}`,
282
+ kind: opts.kind ?? 'Lifecycle Operation',
283
+ operation: opts.operation,
284
+ scope: opts.scope,
285
+ trigger: redactSource(opts.trigger),
286
+ startedAt,
287
+ completedAt,
288
+ expectedStateRevision: opts.expectedStateRevision,
289
+ targetStateRevision: opts.targetStateRevision,
290
+ observedStateRevision: opts.observedStateRevision,
291
+ validationSnapshot: opts.validationSnapshot,
292
+ snapshotFingerprints: opts.snapshotFingerprints,
293
+ durableOutcome: durable,
294
+ findings: sortedFindings,
295
+ runtimeOutcome: runtime,
296
+ summary,
297
+ recoveryActions,
298
+ stateChanged: opts.stateChanged ?? (durable === 'committed'),
299
+ recoversReceiptId: opts.recoversReceiptId,
300
+ supersedesReceiptId: opts.supersedesReceiptId,
301
+ createdAt: completedAt,
302
+ };
303
+
304
+ return Object.freeze(receipt);
305
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Registration identity and duplicate detection.
3
+ * See CONTEXT.md: Registration ID, Registration Alias, Source Key, Marketplace ID.
4
+ *
5
+ * Registration ID: opaque immutable lowercase UUIDv4, generated locally, allocated BEFORE preflight
6
+ * so findings have stable derived identities; persisted only after Registration Confirmation; never
7
+ * reused after a failed or declined attempt.
8
+ *
9
+ * Registration Alias: optional scope-local human-readable handle, initially derived from a
10
+ * compatible declared Marketplace name; unique within its scope.
11
+ */
12
+
13
+ import { randomUUID } from 'node:crypto';
14
+
15
+ import { CODE, RULE, blocking } from './findings.js';
16
+ import type { ValidationFinding } from './findings.js';
17
+ import type { Scope } from '../bridge-state/types.js';
18
+ import type { Registration } from '../bridge-state/types.js';
19
+ import { localSourceKey, sourceKeyEquals, type SourceKey } from './source-key.js';
20
+
21
+ /** Allocate an immutable lowercase UUIDv4 before preflight. Never reused. */
22
+ export function allocateRegistrationId(): string {
23
+ return randomUUID();
24
+ }
25
+
26
+ /** Sanitize a declared marketplace name into an initial Registration Alias (scope-local unique). */
27
+ export function deriveInitialAlias(marketplaceName: string | undefined, existingAliases: string[]): string | undefined {
28
+ if (!marketplaceName || marketplaceName.trim().length === 0) return undefined;
29
+ let alias = marketplaceName.trim();
30
+ const taken = new Set(existingAliases);
31
+ if (!taken.has(alias)) return alias;
32
+ // scope-local uniqueness: append a numeric suffix
33
+ let n = 2;
34
+ while (taken.has(`${alias}-${n}`)) n += 1;
35
+ return `${alias}-${n}`;
36
+ }
37
+
38
+ export interface DuplicateCheckResult {
39
+ duplicate: boolean;
40
+ /** The existing registration matched by identical Source Key (same kind). */
41
+ existing?: Registration;
42
+ finding?: ValidationFinding;
43
+ }
44
+
45
+ /**
46
+ * Detect repeated registration within one scope by identical Source Key (same kind).
47
+ * Local and Git kinds remain distinct; equal keys across scopes do not merge registrations.
48
+ */
49
+ export function findDuplicateRegistration(
50
+ scope: Scope,
51
+ sourceKey: SourceKey,
52
+ registrations: Registration[],
53
+ ): DuplicateCheckResult {
54
+ for (const reg of registrations) {
55
+ if (!reg.sourceKey) continue;
56
+ if (sourceKeyEquals(reg.sourceKey, sourceKey)) {
57
+ return {
58
+ duplicate: true,
59
+ existing: reg,
60
+ finding: blocking({
61
+ code: CODE.DUPLICATE_SOURCE_KEY,
62
+ phase: 'identity',
63
+ target: 'registration',
64
+ scope,
65
+ pointer: `${sourceKey.kind}:${sourceKey.key}`,
66
+ rule: RULE.DUPLICATE_SOURCE_KEY,
67
+ outcome:
68
+ sourceKey.kind === 'local'
69
+ ? `a local Registration for canonical path '${sourceKey.canonicalPath ?? sourceKey.key}' already exists (${reg.id.slice(0, 8)}…)`
70
+ : `a Registration with an identical Source Key already exists (${reg.id.slice(0, 8)}…)`,
71
+ }),
72
+ };
73
+ }
74
+ }
75
+ return { duplicate: false };
76
+ }
77
+
78
+ /** Compute the local Source Key; returns Blocking findings on invalid input. */
79
+ export function sourceKeyForLocalRoot(
80
+ rootPath: string,
81
+ scope: Scope,
82
+ ): { ok: true; sourceKey: SourceKey } | { ok: false; findings: ValidationFinding[] } {
83
+ const res = localSourceKey(rootPath);
84
+ if (!res.ok) {
85
+ const missing = res.errno === 'ENOENT';
86
+ return {
87
+ ok: false,
88
+ findings: [
89
+ blocking({
90
+ code: missing ? CODE.SOURCE_NOT_EXISTS : CODE.SOURCE_NOT_DIRECTORY,
91
+ phase: 'identity',
92
+ target: 'source',
93
+ scope,
94
+ pointer: rootPath,
95
+ rule: missing ? RULE.SOURCE_NOT_EXISTS : RULE.SOURCE_NOT_DIRECTORY,
96
+ outcome: res.error ?? 'unable to resolve local Marketplace Root',
97
+ }),
98
+ ],
99
+ };
100
+ }
101
+ return { ok: true, sourceKey: res.sourceKey! };
102
+ }