@forgeax/engine-pack 0.1.3 → 0.1.4
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.
- package/README.md +51 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
- package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
- package/dist/__tests__/package-finalizer.contract.test.d.ts +2 -0
- package/dist/__tests__/package-finalizer.contract.test.d.ts.map +1 -0
- package/dist/__tests__/runtime.browser.test.d.ts +2 -0
- package/dist/__tests__/runtime.browser.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-inventory.contract.test.d.ts +2 -0
- package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +1 -0
- package/dist/artifact-path.mjs +53 -0
- package/dist/artifact-path.mjs.map +1 -0
- package/dist/atlas/shelf-pack.d.ts.map +1 -1
- package/dist/build.d.ts +17 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.mjs +10168 -0
- package/dist/build.mjs.map +1 -0
- package/dist/builtin.d.ts +20 -0
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.mjs +141 -0
- package/dist/builtin.mjs.map +1 -0
- package/dist/catalog-builder.d.ts +43 -0
- package/dist/catalog-builder.d.ts.map +1 -0
- package/dist/catalog-delta.d.ts +11 -0
- package/dist/catalog-delta.d.ts.map +1 -0
- package/dist/catalog-projection.d.ts +91 -0
- package/dist/catalog-projection.d.ts.map +1 -0
- package/dist/cli-asset.mjs +231 -484
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/index.mjs +83 -1
- package/dist/index.mjs.map +1 -1
- package/dist/material-cook.d.ts +2 -0
- package/dist/material-cook.d.ts.map +1 -0
- package/dist/material-cook.mjs +193 -0
- package/dist/material-cook.mjs.map +1 -0
- package/dist/mesh-bin-contract.mjs +96 -0
- package/dist/mesh-bin-contract.mjs.map +1 -0
- package/dist/native-cooker-registry.d.ts +19 -0
- package/dist/native-cooker-registry.d.ts.map +1 -1
- package/dist/native-cooker.mjs +37 -0
- package/dist/native-cooker.mjs.map +1 -1
- package/dist/package-finalizer.d.ts +121 -0
- package/dist/package-finalizer.d.ts.map +1 -0
- package/dist/resolve-asset-source.d.ts.map +1 -1
- package/dist/resolve-asset-source.mjs +16 -6
- package/dist/resolve-asset-source.mjs.map +1 -1
- package/dist/runtime-publication.d.ts +44 -0
- package/dist/runtime-publication.d.ts.map +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.mjs +7261 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/scanner.d.ts +87 -54
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.mjs +258 -454
- package/dist/scanner.mjs.map +1 -1
- package/dist/scriptable-pack-node.d.ts +3 -8
- package/dist/scriptable-pack-node.d.ts.map +1 -1
- package/dist/scriptable-pack-node.mjs +93 -9
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack-worker.mjs +1 -0
- package/dist/scriptable-pack-worker.mjs.map +1 -1
- package/dist/scriptable-pack.d.ts +33 -0
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/scriptable-pack.mjs +91 -2
- package/dist/scriptable-pack.mjs.map +1 -1
- package/dist/topology.d.ts +1 -1
- package/dist/topology.d.ts.map +1 -1
- package/package.json +27 -2
- package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +2 -1
- package/src/__tests__/owner-chain.integration.test.ts +78 -0
- package/src/__tests__/package-finalizer.contract.test.ts +105 -0
- package/src/__tests__/runtime.browser.test.ts +15 -0
- package/src/__tests__/scanner-inventory.contract.test.ts +167 -0
- package/src/__tests__/scanner-inventory.test.ts +16 -12
- package/src/__tests__/scriptable-pack.unit.test.ts +42 -0
- package/src/atlas/shelf-pack.ts +20 -24
- package/src/build.ts +137 -0
- package/src/builtin.ts +38 -0
- package/src/catalog-builder.ts +313 -0
- package/src/catalog-delta.ts +189 -0
- package/src/catalog-projection.ts +344 -0
- package/src/cli-asset.ts +1 -5
- package/src/index.ts +2 -2
- package/src/material-cook.ts +13 -0
- package/src/native-cooker-registry.ts +72 -0
- package/src/package-finalizer.ts +394 -0
- package/src/resolve-asset-source.ts +2 -7
- package/src/runtime-publication.ts +190 -0
- package/src/runtime.ts +23 -0
- package/src/scanner.ts +289 -290
- package/src/scriptable-pack-node.ts +27 -23
- package/src/scriptable-pack-worker.ts +2 -0
- package/src/scriptable-pack.ts +123 -1
- package/src/topology.ts +2 -1
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssetAuthoringCapability,
|
|
3
|
+
AssetPublicationEnvelope,
|
|
4
|
+
AssetRelation,
|
|
5
|
+
CatalogDiagnostic,
|
|
6
|
+
CatalogEntryV2,
|
|
7
|
+
CatalogLifecycle,
|
|
8
|
+
CatalogSubject,
|
|
9
|
+
CookExecution,
|
|
10
|
+
PackIndexEntry,
|
|
11
|
+
ProducerContractDiagnostic,
|
|
12
|
+
ProducerContractResult,
|
|
13
|
+
ProviderProvenance,
|
|
14
|
+
ResourceRevision,
|
|
15
|
+
SourceOverrideDescriptor,
|
|
16
|
+
} from '@forgeax/engine-types';
|
|
17
|
+
import { authoringCapabilityForAssetKind, catalogOperationsFor } from '@forgeax/engine-types';
|
|
18
|
+
import { isScriptablePackAssetKind } from './scriptable-pack.js';
|
|
19
|
+
|
|
20
|
+
export function catalogProjectionFor(
|
|
21
|
+
subject: 'internal-asset' | 'imported-output',
|
|
22
|
+
execution: 'direct' | 'cooked',
|
|
23
|
+
lifecycle: 'missing' | 'cooking' | 'current' | 'stale' | 'failed',
|
|
24
|
+
) {
|
|
25
|
+
return {
|
|
26
|
+
subject,
|
|
27
|
+
execution,
|
|
28
|
+
lifecycle,
|
|
29
|
+
operations: catalogOperationsFor({ subject, execution, lifecycle }),
|
|
30
|
+
} as const;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function currentProjectionFor(
|
|
34
|
+
subject: 'internal-asset' | 'imported-output',
|
|
35
|
+
execution: 'direct' | 'cooked',
|
|
36
|
+
) {
|
|
37
|
+
return catalogRowProjectionFor(subject, execution, 'current');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function catalogRowProjectionFor(
|
|
41
|
+
subject: 'internal-asset' | 'imported-output',
|
|
42
|
+
execution: 'direct' | 'cooked',
|
|
43
|
+
lifecycle: 'missing' | 'cooking' | 'current' | 'stale' | 'failed',
|
|
44
|
+
) {
|
|
45
|
+
const projection = catalogProjectionFor(subject, execution, lifecycle);
|
|
46
|
+
return { ...projection, projection } as const;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CatalogProducerMeta {
|
|
50
|
+
readonly schemaVersion: string | number;
|
|
51
|
+
readonly packageId?: string;
|
|
52
|
+
readonly provenance?: ProviderProvenance;
|
|
53
|
+
readonly revision?: ResourceRevision;
|
|
54
|
+
readonly diagnostics?: readonly CatalogDiagnostic[];
|
|
55
|
+
readonly provider?: string;
|
|
56
|
+
readonly importer?: string;
|
|
57
|
+
readonly sourceOverrides?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
58
|
+
readonly sourceOverrideDescriptors?: readonly SourceOverrideDescriptor[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CatalogOutputDeclaration {
|
|
62
|
+
readonly guid: string;
|
|
63
|
+
readonly kind: string;
|
|
64
|
+
readonly sourceKey?: string;
|
|
65
|
+
readonly sourceIndex?: number;
|
|
66
|
+
readonly relations?: PackIndexEntry['relations'];
|
|
67
|
+
readonly authoring?: AssetAuthoringCapability;
|
|
68
|
+
readonly refs?: readonly string[];
|
|
69
|
+
readonly name?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function referenceRelations(
|
|
73
|
+
guid: string,
|
|
74
|
+
refs: readonly string[] | undefined,
|
|
75
|
+
provenance: ProviderProvenance,
|
|
76
|
+
): readonly AssetRelation[] | undefined {
|
|
77
|
+
if (refs === undefined || refs.length === 0) return undefined;
|
|
78
|
+
return refs.map((ref) => ({
|
|
79
|
+
from: { type: 'asset', id: guid } as const,
|
|
80
|
+
to: { type: 'asset', id: ref } as const,
|
|
81
|
+
type: 'references' as const,
|
|
82
|
+
policy: { strength: 'required' as const },
|
|
83
|
+
provenance,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function producerFields(
|
|
88
|
+
producer: CatalogProducerMeta,
|
|
89
|
+
output: CatalogOutputDeclaration,
|
|
90
|
+
): Pick<
|
|
91
|
+
PackIndexEntry,
|
|
92
|
+
| 'packageId'
|
|
93
|
+
| 'provenance'
|
|
94
|
+
| 'revision'
|
|
95
|
+
| 'sourceKey'
|
|
96
|
+
| 'sourceIndex'
|
|
97
|
+
| 'sourceOverrides'
|
|
98
|
+
| 'sourceOverrideDescriptors'
|
|
99
|
+
| 'relations'
|
|
100
|
+
| 'diagnostics'
|
|
101
|
+
| 'subject'
|
|
102
|
+
| 'execution'
|
|
103
|
+
| 'lifecycle'
|
|
104
|
+
| 'projection'
|
|
105
|
+
| 'authoring'
|
|
106
|
+
> {
|
|
107
|
+
const provenance =
|
|
108
|
+
producer.provenance ??
|
|
109
|
+
({
|
|
110
|
+
provider: producer.provider ?? producer.importer ?? 'engine',
|
|
111
|
+
version: String(producer.schemaVersion),
|
|
112
|
+
} satisfies ProviderProvenance);
|
|
113
|
+
const relations = output.relations ?? referenceRelations(output.guid, output.refs, provenance);
|
|
114
|
+
const inferred = authoringCapabilityForAssetKind(output.kind);
|
|
115
|
+
return {
|
|
116
|
+
provenance,
|
|
117
|
+
...(producer.packageId === undefined ? {} : { packageId: producer.packageId }),
|
|
118
|
+
...(producer.revision === undefined ? {} : { revision: producer.revision }),
|
|
119
|
+
...(output.sourceKey === undefined ? {} : { sourceKey: output.sourceKey }),
|
|
120
|
+
...(output.sourceIndex === undefined ? {} : { sourceIndex: output.sourceIndex }),
|
|
121
|
+
...(producer.sourceOverrides === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { sourceOverrides: producer.sourceOverrides }),
|
|
124
|
+
...(producer.sourceOverrideDescriptors === undefined || output.sourceKey === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: {
|
|
127
|
+
sourceOverrideDescriptors: producer.sourceOverrideDescriptors.filter(
|
|
128
|
+
(descriptor) => descriptor.sourceKey === output.sourceKey,
|
|
129
|
+
),
|
|
130
|
+
}),
|
|
131
|
+
...(relations === undefined ? {} : { relations }),
|
|
132
|
+
...(producer.diagnostics === undefined ? {} : { diagnostics: producer.diagnostics }),
|
|
133
|
+
...(output.authoring === undefined && inferred.ui === undefined
|
|
134
|
+
? {}
|
|
135
|
+
: { authoring: output.authoring ?? inferred }),
|
|
136
|
+
...catalogRowProjectionFor('imported-output', 'cooked', 'missing'),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Project producer declarations without interpreting a provider's kind vocabulary. */
|
|
141
|
+
export function projectExternalCatalogEntries(
|
|
142
|
+
producer: CatalogProducerMeta,
|
|
143
|
+
sourcePath: string,
|
|
144
|
+
packageUrl: string,
|
|
145
|
+
outputs: readonly CatalogOutputDeclaration[],
|
|
146
|
+
nameFor?: (output: CatalogOutputDeclaration, index: number) => string | undefined,
|
|
147
|
+
): PackIndexEntry[] {
|
|
148
|
+
return outputs.map((output, index) => ({
|
|
149
|
+
guid: output.guid,
|
|
150
|
+
packageUrl,
|
|
151
|
+
kind: output.kind,
|
|
152
|
+
sourcePath,
|
|
153
|
+
...producerFields(producer, output),
|
|
154
|
+
...((nameFor?.(output, index) ?? output.name) === undefined
|
|
155
|
+
? {}
|
|
156
|
+
: { name: nameFor?.(output, index) ?? output.name }),
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Return the first declaration that would shadow an engine-owned Pack kind. */
|
|
161
|
+
export function findReservedAssetKindConflict(
|
|
162
|
+
outputs: readonly Pick<CatalogOutputDeclaration, 'kind'>[],
|
|
163
|
+
): Pick<CatalogOutputDeclaration, 'kind'> | undefined {
|
|
164
|
+
return outputs.find((output) => isScriptablePackAssetKind(output.kind));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface PackageCatalogInput {
|
|
168
|
+
readonly guid: string;
|
|
169
|
+
readonly kind: string;
|
|
170
|
+
readonly sourcePath: string;
|
|
171
|
+
readonly name?: string;
|
|
172
|
+
readonly refs?: readonly string[];
|
|
173
|
+
readonly authoring?: AssetAuthoringCapability;
|
|
174
|
+
readonly execution?: 'direct' | 'cooked';
|
|
175
|
+
readonly packageId?: string;
|
|
176
|
+
readonly provenance?: ProviderProvenance;
|
|
177
|
+
readonly revision?: ResourceRevision;
|
|
178
|
+
readonly sourceKey?: string;
|
|
179
|
+
readonly sourceIndex?: number;
|
|
180
|
+
readonly sourceOverrides?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
181
|
+
readonly sourceOverrideDescriptors?: readonly SourceOverrideDescriptor[];
|
|
182
|
+
readonly relations?: PackIndexEntry['relations'];
|
|
183
|
+
readonly diagnostics?: readonly CatalogDiagnostic[];
|
|
184
|
+
readonly cookReceiptUrl?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Project self-contained Pack rows into the same Catalog contract as producer output. */
|
|
188
|
+
export function projectPackageCatalog(
|
|
189
|
+
entries: readonly PackageCatalogInput[],
|
|
190
|
+
packageUrl: string,
|
|
191
|
+
): PackIndexEntry[] {
|
|
192
|
+
return entries.map((entry) => {
|
|
193
|
+
const execution = entry.execution ?? 'direct';
|
|
194
|
+
const lifecycle = execution === 'cooked' ? 'missing' : 'current';
|
|
195
|
+
const inferred = authoringCapabilityForAssetKind(entry.kind);
|
|
196
|
+
const provenance =
|
|
197
|
+
entry.provenance ?? ({ provider: 'pack', version: 'unknown' } satisfies ProviderProvenance);
|
|
198
|
+
const relations = entry.relations ?? referenceRelations(entry.guid, entry.refs, provenance);
|
|
199
|
+
return {
|
|
200
|
+
guid: entry.guid,
|
|
201
|
+
kind: entry.kind,
|
|
202
|
+
sourcePath: entry.sourcePath,
|
|
203
|
+
packageUrl,
|
|
204
|
+
...(entry.name === undefined ? {} : { name: entry.name }),
|
|
205
|
+
...(entry.refs === undefined ? {} : { refs: entry.refs }),
|
|
206
|
+
...(entry.packageId === undefined ? {} : { packageId: entry.packageId }),
|
|
207
|
+
...(entry.provenance === undefined ? {} : { provenance: entry.provenance }),
|
|
208
|
+
...(entry.revision === undefined ? {} : { revision: entry.revision }),
|
|
209
|
+
...(entry.sourceKey === undefined ? {} : { sourceKey: entry.sourceKey }),
|
|
210
|
+
...(entry.sourceIndex === undefined ? {} : { sourceIndex: entry.sourceIndex }),
|
|
211
|
+
...(entry.sourceOverrides === undefined ? {} : { sourceOverrides: entry.sourceOverrides }),
|
|
212
|
+
...(entry.sourceOverrideDescriptors === undefined
|
|
213
|
+
? {}
|
|
214
|
+
: { sourceOverrideDescriptors: entry.sourceOverrideDescriptors }),
|
|
215
|
+
...(relations === undefined ? {} : { relations }),
|
|
216
|
+
...(entry.diagnostics === undefined ? {} : { diagnostics: entry.diagnostics }),
|
|
217
|
+
...(entry.cookReceiptUrl === undefined ? {} : { cookReceiptUrl: entry.cookReceiptUrl }),
|
|
218
|
+
...(entry.authoring === undefined && inferred.ui === undefined
|
|
219
|
+
? {}
|
|
220
|
+
: { authoring: entry.authoring ?? inferred }),
|
|
221
|
+
...catalogRowProjectionFor('internal-asset', execution, lifecycle),
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface CookedPackageProjection {
|
|
227
|
+
readonly packageUrl: string;
|
|
228
|
+
readonly revision: ResourceRevision;
|
|
229
|
+
readonly refs: readonly string[];
|
|
230
|
+
readonly cookReceiptUrl?: string;
|
|
231
|
+
readonly publication?: AssetPublicationEnvelope;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Project one complete cooked package; navigation facts stay in the Pack owner. */
|
|
235
|
+
export function projectCookedPackageEntry(
|
|
236
|
+
entry: PackIndexEntry,
|
|
237
|
+
projection: CookedPackageProjection,
|
|
238
|
+
): PackIndexEntry {
|
|
239
|
+
return {
|
|
240
|
+
...entry,
|
|
241
|
+
...(projection.publication === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: { sourcePath: projection.publication.sourcePath }),
|
|
244
|
+
packageUrl: projection.packageUrl,
|
|
245
|
+
...(projection.cookReceiptUrl === undefined
|
|
246
|
+
? {}
|
|
247
|
+
: { cookReceiptUrl: projection.cookReceiptUrl }),
|
|
248
|
+
revision: projection.revision,
|
|
249
|
+
...(projection.publication === undefined ? {} : { publication: projection.publication }),
|
|
250
|
+
...currentProjectionFor('imported-output', 'cooked'),
|
|
251
|
+
refs: projection.refs,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface RuntimeCatalogRowInput {
|
|
256
|
+
readonly scopeId: string;
|
|
257
|
+
readonly generation: number;
|
|
258
|
+
readonly digest: string;
|
|
259
|
+
readonly outputSetDigest: string;
|
|
260
|
+
readonly guid: string;
|
|
261
|
+
readonly kind: string;
|
|
262
|
+
readonly packageUrl: string;
|
|
263
|
+
readonly sourcePath?: string;
|
|
264
|
+
readonly subject?: CatalogSubject;
|
|
265
|
+
readonly execution?: CookExecution;
|
|
266
|
+
readonly lifecycle?: CatalogLifecycle;
|
|
267
|
+
readonly publication?: unknown;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function runtimeCatalogRowError(
|
|
271
|
+
subjectId: string,
|
|
272
|
+
expected: string,
|
|
273
|
+
): ProducerContractResult<never> {
|
|
274
|
+
const diagnostic: ProducerContractDiagnostic = {
|
|
275
|
+
code: 'invalid-producer-fact',
|
|
276
|
+
subject: { type: 'asset', id: subjectId },
|
|
277
|
+
expected,
|
|
278
|
+
hint: 'recook the source and publish one current runtime Catalog row',
|
|
279
|
+
authority: 'producer',
|
|
280
|
+
};
|
|
281
|
+
return { ok: false, error: diagnostic };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function projectRuntimeCatalogRowValue(
|
|
285
|
+
input: RuntimeCatalogRowInput,
|
|
286
|
+
): ProducerContractResult<CatalogEntryV2> {
|
|
287
|
+
if (
|
|
288
|
+
input.publication !== undefined ||
|
|
289
|
+
input.guid.length === 0 ||
|
|
290
|
+
input.kind.length === 0 ||
|
|
291
|
+
input.packageUrl.length === 0 ||
|
|
292
|
+
input.scopeId.length === 0 ||
|
|
293
|
+
!Number.isInteger(input.generation) ||
|
|
294
|
+
input.generation < 1 ||
|
|
295
|
+
input.digest.length === 0 ||
|
|
296
|
+
input.outputSetDigest.length === 0
|
|
297
|
+
) {
|
|
298
|
+
return runtimeCatalogRowError(
|
|
299
|
+
input.guid,
|
|
300
|
+
'one flat publication tuple and no legacy publication object',
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const subject = input.subject ?? 'imported-output';
|
|
305
|
+
const execution = input.execution ?? 'cooked';
|
|
306
|
+
const lifecycle = input.lifecycle ?? 'current';
|
|
307
|
+
return {
|
|
308
|
+
ok: true,
|
|
309
|
+
value: {
|
|
310
|
+
scopeId: input.scopeId,
|
|
311
|
+
generation: input.generation,
|
|
312
|
+
digest: input.digest,
|
|
313
|
+
outputSetDigest: input.outputSetDigest,
|
|
314
|
+
guid: input.guid,
|
|
315
|
+
kind: input.kind,
|
|
316
|
+
packageUrl: input.packageUrl,
|
|
317
|
+
sourcePath: input.sourcePath ?? input.packageUrl,
|
|
318
|
+
subject,
|
|
319
|
+
execution,
|
|
320
|
+
lifecycle,
|
|
321
|
+
projection: {
|
|
322
|
+
subject,
|
|
323
|
+
execution,
|
|
324
|
+
lifecycle,
|
|
325
|
+
operations: catalogOperationsFor({ subject, execution, lifecycle }),
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Project one row; an array is accepted only to fail closed on double rows. */
|
|
332
|
+
export function projectRuntimeCatalogRow(
|
|
333
|
+
input: RuntimeCatalogRowInput | readonly RuntimeCatalogRowInput[],
|
|
334
|
+
): ProducerContractResult<CatalogEntryV2> {
|
|
335
|
+
if (Array.isArray(input)) {
|
|
336
|
+
if (input.length !== 1) {
|
|
337
|
+
return runtimeCatalogRowError(input[0]?.guid ?? 'unknown', 'exactly one runtime row');
|
|
338
|
+
}
|
|
339
|
+
const [row] = input;
|
|
340
|
+
if (row === undefined) return runtimeCatalogRowError('unknown', 'exactly one runtime row');
|
|
341
|
+
return projectRuntimeCatalogRowValue(row);
|
|
342
|
+
}
|
|
343
|
+
return projectRuntimeCatalogRowValue(input as RuntimeCatalogRowInput);
|
|
344
|
+
}
|
package/src/cli-asset.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { readSourceInventory } from './evidence/source-inventory.js';
|
|
|
27
27
|
import { isValidAssetGuidString } from './guid.js';
|
|
28
28
|
import { parsePackV2 } from './index.js';
|
|
29
29
|
import { scan } from './scanner.js';
|
|
30
|
-
import { projectScriptablePackMeta } from './scriptable-pack.js';
|
|
30
|
+
import { isRecord, projectScriptablePackMeta } from './scriptable-pack.js';
|
|
31
31
|
import { loadScriptablePack } from './scriptable-pack-node.js';
|
|
32
32
|
|
|
33
33
|
export interface PackEntry {
|
|
@@ -453,10 +453,6 @@ function catalogRows(value: unknown): readonly Record<string, unknown>[] | undef
|
|
|
453
453
|
return value.entries.filter(isRecord);
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
457
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
456
|
function packagePath(projectRoot: string, packageUrl: string): string {
|
|
461
457
|
const relative = packageUrl.replace(/^\/+/, '');
|
|
462
458
|
return resolve(projectRoot, relative);
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @forgeax/engine-pack
|
|
2
|
-
// Disk schema, GUID tools,
|
|
3
|
-
//
|
|
2
|
+
// Disk schema, GUID tools, and browser-safe asset contracts.
|
|
3
|
+
// Node-only catalog/build APIs live under @forgeax/engine-pack/build.
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
type CookedMaterialRecord,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type CookedMaterialRecord,
|
|
3
|
+
collectMaterialCookRefs,
|
|
4
|
+
createMaterialArtifactDigest,
|
|
5
|
+
type MaterialCookArtifact,
|
|
6
|
+
type MaterialCookReceipt,
|
|
7
|
+
type MaterialCookRecordError,
|
|
8
|
+
type MaterialCookRefs,
|
|
9
|
+
projectCookedMaterialRecord,
|
|
10
|
+
serializeCookedMaterialRecord,
|
|
11
|
+
serializeMaterialCookReceipt,
|
|
12
|
+
validateCookedMaterialRecord,
|
|
13
|
+
} from './evidence/material-cook.js';
|
|
@@ -28,6 +28,27 @@ export interface NativeCooker<P = unknown, I = unknown> {
|
|
|
28
28
|
cook(input: I): NativeCookDraft<P> | Promise<NativeCookDraft<P>>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export interface NativeCookTransactionSnapshot<P = unknown> {
|
|
32
|
+
readonly draft: NativeCookDraft<P>;
|
|
33
|
+
readonly generation: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NativeCookTransactionOptions<P = unknown, I = unknown> {
|
|
37
|
+
readonly key: string;
|
|
38
|
+
readonly input: I;
|
|
39
|
+
readonly previous?: NativeCookTransactionSnapshot<P>;
|
|
40
|
+
readonly validate?: (draft: NativeCookDraft<P>) => string | undefined;
|
|
41
|
+
readonly publish?: (draft: NativeCookDraft<P>, generation: number) => void | Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NativeCookTransactionResult<P = unknown> extends NativeCookTransactionSnapshot<P> {
|
|
45
|
+
readonly status: 'committed' | 'recovered';
|
|
46
|
+
readonly lastKnownGood: NativeCookDraft<P>;
|
|
47
|
+
readonly candidateGeneration: number;
|
|
48
|
+
readonly lastKnownGoodGeneration: number;
|
|
49
|
+
readonly recoveryHint?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
function canonicalize(value: unknown): unknown {
|
|
32
53
|
if (value instanceof Uint8Array) return Buffer.from(value).toString('base64');
|
|
33
54
|
if (Array.isArray(value)) return value.map(canonicalize);
|
|
@@ -67,6 +88,24 @@ function failure(key: string, detail: NativeCookErrorDetail): Result<never, Asse
|
|
|
67
88
|
});
|
|
68
89
|
}
|
|
69
90
|
|
|
91
|
+
function transactionFailure(key: string, reason: string): Result<never, AssetStageError> {
|
|
92
|
+
return failure(key, { guid: 'unknown', producer: reason });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function recovered<P>(
|
|
96
|
+
key: string,
|
|
97
|
+
previous: NativeCookTransactionSnapshot<P>,
|
|
98
|
+
): NativeCookTransactionResult<P> {
|
|
99
|
+
return {
|
|
100
|
+
...previous,
|
|
101
|
+
status: 'recovered',
|
|
102
|
+
lastKnownGood: previous.draft,
|
|
103
|
+
candidateGeneration: previous.generation + 1,
|
|
104
|
+
lastKnownGoodGeneration: previous.generation,
|
|
105
|
+
recoveryHint: `repair ${key} and submit a new candidate generation`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
70
109
|
/** Injectable build-time table for native authored Pack producers. */
|
|
71
110
|
export class NativeCookerRegistry {
|
|
72
111
|
private readonly cookers = new Map<string, NativeCooker>();
|
|
@@ -160,4 +199,37 @@ export class NativeCookerRegistry {
|
|
|
160
199
|
};
|
|
161
200
|
return ok(product);
|
|
162
201
|
}
|
|
202
|
+
|
|
203
|
+
async runTransaction<P = unknown, I = unknown>(
|
|
204
|
+
options: NativeCookTransactionOptions<P, I>,
|
|
205
|
+
): Promise<Result<NativeCookTransactionResult<P>, AssetStageError>> {
|
|
206
|
+
const candidate = await this.runDraft<P, I>(options.key, options.input);
|
|
207
|
+
if (!candidate.ok) {
|
|
208
|
+
return options.previous === undefined
|
|
209
|
+
? candidate
|
|
210
|
+
: ok(recovered(options.key, options.previous));
|
|
211
|
+
}
|
|
212
|
+
const invalidReason = options.validate?.(candidate.value);
|
|
213
|
+
if (invalidReason !== undefined) {
|
|
214
|
+
return options.previous === undefined
|
|
215
|
+
? transactionFailure(options.key, invalidReason)
|
|
216
|
+
: ok(recovered(options.key, options.previous));
|
|
217
|
+
}
|
|
218
|
+
const generation = (options.previous?.generation ?? 0) + 1;
|
|
219
|
+
try {
|
|
220
|
+
await options.publish?.(candidate.value, generation);
|
|
221
|
+
} catch {
|
|
222
|
+
return options.previous === undefined
|
|
223
|
+
? transactionFailure(options.key, 'atomic producer publication failed')
|
|
224
|
+
: ok(recovered(options.key, options.previous));
|
|
225
|
+
}
|
|
226
|
+
return ok({
|
|
227
|
+
draft: candidate.value,
|
|
228
|
+
generation,
|
|
229
|
+
status: 'committed',
|
|
230
|
+
lastKnownGood: candidate.value,
|
|
231
|
+
candidateGeneration: generation,
|
|
232
|
+
lastKnownGoodGeneration: generation,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
163
235
|
}
|