@forgeax/engine-import 0.0.0-dev.8d955ade1c79

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 (118) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +227 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/decode-image-owner.test-d.d.ts +2 -0
  5. package/dist/__tests__/decode-image-owner.test-d.d.ts.map +1 -0
  6. package/dist/__tests__/import-contract-migration.test.d.ts +2 -0
  7. package/dist/__tests__/import-contract-migration.test.d.ts.map +1 -0
  8. package/dist/__tests__/import-dependencies.test.d.ts +2 -0
  9. package/dist/__tests__/import-dependencies.test.d.ts.map +1 -0
  10. package/dist/__tests__/import-finalizer-digest-retry.test.d.ts +2 -0
  11. package/dist/__tests__/import-finalizer-digest-retry.test.d.ts.map +1 -0
  12. package/dist/__tests__/import-local-artifacts.test.d.ts +2 -0
  13. package/dist/__tests__/import-local-artifacts.test.d.ts.map +1 -0
  14. package/dist/__tests__/import.unit.test.d.ts +2 -0
  15. package/dist/__tests__/import.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/mesh-bin-contract.test.d.ts +2 -0
  17. package/dist/__tests__/mesh-bin-contract.test.d.ts.map +1 -0
  18. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  19. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  20. package/dist/__tests__/mesh-bin.test.d.ts +2 -0
  21. package/dist/__tests__/mesh-bin.test.d.ts.map +1 -0
  22. package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
  23. package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/scriptable-pack-host.unit.test.d.ts +2 -0
  25. package/dist/__tests__/scriptable-pack-host.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts +2 -0
  27. package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +2 -0
  29. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +1 -0
  30. package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts +2 -0
  31. package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts +2 -0
  33. package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts.map +1 -0
  34. package/dist/__tests__/scriptable-pack.integration.test.d.ts +2 -0
  35. package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/source-overrides-import-context.unit.test.d.ts +2 -0
  37. package/dist/__tests__/source-overrides-import-context.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/source-package-publication.integration.test.d.ts +2 -0
  39. package/dist/__tests__/source-package-publication.integration.test.d.ts.map +1 -0
  40. package/dist/browser.d.ts +5 -0
  41. package/dist/browser.d.ts.map +1 -0
  42. package/dist/browser.mjs +782 -0
  43. package/dist/browser.mjs.map +1 -0
  44. package/dist/build-production.d.ts +48 -0
  45. package/dist/build-production.d.ts.map +1 -0
  46. package/dist/catalog-importer-policy.d.ts +13 -0
  47. package/dist/catalog-importer-policy.d.ts.map +1 -0
  48. package/dist/catalog-inventory.d.ts +4 -0
  49. package/dist/catalog-inventory.d.ts.map +1 -0
  50. package/dist/import-product.d.ts +27 -0
  51. package/dist/import-product.d.ts.map +1 -0
  52. package/dist/import-runner.d.ts +136 -0
  53. package/dist/import-runner.d.ts.map +1 -0
  54. package/dist/importer-registry.d.ts +48 -0
  55. package/dist/importer-registry.d.ts.map +1 -0
  56. package/dist/index.d.ts +19 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.mjs +2849 -0
  59. package/dist/index.mjs.map +1 -0
  60. package/dist/mesh-bin.d.ts +19 -0
  61. package/dist/mesh-bin.d.ts.map +1 -0
  62. package/dist/mesh-bin.mjs +182 -0
  63. package/dist/mesh-bin.mjs.map +1 -0
  64. package/dist/pack-projection.d.ts +5 -0
  65. package/dist/pack-projection.d.ts.map +1 -0
  66. package/dist/scriptable-pack-host.d.ts +72 -0
  67. package/dist/scriptable-pack-host.d.ts.map +1 -0
  68. package/dist/scriptable-pack-output-producers.d.ts +7 -0
  69. package/dist/scriptable-pack-output-producers.d.ts.map +1 -0
  70. package/dist/scriptable-pack-staged-snapshot.d.ts +21 -0
  71. package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -0
  72. package/dist/scriptable-pack.d.ts +141 -0
  73. package/dist/scriptable-pack.d.ts.map +1 -0
  74. package/dist/scriptable-source-package.d.ts +14 -0
  75. package/dist/scriptable-source-package.d.ts.map +1 -0
  76. package/dist/source-package-errors.d.ts +52 -0
  77. package/dist/source-package-errors.d.ts.map +1 -0
  78. package/dist/source-package-publication.d.ts +63 -0
  79. package/dist/source-package-publication.d.ts.map +1 -0
  80. package/dist/source-package.d.ts +60 -0
  81. package/dist/source-package.d.ts.map +1 -0
  82. package/package.json +69 -0
  83. package/src/__tests__/decode-image-owner.test-d.ts +53 -0
  84. package/src/__tests__/import-contract-migration.test.ts +223 -0
  85. package/src/__tests__/import-dependencies.test.ts +65 -0
  86. package/src/__tests__/import-finalizer-digest-retry.test.ts +113 -0
  87. package/src/__tests__/import-local-artifacts.test.ts +102 -0
  88. package/src/__tests__/import.unit.test.ts +429 -0
  89. package/src/__tests__/mesh-bin-contract.test.ts +95 -0
  90. package/src/__tests__/mesh-bin-morph-v3.test.ts +44 -0
  91. package/src/__tests__/mesh-bin.test.ts +102 -0
  92. package/src/__tests__/owner-chain.integration.test.ts +37 -0
  93. package/src/__tests__/scriptable-pack-host.unit.test.ts +86 -0
  94. package/src/__tests__/scriptable-pack-output-producers.unit.test.ts +190 -0
  95. package/src/__tests__/scriptable-pack-product.contract.test.ts +47 -0
  96. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +488 -0
  97. package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +290 -0
  98. package/src/__tests__/scriptable-pack.integration.test.ts +435 -0
  99. package/src/__tests__/source-overrides-import-context.unit.test.ts +92 -0
  100. package/src/__tests__/source-package-publication.integration.test.ts +75 -0
  101. package/src/browser.ts +14 -0
  102. package/src/build-production.ts +493 -0
  103. package/src/catalog-importer-policy.ts +58 -0
  104. package/src/catalog-inventory.ts +24 -0
  105. package/src/import-product.ts +163 -0
  106. package/src/import-runner.ts +749 -0
  107. package/src/importer-registry.ts +103 -0
  108. package/src/index.ts +175 -0
  109. package/src/mesh-bin.ts +229 -0
  110. package/src/pack-projection.ts +21 -0
  111. package/src/scriptable-pack-host.ts +544 -0
  112. package/src/scriptable-pack-output-producers.ts +456 -0
  113. package/src/scriptable-pack-staged-snapshot.ts +172 -0
  114. package/src/scriptable-pack.ts +626 -0
  115. package/src/scriptable-source-package.ts +88 -0
  116. package/src/source-package-errors.ts +201 -0
  117. package/src/source-package-publication.ts +326 -0
  118. package/src/source-package.ts +187 -0
@@ -0,0 +1,88 @@
1
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
2
+ import { projectScriptablePackMeta, type ScriptablePackError } from '@forgeax/engine-pack/source';
3
+ import type {
4
+ AssetError,
5
+ ImportError,
6
+ ImportedAsset,
7
+ ImportProduct,
8
+ Result,
9
+ } from '@forgeax/engine-types';
10
+ import { err, ok } from '@forgeax/engine-types';
11
+ import {
12
+ type BuildScriptablePackOptions,
13
+ buildScriptablePack,
14
+ type ScriptablePackDomainError,
15
+ type ScriptablePackExternalEvidence,
16
+ type ScriptablePackStagedOutput,
17
+ } from './scriptable-pack.js';
18
+
19
+ const META_ARTIFACT_KEY = 'scriptable-pack.meta.json';
20
+
21
+ export interface ScriptableSourcePackageProduct {
22
+ readonly anchorGuid: string;
23
+ readonly declaredGuids: readonly string[];
24
+ readonly product: ImportProduct<unknown>;
25
+ readonly inputFingerprint: string;
26
+ readonly externalEvidence: readonly ScriptablePackExternalEvidence[];
27
+ readonly stagedOutputs: readonly ScriptablePackStagedOutput[];
28
+ }
29
+
30
+ export type ScriptableSourcePackageResult = Result<
31
+ ScriptableSourcePackageProduct,
32
+ ScriptablePackError | AssetError | ImportError | ScriptablePackDomainError
33
+ >;
34
+
35
+ function withPrebuiltMeta<P>(
36
+ product: ImportProduct<P>,
37
+ anchorGuid: string,
38
+ meta: unknown,
39
+ ): ImportProduct<P> {
40
+ const bytes = new TextEncoder().encode(`${JSON.stringify(meta)}\n`);
41
+ return {
42
+ ...product,
43
+ assets: product.assets.map(
44
+ (asset): ImportedAsset<P> =>
45
+ asset.guid.toLowerCase() === anchorGuid
46
+ ? {
47
+ ...asset,
48
+ artifacts: {
49
+ ...asset.artifacts,
50
+ [META_ARTIFACT_KEY]: { mediaType: 'application/json', bytes },
51
+ },
52
+ }
53
+ : asset,
54
+ ),
55
+ };
56
+ }
57
+
58
+ export async function produceScriptableSourcePackage(
59
+ options: BuildScriptablePackOptions,
60
+ ): Promise<ScriptableSourcePackageResult> {
61
+ const built = await buildScriptablePack(options);
62
+ if (!built.ok) return err(built.error);
63
+ const declaredGuids = Object.values(options.definition.assets)
64
+ .map((descriptor) => AssetGuid.format(descriptor.guid).toLowerCase())
65
+ .sort();
66
+ const anchorGuid = declaredGuids[0];
67
+ if (anchorGuid === undefined) {
68
+ return err({
69
+ code: 'pack-source-output-invalid',
70
+ expected: 'at least one declared ScriptablePack output',
71
+ hint: 'add an output descriptor before building the package',
72
+ detail: { missingGuids: [], unexpectedSourceKeys: [], kindMismatches: [] },
73
+ });
74
+ }
75
+ const product = withPrebuiltMeta(
76
+ built.value.product,
77
+ anchorGuid,
78
+ projectScriptablePackMeta(options.definition, options.sourcePath),
79
+ );
80
+ return ok({
81
+ anchorGuid,
82
+ declaredGuids,
83
+ product,
84
+ stagedOutputs: built.value.stagedOutputs,
85
+ inputFingerprint: built.value.inputFingerprint,
86
+ externalEvidence: built.value.externalEvidence,
87
+ });
88
+ }
@@ -0,0 +1,201 @@
1
+ import type { ImportError } from '@forgeax/engine-types';
2
+
3
+ const SOURCE_PACKAGE_ERROR_POLICY = {
4
+ 'source-package-meta-invalid': {
5
+ expected: 'a valid source Meta declaration with complete GUID topology',
6
+ hint: 'repair the Meta declaration, then rebuild or cold-cook the source package',
7
+ },
8
+ 'source-package-importer-missing': {
9
+ expected: 'a registered importer for the source Meta importer key',
10
+ hint: 'register the named importer, then rebuild or cold-cook the source package',
11
+ },
12
+ 'source-package-conversion-failed': {
13
+ expected: 'the configured importer to convert the source successfully',
14
+ hint: 'repair the source or importer, then rebuild or cold-cook the source package',
15
+ },
16
+ 'source-package-ddc-failed': {
17
+ expected: 'a readable persistent DDC entry with matching integrity evidence',
18
+ hint: 'discard the invalid derived entry, then rebuild or cold-cook the source package',
19
+ },
20
+ 'source-package-publication-invalid': {
21
+ expected: 'a complete Pack body, refs, artifacts, and route integrity',
22
+ hint: 'repair the missing product bytes, then rebuild or cold-cook the source package',
23
+ },
24
+ 'source-package-guid-closure-mismatch': {
25
+ expected: 'exactly one produced asset for every declared GUID',
26
+ hint: 'repair the Meta topology or importer output, then rebuild the whole source package',
27
+ },
28
+ } as const;
29
+
30
+ export type SourcePackageErrorCode = keyof typeof SOURCE_PACKAGE_ERROR_POLICY;
31
+
32
+ export type SourcePackageErrorStage =
33
+ | 'meta'
34
+ | 'importer'
35
+ | 'conversion'
36
+ | 'closure'
37
+ | 'ddc'
38
+ | 'route-integrity';
39
+
40
+ export interface SourcePackageErrorContext {
41
+ readonly sourceMeta: string;
42
+ readonly anchorGuid: string;
43
+ readonly affectedGuids: readonly string[];
44
+ readonly producer: string;
45
+ readonly importer: string;
46
+ }
47
+
48
+ export interface SourcePackageErrorDetail extends SourcePackageErrorContext {
49
+ readonly stage: SourcePackageErrorStage;
50
+ readonly reason?: string;
51
+ readonly missing?: readonly string[];
52
+ readonly unexpected?: readonly string[];
53
+ readonly registeredImporters?: readonly string[];
54
+ }
55
+
56
+ export interface SourcePackageError {
57
+ readonly code: SourcePackageErrorCode;
58
+ readonly expected: string;
59
+ readonly hint: string;
60
+ readonly detail: SourcePackageErrorDetail;
61
+ }
62
+
63
+ export function sourcePackageError(
64
+ code: SourcePackageErrorCode,
65
+ context: SourcePackageErrorContext,
66
+ detail: Omit<SourcePackageErrorDetail, keyof SourcePackageErrorContext>,
67
+ ): SourcePackageError {
68
+ const policy = SOURCE_PACKAGE_ERROR_POLICY[code];
69
+ return {
70
+ code,
71
+ expected: policy?.expected as string,
72
+ hint: policy?.hint as string,
73
+ detail: { ...context, ...detail },
74
+ };
75
+ }
76
+
77
+ function unknownReason(error: unknown): string {
78
+ if (error !== null && typeof error === 'object') {
79
+ const value = error as Record<string, unknown>;
80
+ if (typeof value.code === 'string') {
81
+ const detail = value.detail;
82
+ const reason =
83
+ detail !== null && typeof detail === 'object' && 'reason' in detail
84
+ ? (detail as { readonly reason?: unknown }).reason
85
+ : undefined;
86
+ const diagnostic =
87
+ detail !== null && typeof detail === 'object' && 'diagnostic' in detail
88
+ ? (detail as { readonly diagnostic?: unknown }).diagnostic
89
+ : undefined;
90
+ return `${value.code}: ${
91
+ typeof diagnostic === 'string'
92
+ ? diagnostic
93
+ : typeof reason === 'string'
94
+ ? reason
95
+ : String(value.expected ?? 'producer failure')
96
+ }`;
97
+ }
98
+ }
99
+ return error instanceof Error ? error.message : String(error);
100
+ }
101
+
102
+ function importFailureCode(error: ImportError): SourcePackageErrorCode {
103
+ switch (error.code) {
104
+ case 'importer-not-registered':
105
+ return 'source-package-importer-missing';
106
+ case 'import-produced-no-assets':
107
+ case 'guid-mismatch':
108
+ case 'source-validation-failed':
109
+ return 'source-package-guid-closure-mismatch';
110
+ case 'source-read-failed':
111
+ case 'import-internal-error':
112
+ case 'mesh-material-slot-topology-change':
113
+ case 'unknown-source-key':
114
+ case 'duplicate-source-key':
115
+ case 'invalid-source-overrides':
116
+ case 'invalid-source-override-payload':
117
+ return 'source-package-conversion-failed';
118
+ }
119
+ }
120
+
121
+ export function normalizeSourcePackageError(
122
+ error: unknown,
123
+ context: SourcePackageErrorContext,
124
+ ): SourcePackageError {
125
+ if (isSourcePackageError(error)) return error;
126
+ if (isScriptablePackFailure(error)) {
127
+ return sourcePackageError('source-package-conversion-failed', context, {
128
+ stage: 'conversion',
129
+ reason: unknownReason(error),
130
+ });
131
+ }
132
+ if (isImportError(error)) {
133
+ const code = importFailureCode(error);
134
+ const detail: Omit<SourcePackageErrorDetail, keyof SourcePackageErrorContext> = {
135
+ stage: code === 'source-package-importer-missing' ? 'importer' : 'conversion',
136
+ reason: unknownReason(error),
137
+ ...(code === 'source-package-importer-missing' && 'registeredImporters' in error.detail
138
+ ? { registeredImporters: error.detail.registeredImporters }
139
+ : {}),
140
+ };
141
+ return sourcePackageError(code, context, detail);
142
+ }
143
+ return sourcePackageError(contextErrorStage(error), context, {
144
+ stage: 'conversion',
145
+ reason: unknownReason(error),
146
+ });
147
+ }
148
+
149
+ function isSourcePackageError(error: unknown): error is SourcePackageError {
150
+ return (
151
+ error !== null &&
152
+ typeof error === 'object' &&
153
+ 'code' in error &&
154
+ typeof error.code === 'string' &&
155
+ error.code in SOURCE_PACKAGE_ERROR_POLICY &&
156
+ 'expected' in error &&
157
+ typeof error.expected === 'string' &&
158
+ 'hint' in error &&
159
+ typeof error.hint === 'string' &&
160
+ 'detail' in error &&
161
+ error.detail !== null &&
162
+ typeof error.detail === 'object'
163
+ );
164
+ }
165
+
166
+ function contextErrorStage(error: unknown): SourcePackageErrorCode {
167
+ return error instanceof SyntaxError
168
+ ? 'source-package-meta-invalid'
169
+ : 'source-package-conversion-failed';
170
+ }
171
+
172
+ function isImportError(error: unknown): error is ImportError {
173
+ return (
174
+ error !== null &&
175
+ typeof error === 'object' &&
176
+ 'code' in error &&
177
+ 'expected' in error &&
178
+ 'hint' in error &&
179
+ 'detail' in error
180
+ );
181
+ }
182
+
183
+ function isScriptablePackFailure(error: unknown): error is {
184
+ readonly code: string;
185
+ readonly expected: string;
186
+ readonly hint: string;
187
+ readonly detail: unknown;
188
+ } {
189
+ return (
190
+ error !== null &&
191
+ typeof error === 'object' &&
192
+ 'code' in error &&
193
+ typeof error.code === 'string' &&
194
+ error.code.startsWith('pack-source-') &&
195
+ 'expected' in error &&
196
+ typeof error.expected === 'string' &&
197
+ 'hint' in error &&
198
+ typeof error.hint === 'string' &&
199
+ 'detail' in error
200
+ );
201
+ }
@@ -0,0 +1,326 @@
1
+ import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import {
4
+ type DdcEntry,
5
+ type DdcGenerationEntryCandidate,
6
+ DdcGenerationSession,
7
+ type DdcHead,
8
+ DdcLifecycle,
9
+ ddcOutputDigest,
10
+ } from '@forgeax/engine-ddc';
11
+ import {
12
+ type CatalogEntry,
13
+ err,
14
+ ok,
15
+ type ResourceRevision,
16
+ type Result,
17
+ } from '@forgeax/engine-types';
18
+ import {
19
+ type SourcePackageError,
20
+ type SourcePackageErrorContext,
21
+ sourcePackageError,
22
+ } from './source-package-errors.js';
23
+
24
+ interface SourcePackageDdcEntryInput {
25
+ readonly root: string;
26
+ readonly entry: DdcEntry;
27
+ readonly context: SourcePackageErrorContext;
28
+ }
29
+
30
+ interface StagedSourcePackageDdc extends DdcGenerationEntryCandidate {
31
+ readonly root: string;
32
+ readonly session: DdcGenerationSession;
33
+ readonly context: SourcePackageErrorContext;
34
+ }
35
+
36
+ export interface ImportPublicationInput {
37
+ readonly root: string;
38
+ readonly guid: string;
39
+ readonly desiredKey: string;
40
+ readonly pack: unknown;
41
+ readonly previousCatalog: readonly CatalogEntry[];
42
+ readonly nextCatalog: readonly CatalogEntry[];
43
+ readonly publishedGuids: readonly string[];
44
+ readonly transport?: {
45
+ readonly path: string;
46
+ readonly body: string;
47
+ };
48
+ }
49
+
50
+ export interface ImportPublicationError {
51
+ readonly code: SourcePackageError['code'];
52
+ readonly expected: string;
53
+ readonly hint: string;
54
+ readonly detail: string;
55
+ readonly diagnostic: SourcePackageError;
56
+ }
57
+
58
+ export type ImportPublicationResult =
59
+ | {
60
+ readonly ok: true;
61
+ readonly key: string;
62
+ readonly head: DdcHead;
63
+ readonly catalog: readonly CatalogEntry[];
64
+ readonly revision: ResourceRevision;
65
+ readonly transportPersisted: boolean;
66
+ }
67
+ | { readonly ok: false; readonly error: ImportPublicationError; readonly head: DdcHead };
68
+
69
+ export interface StagedImportPublication {
70
+ readonly input: ImportPublicationInput;
71
+ readonly ddc: StagedSourcePackageDdc;
72
+ readonly key: string;
73
+ readonly head: DdcHead;
74
+ readonly catalog: readonly CatalogEntry[];
75
+ readonly revision: ResourceRevision;
76
+ }
77
+
78
+ export type StagedImportPublicationResult =
79
+ | { readonly ok: true; readonly candidate: StagedImportPublication }
80
+ | { readonly ok: false; readonly error: ImportPublicationError; readonly head: DdcHead };
81
+
82
+ function inspectHead(root: string, guid: string, desiredKey: string): Promise<DdcHead> {
83
+ return new DdcLifecycle(root).inspect(guid, desiredKey);
84
+ }
85
+
86
+ const PUBLICATION_SETTLE_TIMEOUT_MS = 5_000;
87
+ const PUBLICATION_SETTLE_POLL_MS = 10;
88
+
89
+ async function inspectAfterSupersededCommit(candidate: StagedSourcePackageDdc): Promise<DdcHead> {
90
+ let head = await inspectHead(candidate.root, candidate.lease.guid, candidate.lease.desiredKey);
91
+ if (head.state === 'current' && head.currentKey === candidate.lease.desiredKey) return head;
92
+
93
+ // A newer owner may already have superseded this lease while it is still
94
+ // cooking. The strict DDC CAS correctly returns `stale` to this candidate;
95
+ // wait for that owner to publish before turning an equivalent publication
96
+ // into a user-visible import failure. The wait is bounded so a crashed
97
+ // owner never leaves the caller hanging and a different desired key still
98
+ // fails closed.
99
+ const deadline = Date.now() + PUBLICATION_SETTLE_TIMEOUT_MS;
100
+ while (
101
+ Date.now() < deadline &&
102
+ head.activeLease !== undefined &&
103
+ head.activeLease.attempt !== candidate.lease.attempt &&
104
+ head.activeLease.desiredKey === candidate.lease.desiredKey
105
+ ) {
106
+ await new Promise((resolve) => setTimeout(resolve, PUBLICATION_SETTLE_POLL_MS));
107
+ head = await inspectHead(candidate.root, candidate.lease.guid, candidate.lease.desiredKey);
108
+ if (head.state === 'current' && head.currentKey === candidate.lease.desiredKey) return head;
109
+ }
110
+ return head;
111
+ }
112
+
113
+ async function stageSourcePackageDdc(
114
+ input: SourcePackageDdcEntryInput,
115
+ ): Promise<Result<StagedSourcePackageDdc, SourcePackageError>> {
116
+ const session = new DdcGenerationSession(input.root, { generation: 1 });
117
+ try {
118
+ const candidate = await session.stageEntry(input.entry);
119
+ return ok({ ...candidate, root: input.root, session, context: input.context });
120
+ } catch (error) {
121
+ return err(
122
+ sourcePackageError('source-package-ddc-failed', input.context, {
123
+ stage: 'ddc',
124
+ reason: error instanceof Error ? error.message : String(error),
125
+ }),
126
+ );
127
+ }
128
+ }
129
+
130
+ async function commitSourcePackageDdc(
131
+ candidate: StagedSourcePackageDdc,
132
+ ): Promise<Result<DdcHead, SourcePackageError>> {
133
+ try {
134
+ const commit = await candidate.session.commitEntry(candidate, candidate.lease.desiredKey);
135
+ if (commit.result !== 'current') {
136
+ const head = await inspectAfterSupersededCommit(candidate);
137
+ if (head.state === 'current' && head.currentKey === candidate.lease.desiredKey) {
138
+ return ok(head);
139
+ }
140
+ return err(
141
+ sourcePackageError('source-package-ddc-failed', candidate.context, {
142
+ stage: 'ddc',
143
+ reason: `DDC lifecycle commit returned ${commit.result}`,
144
+ }),
145
+ );
146
+ }
147
+ return ok(await inspectHead(candidate.root, candidate.lease.guid, candidate.lease.desiredKey));
148
+ } catch (error) {
149
+ return err(
150
+ sourcePackageError('source-package-ddc-failed', candidate.context, {
151
+ stage: 'ddc',
152
+ reason: error instanceof Error ? error.message : String(error),
153
+ }),
154
+ );
155
+ }
156
+ }
157
+
158
+ async function persistTransport(transport: ImportPublicationInput['transport']): Promise<boolean> {
159
+ if (transport === undefined) return true;
160
+ const directory = dirname(transport.path);
161
+ const temporary = `${transport.path}.tmp`;
162
+ try {
163
+ await mkdir(directory, { recursive: true });
164
+ if (((await stat(directory)).mode & 0o222) === 0) {
165
+ throw new Error('transport directory is read-only');
166
+ }
167
+ await writeFile(temporary, transport.body);
168
+ await rename(temporary, transport.path);
169
+ return true;
170
+ } catch {
171
+ await rm(temporary, { force: true }).catch(() => {});
172
+ return false;
173
+ }
174
+ }
175
+
176
+ function importPublicationFailure(error: SourcePackageError): ImportPublicationError {
177
+ return {
178
+ code: error.code,
179
+ expected: error.expected,
180
+ hint: error.hint,
181
+ detail: error.detail.reason ?? error.detail.stage,
182
+ diagnostic: error,
183
+ };
184
+ }
185
+
186
+ function projectImportPublication(
187
+ input: ImportPublicationInput,
188
+ head: DdcHead,
189
+ observedAt: number,
190
+ ): { readonly catalog: readonly CatalogEntry[]; readonly revision: ResourceRevision } {
191
+ const digest = head.currentKey ?? input.desiredKey;
192
+ const revision: ResourceRevision = { digest, observedAt, rootId: input.root };
193
+ const published = new Set(input.publishedGuids.map((guid) => guid.toLowerCase()));
194
+ const catalog = input.nextCatalog.map((row) => {
195
+ if (!published.has(row.guid.toLowerCase())) return row;
196
+ const previousLkg =
197
+ head.lastKnownGoodKey === undefined
198
+ ? undefined
199
+ : input.previousCatalog.find(
200
+ (candidate) =>
201
+ candidate.guid.toLowerCase() === row.guid.toLowerCase() &&
202
+ candidate.revision?.digest === head.lastKnownGoodKey,
203
+ );
204
+ const projection =
205
+ row.projection === undefined
206
+ ? undefined
207
+ : (() => {
208
+ const { lastKnownGood: _staleLastKnownGood, ...withoutLastKnownGood } = row.projection;
209
+ return previousLkg?.packageUrl === undefined
210
+ ? withoutLastKnownGood
211
+ : { ...withoutLastKnownGood, lastKnownGood: { packageUrl: previousLkg.packageUrl } };
212
+ })();
213
+ return {
214
+ ...row,
215
+ revision,
216
+ ...(projection === undefined ? {} : { projection }),
217
+ };
218
+ });
219
+ return { catalog, revision };
220
+ }
221
+
222
+ export async function publishImportPublication(
223
+ input: ImportPublicationInput,
224
+ ): Promise<ImportPublicationResult> {
225
+ const staged = await stageImportPublication(input);
226
+ if (!staged.ok) return staged;
227
+ return commitImportPublication(staged.candidate);
228
+ }
229
+
230
+ export async function stageImportPublication(
231
+ input: ImportPublicationInput,
232
+ ): Promise<StagedImportPublicationResult> {
233
+ const staged = await stageSourcePackageDdc({
234
+ root: input.root,
235
+ entry: {
236
+ key: input.desiredKey,
237
+ guid: input.guid,
238
+ payload: input.pack,
239
+ refs: [],
240
+ artifacts: {},
241
+ receipt: {
242
+ guid: input.guid,
243
+ key: input.desiredKey,
244
+ producer: 'engine-import/source-package-publication',
245
+ inputFingerprint: input.desiredKey,
246
+ outputDigest: ddcOutputDigest({
247
+ guid: input.guid,
248
+ payload: input.pack,
249
+ refs: [],
250
+ artifacts: {},
251
+ }),
252
+ },
253
+ },
254
+ context: {
255
+ sourceMeta: '<import-publication>',
256
+ anchorGuid: input.guid,
257
+ affectedGuids: input.publishedGuids,
258
+ producer: 'source-package/import-publication',
259
+ importer: 'import-publication',
260
+ },
261
+ });
262
+ if (!staged.ok) {
263
+ return {
264
+ ok: false,
265
+ error: importPublicationFailure(staged.error),
266
+ head: await inspectHead(input.root, input.guid, input.desiredKey),
267
+ };
268
+ }
269
+ const lastKnownGoodKey =
270
+ staged.value.previousHead.currentKey !== undefined &&
271
+ staged.value.previousHead.currentKey !== input.desiredKey
272
+ ? staged.value.previousHead.currentKey
273
+ : staged.value.previousHead.lastKnownGoodKey;
274
+ const projectedHead: DdcHead = {
275
+ ...staged.value.previousHead,
276
+ state: 'cooking',
277
+ currentKey: input.desiredKey,
278
+ ...(lastKnownGoodKey === undefined ? {} : { lastKnownGoodKey }),
279
+ };
280
+ const projected = projectImportPublication(input, projectedHead, Date.now());
281
+ return {
282
+ ok: true,
283
+ candidate: {
284
+ input,
285
+ ddc: staged.value,
286
+ key: input.desiredKey,
287
+ head: projectedHead,
288
+ catalog: projected.catalog,
289
+ revision: projected.revision,
290
+ },
291
+ };
292
+ }
293
+
294
+ export async function commitImportPublication(
295
+ candidate: StagedImportPublication,
296
+ ): Promise<ImportPublicationResult> {
297
+ const publication = await commitSourcePackageDdc(candidate.ddc);
298
+ if (!publication.ok) {
299
+ return {
300
+ ok: false,
301
+ error: importPublicationFailure(publication.error),
302
+ head: await inspectHead(
303
+ candidate.input.root,
304
+ candidate.input.guid,
305
+ candidate.input.desiredKey,
306
+ ),
307
+ };
308
+ }
309
+ const projected = projectImportPublication(candidate.input, publication.value, Date.now());
310
+ return {
311
+ ok: true,
312
+ key: candidate.input.desiredKey,
313
+ head: publication.value,
314
+ catalog: projected.catalog,
315
+ revision: projected.revision,
316
+ transportPersisted: await persistTransport(candidate.input.transport),
317
+ };
318
+ }
319
+
320
+ export async function discardImportPublication(candidate: StagedImportPublication): Promise<void> {
321
+ await candidate.ddc.session.discardEntry(candidate.ddc);
322
+ }
323
+
324
+ export async function restoreImportPublication(candidate: StagedImportPublication): Promise<void> {
325
+ await candidate.ddc.session.restoreEntry(candidate.ddc);
326
+ }