@voce-engine/core 0.1.0-rc.1
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/LICENSE +1 -0
- package/README.md +9 -0
- package/dist/canonical.d.ts +4 -0
- package/dist/canonical.js +30 -0
- package/dist/evidence.d.ts +31 -0
- package/dist/evidence.js +1053 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +608 -0
- package/dist/m4.d.ts +157 -0
- package/dist/m4.js +2250 -0
- package/dist/m5.d.ts +183 -0
- package/dist/m5.js +2433 -0
- package/dist/m6.d.ts +182 -0
- package/dist/m6.js +1183 -0
- package/package.json +50 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LocalScenarioPackSource, ScenarioPack, ScenarioPackCatalogSnapshot, ScenarioPackDescriptor, ScenarioPackRegistry, ScenarioPackResolution, ScenarioPackSelection } from '@voce-engine/contracts';
|
|
2
|
+
export type { JsonValue } from '@voce-engine/contracts';
|
|
3
|
+
export { canonicalize, hashWithoutSelf, sha256 } from './canonical.js';
|
|
4
|
+
export declare const RESOLVER_VERSION = "voce.scenario-pack-resolver/v1alpha1";
|
|
5
|
+
export declare function validateManifest(value: unknown): void;
|
|
6
|
+
export declare function resolveScenario(selection: ScenarioPackSelection, catalog: ScenarioPackCatalogSnapshot, packages: ReadonlyMap<string, ScenarioPack>, descriptors?: ReadonlyMap<string, ScenarioPackDescriptor>): ScenarioPackResolution;
|
|
7
|
+
export declare class MemoryScenarioPackRegistry implements ScenarioPackRegistry {
|
|
8
|
+
private revision;
|
|
9
|
+
private readonly packs;
|
|
10
|
+
private readonly descriptors;
|
|
11
|
+
private readonly policies;
|
|
12
|
+
register(source: LocalScenarioPackSource): ScenarioPackDescriptor;
|
|
13
|
+
list(): ScenarioPackDescriptor[];
|
|
14
|
+
snapshot(): ScenarioPackCatalogSnapshot;
|
|
15
|
+
resolve(selection: ScenarioPackSelection, catalog?: ScenarioPackCatalogSnapshot): ScenarioPackResolution;
|
|
16
|
+
}
|
|
17
|
+
export declare function createScenarioPackRegistry(): ScenarioPackRegistry;
|
|
18
|
+
export * from './evidence.js';
|
|
19
|
+
export * from './m4.js';
|
|
20
|
+
export * from './m5.js';
|
|
21
|
+
export * from './m6.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { valid as semverValid, validRange as semverValidRange, satisfies as semverSatisfies } from 'semver';
|
|
3
|
+
import { canonicalize, hashWithoutSelf, sha256 } from './canonical.js';
|
|
4
|
+
export { canonicalize, hashWithoutSelf, sha256 } from './canonical.js';
|
|
5
|
+
export const RESOLVER_VERSION = 'voce.scenario-pack-resolver/v1alpha1';
|
|
6
|
+
const CONTRACT_VERSION = 'voce.scenario-pack/v1alpha1';
|
|
7
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
8
|
+
const NORMAL_SEMVER = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
|
|
9
|
+
const keyOf = (packId, version) => `${packId}@${version}`;
|
|
10
|
+
function compareCodeUnits(a, b) {
|
|
11
|
+
const length = Math.min(a.length, b.length);
|
|
12
|
+
for (let index = 0; index < length; index += 1) {
|
|
13
|
+
const difference = a.charCodeAt(index) - b.charCodeAt(index);
|
|
14
|
+
if (difference !== 0)
|
|
15
|
+
return difference;
|
|
16
|
+
}
|
|
17
|
+
return a.length - b.length;
|
|
18
|
+
}
|
|
19
|
+
function bytesHash(bytes) {
|
|
20
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
21
|
+
}
|
|
22
|
+
function isExactSemVer(value) {
|
|
23
|
+
if (!NORMAL_SEMVER.test(value))
|
|
24
|
+
return false;
|
|
25
|
+
try {
|
|
26
|
+
return semverValid(value) === value;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function rangeMatches(range, version) {
|
|
33
|
+
try {
|
|
34
|
+
return semverSatisfies(version, range);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function safePath(path) {
|
|
41
|
+
return path.length > 0 && !path.includes('\\') && !path.startsWith('/') && !path.includes(':') && !path.split('/').some((part) => part === '' || part === '.' || part === '..');
|
|
42
|
+
}
|
|
43
|
+
function json(value) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function cloneData(value) {
|
|
47
|
+
if (value instanceof Uint8Array)
|
|
48
|
+
return new Uint8Array(value);
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
return value.map((item) => cloneData(item));
|
|
51
|
+
if (value && typeof value === 'object') {
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const [key, item] of Object.entries(value))
|
|
54
|
+
result[key] = cloneData(item);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function validateManifestBase(value) {
|
|
60
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
61
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
62
|
+
const object = value;
|
|
63
|
+
for (const field of ['schemaVersion', 'packId', 'version', 'kind', 'declarations', 'permissions', 'distributionInventory']) {
|
|
64
|
+
if (!(field in object))
|
|
65
|
+
throw new Error(`ScenarioPackManifest.${field}: required`);
|
|
66
|
+
}
|
|
67
|
+
if (object.schemaVersion !== 'voce.scenario-pack/v1alpha1')
|
|
68
|
+
throw new Error('PACK_SCHEMA_UNSUPPORTED');
|
|
69
|
+
if (typeof object.packId !== 'string' || !object.packId || typeof object.version !== 'string' || !object.version)
|
|
70
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
71
|
+
if (object.kind !== 'root' && object.kind !== 'extension')
|
|
72
|
+
throw new Error('PACK_KIND_INVALID');
|
|
73
|
+
const declarations = object.declarations;
|
|
74
|
+
const permissions = object.permissions;
|
|
75
|
+
if (!declarations || typeof declarations !== 'object' || Array.isArray(declarations) || !permissions || typeof permissions !== 'object' || Array.isArray(permissions))
|
|
76
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
77
|
+
for (const key of ['containsExecutableScenarioCode', 'distributionLifecycleScripts', 'containsExecutableFiles', 'fixturesRequireNetwork', 'fixturesRequireRealProvider']) {
|
|
78
|
+
if (declarations[key] !== false)
|
|
79
|
+
throw new Error('PACK_DECLARATION_INVALID');
|
|
80
|
+
}
|
|
81
|
+
for (const key of ['network', 'remoteCalls', 'secrets', 'filesystemWrite', 'mutateConfirmedFacts', 'authorizeCalls', 'overrideHostPolicy', 'selectProvider', 'changeBudgets']) {
|
|
82
|
+
if (permissions[key] !== false)
|
|
83
|
+
throw new Error('PACK_PERMISSION_FORBIDDEN');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function validateManifest(value) {
|
|
87
|
+
validateManifestBase(value);
|
|
88
|
+
}
|
|
89
|
+
function validateManifestStrict(manifest) {
|
|
90
|
+
validateManifestBase(manifest);
|
|
91
|
+
const requiredArrays = ['supportedInteractionModes', 'inputExpectations', 'outputExpectations', 'dependencies', 'conflicts', 'fixtures', 'migrations', 'capabilityRequirements'];
|
|
92
|
+
for (const field of requiredArrays)
|
|
93
|
+
if (!Array.isArray(manifest[field]))
|
|
94
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
95
|
+
for (const field of ['provenance', 'contractRanges', 'ui', 'composition', 'contributions']) {
|
|
96
|
+
const value = manifest[field];
|
|
97
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
98
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(manifest.distributionInventory))
|
|
101
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
102
|
+
if (!Array.isArray(manifest.composition.before) || !Array.isArray(manifest.composition.after) || manifest.composition.before.some((id) => typeof id !== 'string') || manifest.composition.after.some((id) => typeof id !== 'string'))
|
|
103
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
104
|
+
for (const category of ['ontologyVocabulary', 'rulePacks', 'interpretationScopes', 'promptSections', 'reviewTemplates', 'defaults', 'overridePoints']) {
|
|
105
|
+
if (!Array.isArray(manifest.contributions[category]))
|
|
106
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
107
|
+
}
|
|
108
|
+
if (manifest.kind === 'extension' && (!manifest.extensionOf || typeof manifest.extensionOf.rootPackId !== 'string' || typeof manifest.extensionOf.rootVersionRange !== 'string'))
|
|
109
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
110
|
+
if (!isExactSemVer(manifest.version))
|
|
111
|
+
throw new Error('PACK_VERSION_UNSATISFIABLE');
|
|
112
|
+
if (manifest.kind === 'root' ? manifest.extensionOf !== undefined : !manifest.extensionOf)
|
|
113
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
114
|
+
if (manifest.extensionOf && !isExactSemVer(manifest.extensionOf.rootVersionRange))
|
|
115
|
+
throw new Error('PACK_VERSION_UNSATISFIABLE');
|
|
116
|
+
const declarations = manifest.declarations;
|
|
117
|
+
const permissions = manifest.permissions;
|
|
118
|
+
if (declarations.containsExecutableScenarioCode || declarations.distributionLifecycleScripts || declarations.containsExecutableFiles || declarations.fixturesRequireNetwork || declarations.fixturesRequireRealProvider || declarations.collectsTelemetry)
|
|
119
|
+
throw new Error('PACK_DECLARATION_INVALID');
|
|
120
|
+
if (permissions.network || permissions.remoteCalls || permissions.secrets || permissions.filesystemWrite || permissions.mutateConfirmedFacts || permissions.authorizeCalls || permissions.overrideHostPolicy || permissions.selectProvider || permissions.changeBudgets)
|
|
121
|
+
throw new Error('PACK_PERMISSION_FORBIDDEN');
|
|
122
|
+
for (const dependency of manifest.dependencies) {
|
|
123
|
+
if (!dependency || typeof dependency.packId !== 'string' || typeof dependency.versionRange !== 'string' || typeof dependency.role !== 'string' || typeof dependency.reasonCode !== 'string')
|
|
124
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
125
|
+
if (dependency.role !== 'extension' || !isExactSemVer(dependency.versionRange))
|
|
126
|
+
throw new Error('PACK_DEPENDENCY_UNSATISFIABLE');
|
|
127
|
+
}
|
|
128
|
+
for (const conflict of manifest.conflicts) {
|
|
129
|
+
if (!conflict || typeof conflict.packId !== 'string' || typeof conflict.versionRange !== 'string' || typeof conflict.reasonCode !== 'string')
|
|
130
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
131
|
+
try {
|
|
132
|
+
if (semverValidRange(conflict.versionRange) === null)
|
|
133
|
+
throw new Error('PACK_VERSION_UNSATISFIABLE');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw new Error('PACK_VERSION_UNSATISFIABLE');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const paths = new Set();
|
|
140
|
+
const foldedPaths = new Set();
|
|
141
|
+
for (const file of manifest.distributionInventory) {
|
|
142
|
+
if (!file || typeof file.path !== 'string' || typeof file.contentDigest !== 'string' || typeof file.role !== 'string')
|
|
143
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
144
|
+
if (!safePath(file.path) || !DIGEST.test(file.contentDigest) || paths.has(file.path) || foldedPaths.has(file.path.toLowerCase()))
|
|
145
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
146
|
+
paths.add(file.path);
|
|
147
|
+
foldedPaths.add(file.path.toLowerCase());
|
|
148
|
+
}
|
|
149
|
+
if (manifest.distributionInventory.some((file) => file.path.toLowerCase() === 'scenario-pack/manifest.json'))
|
|
150
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
151
|
+
for (const category of ['ontologyVocabulary', 'rulePacks', 'interpretationScopes', 'promptSections', 'reviewTemplates', 'defaults', 'overridePoints']) {
|
|
152
|
+
const ids = new Set();
|
|
153
|
+
for (const descriptor of manifest.contributions[category]) {
|
|
154
|
+
if (!descriptor || typeof descriptor.id !== 'string' || typeof descriptor.schemaVersion !== 'string' || typeof descriptor.contentDigest !== 'string')
|
|
155
|
+
throw new Error('PACK_CONTRIBUTION_INVALID');
|
|
156
|
+
if (ids.has(descriptor.id) || !DIGEST.test(descriptor.contentDigest))
|
|
157
|
+
throw new Error('PACK_CONTRIBUTION_INVALID');
|
|
158
|
+
ids.add(descriptor.id);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function contributionDigest(value) {
|
|
163
|
+
const copy = { ...value };
|
|
164
|
+
delete copy.contentDigest;
|
|
165
|
+
return sha256(copy);
|
|
166
|
+
}
|
|
167
|
+
function descriptorFor(definition, files) {
|
|
168
|
+
if (!definition || typeof definition !== 'object' || Array.isArray(definition) || !definition.manifest)
|
|
169
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
170
|
+
validateManifestStrict(definition.manifest);
|
|
171
|
+
if (!definition.contributions || typeof definition.contributions !== 'object' || Array.isArray(definition.contributions))
|
|
172
|
+
throw new Error('PACK_CONTRIBUTION_INVALID');
|
|
173
|
+
const inventory = new Map(definition.manifest.distributionInventory.map((file) => [file.path, file]));
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
if (!safePath(file.path) || file.path.toLowerCase() === 'scenario-pack/manifest.json' || seen.has(file.path) || !inventory.has(file.path))
|
|
177
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
178
|
+
seen.add(file.path);
|
|
179
|
+
if (bytesHash(file.bytes) !== inventory.get(file.path).contentDigest)
|
|
180
|
+
throw new Error('PACK_DIGEST_MISMATCH');
|
|
181
|
+
}
|
|
182
|
+
if (seen.size !== inventory.size)
|
|
183
|
+
throw new Error('PACK_MANIFEST_INVALID');
|
|
184
|
+
for (const category of ['ontologyVocabulary', 'rulePacks', 'interpretationScopes', 'promptSections', 'reviewTemplates', 'defaults', 'overridePoints']) {
|
|
185
|
+
const indexed = new Map(definition.manifest.contributions[category].map((descriptor) => [descriptor.id, descriptor.contentDigest]));
|
|
186
|
+
const bodyIds = new Set();
|
|
187
|
+
const bodies = definition.contributions[category];
|
|
188
|
+
if (!Array.isArray(bodies) || bodies.length !== indexed.size)
|
|
189
|
+
throw new Error('PACK_CONTRIBUTION_INVALID');
|
|
190
|
+
for (const raw of bodies) {
|
|
191
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
192
|
+
throw new Error('PACK_CONTRIBUTION_INVALID');
|
|
193
|
+
const contribution = raw;
|
|
194
|
+
const id = typeof contribution.contributionId === 'string' ? contribution.contributionId : String(contribution.id);
|
|
195
|
+
if (typeof contribution.id !== 'string' || typeof contribution.contentDigest !== 'string' || bodyIds.has(id) || !indexed.has(id))
|
|
196
|
+
throw new Error('PACK_CONTRIBUTION_INVALID');
|
|
197
|
+
bodyIds.add(id);
|
|
198
|
+
if (indexed.get(id) !== contribution.contentDigest || contributionDigest(contribution) !== contribution.contentDigest)
|
|
199
|
+
throw new Error('PACK_DIGEST_MISMATCH');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const manifestHash = sha256(json(definition.manifest));
|
|
203
|
+
const normalizedFiles = files.map((file) => ({ path: file.path, contentDigest: bytesHash(file.bytes), byteLength: file.bytes.byteLength, role: inventory.get(file.path).role })).sort((left, right) => compareCodeUnits(left.path, right.path));
|
|
204
|
+
const packageDigest = sha256({ manifestHash, files: normalizedFiles });
|
|
205
|
+
const distributionDigest = sha256({ files: normalizedFiles });
|
|
206
|
+
return { manifest: definition.manifest, manifestHash, packageDigest, distributionDigest, provenance: definition.manifest.provenance, acquisition: { sourceKind: 'memory', sourceLocator: 'memory', distributionDigest, lifecycleScriptsExecuted: false } };
|
|
207
|
+
}
|
|
208
|
+
function selectedEntry(catalog, request) {
|
|
209
|
+
return catalog.entries.find((entry) => entry.manifest.packId === request.packId && entry.manifest.version === request.versionRange);
|
|
210
|
+
}
|
|
211
|
+
function conflict(code, packIds, reason, action, contributionIds = [], overrideIds = []) {
|
|
212
|
+
return { code, packIds, contributionIds, overrideIds: Array.isArray(overrideIds) ? overrideIds : [], reason, action };
|
|
213
|
+
}
|
|
214
|
+
function failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, conflicts, warnings) {
|
|
215
|
+
const base = { status: 'blocked', selected: selected.map((entry) => ({ packId: entry.manifest.packId, version: entry.manifest.version, kind: entry.manifest.kind, packageDigest: entry.packageDigest, manifestHash: entry.manifestHash })), dependencyTrace, compositionTrace, overrideTraces, conflicts, warnings };
|
|
216
|
+
return { ...base, reportHash: hashWithoutSelf(base, 'reportHash') };
|
|
217
|
+
}
|
|
218
|
+
function sortedDescriptors(entries) {
|
|
219
|
+
return [...entries].sort((left, right) => compareCodeUnits(keyOf(left.manifest.packId, left.manifest.version), keyOf(right.manifest.packId, right.manifest.version)) || compareCodeUnits(left.packageDigest, right.packageDigest));
|
|
220
|
+
}
|
|
221
|
+
function catalogBase(catalog) {
|
|
222
|
+
return { contractVersion: catalog.contractVersion, resolverVersion: catalog.resolverVersion, registryRevision: catalog.registryRevision, entries: sortedDescriptors(catalog.entries), availabilityPolicies: [...catalog.availabilityPolicies].sort((left, right) => compareCodeUnits(left.policyHash, right.policyHash)) };
|
|
223
|
+
}
|
|
224
|
+
function validateCatalog(catalog, descriptors) {
|
|
225
|
+
if (!catalog || typeof catalog !== 'object' || !Array.isArray(catalog.entries) || !Array.isArray(catalog.availabilityPolicies))
|
|
226
|
+
return conflict('PACK_MANIFEST_INVALID', [], 'Catalog snapshot shape is invalid.', 'Use a Registry-produced catalog snapshot.');
|
|
227
|
+
if (catalog.entries.some((entry) => !entry || typeof entry !== 'object' || !entry.manifest || typeof entry.manifest !== 'object') || catalog.availabilityPolicies.some((policy) => !policy || typeof policy !== 'object' || typeof policy.policyHash !== 'string'))
|
|
228
|
+
return conflict('PACK_MANIFEST_INVALID', [], 'Catalog snapshot entry shape is invalid.', 'Use a Registry-produced catalog snapshot.');
|
|
229
|
+
if (catalog.contractVersion !== CONTRACT_VERSION || catalog.resolverVersion !== RESOLVER_VERSION)
|
|
230
|
+
return conflict('PACK_COMPATIBILITY_MISMATCH', [], 'Catalog contract or resolver version is not supported.', 'Use a snapshot produced by this M2 Registry.');
|
|
231
|
+
if (!DIGEST.test(catalog.catalogHash) || sha256(json(catalogBase(catalog))) !== catalog.catalogHash)
|
|
232
|
+
return conflict('PACK_DIGEST_MISMATCH', [], 'Catalog hash does not match its canonical snapshot payload.', 'Refresh the explicit local catalog snapshot.');
|
|
233
|
+
const seen = new Set();
|
|
234
|
+
for (const entry of catalog.entries) {
|
|
235
|
+
if (!entry || typeof entry !== 'object' || !entry.manifest)
|
|
236
|
+
return conflict('PACK_MANIFEST_INVALID', [], 'Catalog entry shape is invalid.', 'Refresh the catalog from the Registry.');
|
|
237
|
+
try {
|
|
238
|
+
validateManifestStrict(entry.manifest);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const code = error instanceof Error && error.message.startsWith('PACK_') ? error.message : 'PACK_MANIFEST_INVALID';
|
|
242
|
+
return conflict(code, [typeof entry.manifest.packId === 'string' ? entry.manifest.packId : ''], 'Catalog entry manifest is invalid.', 'Refresh the catalog from the Registry.');
|
|
243
|
+
}
|
|
244
|
+
const key = keyOf(entry.manifest.packId, entry.manifest.version);
|
|
245
|
+
if (seen.has(key))
|
|
246
|
+
return conflict('PACK_DUPLICATE_ID_VERSION', [entry.manifest.packId], 'Catalog contains a duplicate pack identity.', 'Remove the duplicate entry.');
|
|
247
|
+
seen.add(key);
|
|
248
|
+
const local = descriptors.get(key);
|
|
249
|
+
if (!local)
|
|
250
|
+
return conflict('PACK_NOT_FOUND', [entry.manifest.packId], 'Catalog contains a pack not registered in this Registry.', 'Use this Registry snapshot without external entries.');
|
|
251
|
+
if (sha256(json(entry.manifest)) !== entry.manifestHash || local.manifestHash !== entry.manifestHash || canonicalize(json(local.manifest)) !== canonicalize(json(entry.manifest)) || local.packageDigest !== entry.packageDigest || local.distributionDigest !== entry.distributionDigest)
|
|
252
|
+
return conflict('PACK_DIGEST_MISMATCH', [entry.manifest.packId], 'Catalog descriptor manifest or digests do not match the registered local package.', 'Refresh the catalog from the Registry.');
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
function pointerSegments(pointer) {
|
|
257
|
+
if (pointer === '')
|
|
258
|
+
return [];
|
|
259
|
+
if (!pointer.startsWith('/') || pointer.includes('//'))
|
|
260
|
+
return undefined;
|
|
261
|
+
return pointer.slice(1).split('/').map((segment) => {
|
|
262
|
+
if (/~(?![01])/.test(segment))
|
|
263
|
+
return undefined;
|
|
264
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function setJsonPointer(source, pointer, value) {
|
|
268
|
+
const segments = pointerSegments(pointer);
|
|
269
|
+
if (!segments)
|
|
270
|
+
return undefined;
|
|
271
|
+
if (segments.length === 0) {
|
|
272
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
273
|
+
}
|
|
274
|
+
const result = JSON.parse(JSON.stringify(source));
|
|
275
|
+
let cursor = result;
|
|
276
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
277
|
+
const segment = segments[index];
|
|
278
|
+
const next = cursor[segment];
|
|
279
|
+
if (next === null || typeof next !== 'object' || Array.isArray(next))
|
|
280
|
+
return undefined;
|
|
281
|
+
cursor = next;
|
|
282
|
+
}
|
|
283
|
+
cursor[segments[segments.length - 1]] = value;
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
function schemaAccepts(point, value) {
|
|
287
|
+
const schemaId = point.valueSchema?.schemaId;
|
|
288
|
+
if (!schemaId)
|
|
289
|
+
return true;
|
|
290
|
+
if (schemaId === 'string')
|
|
291
|
+
return typeof value === 'string';
|
|
292
|
+
if (schemaId === 'number')
|
|
293
|
+
return typeof value === 'number';
|
|
294
|
+
if (schemaId === 'boolean')
|
|
295
|
+
return typeof value === 'boolean';
|
|
296
|
+
if (schemaId === 'object')
|
|
297
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
298
|
+
if (schemaId === 'array')
|
|
299
|
+
return Array.isArray(value);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
function contributionId(value) {
|
|
303
|
+
const object = value;
|
|
304
|
+
return typeof object.contributionId === 'string' ? object.contributionId : String(object.id);
|
|
305
|
+
}
|
|
306
|
+
function withSource(value, packId, category, sourcePackIds) {
|
|
307
|
+
const contribution = { ...value };
|
|
308
|
+
contribution.packId = packId;
|
|
309
|
+
contribution.contributionKind = category;
|
|
310
|
+
contribution.contributionId = contributionId(value);
|
|
311
|
+
contribution.sourcePackIds = sourcePackIds;
|
|
312
|
+
return contribution;
|
|
313
|
+
}
|
|
314
|
+
function composeCategory(category, ordered, packages, disabled, defaultValues) {
|
|
315
|
+
const byId = new Map();
|
|
316
|
+
for (const descriptor of ordered) {
|
|
317
|
+
const pack = packages.get(keyOf(descriptor.manifest.packId, descriptor.manifest.version));
|
|
318
|
+
for (const raw of pack.contributions[category]) {
|
|
319
|
+
const id = contributionId(raw);
|
|
320
|
+
if (disabled.has(`${category}:${descriptor.manifest.packId}:${id}`))
|
|
321
|
+
continue;
|
|
322
|
+
const object = raw;
|
|
323
|
+
const digest = String(object.contentDigest);
|
|
324
|
+
const target = byId.get(id);
|
|
325
|
+
if (target && target.digest !== digest)
|
|
326
|
+
return { values: [], collision: conflict('PACK_RULE_CONFLICT', [target.packId, descriptor.manifest.packId], 'Equal contribution IDs have different content digests.', 'Rename the contribution or make its content identical.', [id]) };
|
|
327
|
+
if (target) {
|
|
328
|
+
target.sourcePackIds.push(descriptor.manifest.packId);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const defaultTarget = typeof object.targetPath === 'string' ? object.targetPath : id;
|
|
332
|
+
const value = category === 'defaults' && defaultValues.has(`${descriptor.manifest.packId}|${defaultTarget}`) ? { ...object, value: defaultValues.get(`${descriptor.manifest.packId}|${defaultTarget}`), source: { kind: 'host_override', target: defaultTarget } } : object;
|
|
333
|
+
byId.set(id, { value, digest, packId: descriptor.manifest.packId, sourcePackIds: [descriptor.manifest.packId] });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { values: [...byId.values()].map((item) => withSource(item.value, item.packId, category, item.sourcePackIds)) };
|
|
337
|
+
}
|
|
338
|
+
function overridePayload(override) {
|
|
339
|
+
return { id: override.id, operation: override.operation, reasonCode: override.reasonCode };
|
|
340
|
+
}
|
|
341
|
+
function overlayPayload(overlay) {
|
|
342
|
+
const overrides = [...overlay.overrides].sort((left, right) => compareCodeUnits(left.contentHash, right.contentHash) || compareCodeUnits(left.id, right.id));
|
|
343
|
+
return { id: overlay.id, caseId: overlay.caseId, caseRevision: overlay.caseRevision, overrides: overrides, authority: overlay.authority, reasonCode: overlay.reasonCode };
|
|
344
|
+
}
|
|
345
|
+
function validateOverlay(overlay) {
|
|
346
|
+
if (!DIGEST.test(overlay.overlayHash) || sha256(overlayPayload(overlay)) !== overlay.overlayHash)
|
|
347
|
+
return conflict('PACK_OVERRIDE_INVALID', [], 'Host policy overlay hash is invalid.', 'Recompute the overlay hash from the canonical overlay payload.');
|
|
348
|
+
for (const override of overlay.overrides)
|
|
349
|
+
if (!DIGEST.test(override.contentHash) || sha256(overridePayload(override)) !== override.contentHash)
|
|
350
|
+
return conflict('PACK_OVERRIDE_INVALID', [override.operation.packId], 'Host override content hash is invalid.', 'Recompute the override hash from its canonical payload.', [], [override.id]);
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
export function resolveScenario(selection, catalog, packages, descriptors = new Map()) {
|
|
354
|
+
const selected = [];
|
|
355
|
+
const dependencyTrace = [];
|
|
356
|
+
const compositionTrace = [];
|
|
357
|
+
const overrideTraces = [];
|
|
358
|
+
const conflicts = [];
|
|
359
|
+
const warnings = [];
|
|
360
|
+
const catalogConflict = validateCatalog(catalog, descriptors);
|
|
361
|
+
if (catalogConflict)
|
|
362
|
+
return { status: 'blocked', report: failureReport([], [], [], [], [catalogConflict], []) };
|
|
363
|
+
const rootRequest = selection.root;
|
|
364
|
+
if (!isExactSemVer(rootRequest.versionRange))
|
|
365
|
+
conflicts.push(conflict('PACK_VERSION_UNSATISFIABLE', [rootRequest.packId], 'Root request must use exact normal SemVer.', 'Use x.y.z.'));
|
|
366
|
+
const root = selectedEntry(catalog, rootRequest);
|
|
367
|
+
if (!root)
|
|
368
|
+
conflicts.push(conflict('PACK_NOT_FOUND', [rootRequest.packId], 'Root is not present at the exact requested version.', 'Register the exact local pack version.'));
|
|
369
|
+
if (root && root.manifest.kind !== 'root')
|
|
370
|
+
conflicts.push(conflict('PACK_ROOT_REQUIRED', [root.manifest.packId], 'Root request selected an extension.', 'Select a root pack.'));
|
|
371
|
+
if (!root || conflicts.length)
|
|
372
|
+
return { status: 'blocked', report: failureReport(root ? [root] : [], [], [], [], conflicts, []) };
|
|
373
|
+
selected.push(root);
|
|
374
|
+
const add = (descriptor, request, owner) => {
|
|
375
|
+
if (!isExactSemVer(request.versionRange)) {
|
|
376
|
+
conflicts.push(conflict('PACK_VERSION_UNSATISFIABLE', [request.packId], 'Pack request must use exact normal SemVer.', 'Use x.y.z.'));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const extension = descriptor.manifest.extensionOf;
|
|
380
|
+
if (descriptor.manifest.kind !== 'extension' || !extension || extension.rootPackId !== root.manifest.packId || extension.rootVersionRange !== root.manifest.version)
|
|
381
|
+
conflicts.push(conflict('PACK_COMPATIBILITY_MISMATCH', [owner.manifest.packId, descriptor.manifest.packId], 'Extension root compatibility does not match the selected root.', 'Select a compatible extension.'));
|
|
382
|
+
if (!selected.some((entry) => keyOf(entry.manifest.packId, entry.manifest.version) === keyOf(descriptor.manifest.packId, descriptor.manifest.version)))
|
|
383
|
+
selected.push(descriptor);
|
|
384
|
+
};
|
|
385
|
+
for (const request of selection.extensions) {
|
|
386
|
+
const entry = selectedEntry(catalog, request);
|
|
387
|
+
if (!entry)
|
|
388
|
+
conflicts.push(conflict('PACK_NOT_FOUND', [request.packId], 'Explicit extension is missing.', 'Register the exact local extension version.'));
|
|
389
|
+
else
|
|
390
|
+
add(entry, request, root);
|
|
391
|
+
}
|
|
392
|
+
for (let index = 0; index < selected.length; index += 1) {
|
|
393
|
+
const owner = selected[index];
|
|
394
|
+
for (const dependency of owner.manifest.dependencies) {
|
|
395
|
+
const entry = selectedEntry(catalog, { packId: dependency.packId, versionRange: dependency.versionRange });
|
|
396
|
+
if (!entry) {
|
|
397
|
+
dependencyTrace.push({ packId: owner.manifest.packId, dependencyPackId: dependency.packId, status: 'missing', reasonCode: 'PACK_DEPENDENCY_MISSING' });
|
|
398
|
+
conflicts.push(conflict('PACK_DEPENDENCY_MISSING', [owner.manifest.packId, dependency.packId], 'A declared dependency is absent.', 'Register the exact dependency.'));
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
dependencyTrace.push({ packId: owner.manifest.packId, dependencyPackId: dependency.packId, status: 'resolved', reasonCode: 'PACK_DEPENDENCY_RESOLVED' });
|
|
402
|
+
add(entry, { packId: entry.manifest.packId, versionRange: entry.manifest.version }, owner);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
for (const owner of selected)
|
|
407
|
+
for (const declaredConflict of owner.manifest.conflicts)
|
|
408
|
+
if (selected.some((entry) => entry.manifest.packId === declaredConflict.packId && rangeMatches(declaredConflict.versionRange, entry.manifest.version)))
|
|
409
|
+
conflicts.push(conflict('PACK_CONFLICT', [owner.manifest.packId, declaredConflict.packId], 'Manifest declares an explicit SemVer conflict.', 'Remove one conflicting selection.'));
|
|
410
|
+
if (conflicts.length)
|
|
411
|
+
return { status: 'blocked', report: failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, conflicts, warnings) };
|
|
412
|
+
const nodes = selected.map((entry) => keyOf(entry.manifest.packId, entry.manifest.version));
|
|
413
|
+
const edges = new Map(nodes.map((node) => [node, new Set()]));
|
|
414
|
+
const dependencyEdges = [];
|
|
415
|
+
const addEdge = (from, to, reasonCode) => {
|
|
416
|
+
if (from !== to && edges.has(from) && edges.has(to) && !edges.get(from).has(to)) {
|
|
417
|
+
edges.get(from).add(to);
|
|
418
|
+
compositionTrace.push({ from, to, reasonCode });
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
for (const entry of selected) {
|
|
422
|
+
const from = keyOf(entry.manifest.packId, entry.manifest.version);
|
|
423
|
+
if (entry.manifest.composition.before.includes(entry.manifest.packId) || entry.manifest.composition.after.includes(entry.manifest.packId))
|
|
424
|
+
conflicts.push(conflict('PACK_ORDER_CYCLE', [entry.manifest.packId], 'A pack declares itself before or after itself.', 'Remove the self-reference.'));
|
|
425
|
+
for (const dependency of entry.manifest.dependencies) {
|
|
426
|
+
const target = keyOf(dependency.packId, dependency.versionRange);
|
|
427
|
+
dependencyEdges.push([target, from]);
|
|
428
|
+
addEdge(target, from, 'PACK_DEPENDENCY_ORDER');
|
|
429
|
+
}
|
|
430
|
+
for (const before of entry.manifest.composition.before) {
|
|
431
|
+
const target = nodes.find((node) => node.startsWith(`${before}@`));
|
|
432
|
+
if (target)
|
|
433
|
+
addEdge(from, target, 'PACK_MANIFEST_BEFORE');
|
|
434
|
+
}
|
|
435
|
+
for (const after of entry.manifest.composition.after) {
|
|
436
|
+
const target = nodes.find((node) => node.startsWith(`${after}@`));
|
|
437
|
+
if (target)
|
|
438
|
+
addEdge(target, from, 'PACK_MANIFEST_AFTER');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (conflicts.length)
|
|
442
|
+
return { status: 'blocked', report: failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, conflicts, warnings) };
|
|
443
|
+
for (const entry of selected.filter((item) => item.manifest.kind === 'extension'))
|
|
444
|
+
addEdge(keyOf(root.manifest.packId, root.manifest.version), keyOf(entry.manifest.packId, entry.manifest.version), 'ROOT_BEFORE_EXTENSION');
|
|
445
|
+
const dependencyRemaining = new Set(nodes);
|
|
446
|
+
while (dependencyRemaining.size) {
|
|
447
|
+
const ready = [...dependencyRemaining].filter((node) => !dependencyEdges.some(([from, to]) => dependencyRemaining.has(from) && to === node)).sort(compareCodeUnits);
|
|
448
|
+
if (!ready.length)
|
|
449
|
+
break;
|
|
450
|
+
dependencyRemaining.delete(ready[0]);
|
|
451
|
+
}
|
|
452
|
+
if (dependencyRemaining.size) {
|
|
453
|
+
for (const trace of dependencyTrace)
|
|
454
|
+
if (dependencyRemaining.has(keyOf(trace.packId, selected.find((entry) => entry.manifest.packId === trace.packId)?.manifest.version ?? '')) || dependencyRemaining.has(keyOf(trace.dependencyPackId, selected.find((entry) => entry.manifest.packId === trace.dependencyPackId)?.manifest.version ?? '')))
|
|
455
|
+
trace.status = 'cycle';
|
|
456
|
+
return { status: 'blocked', report: failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, [conflict('PACK_DEPENDENCY_UNSATISFIABLE', [...dependencyRemaining].map((node) => node.split('@')[0]), 'Dependencies contain a cycle.', 'Remove the dependency cycle.')], warnings) };
|
|
457
|
+
}
|
|
458
|
+
const order = [];
|
|
459
|
+
const remaining = new Set(nodes);
|
|
460
|
+
while (remaining.size) {
|
|
461
|
+
const ready = [...remaining].filter((node) => ![...edges].some(([from, targets]) => remaining.has(from) && targets.has(node))).sort(compareCodeUnits);
|
|
462
|
+
if (!ready.length)
|
|
463
|
+
return { status: 'blocked', report: failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, [conflict('PACK_ORDER_CYCLE', [...remaining].map((node) => node.split('@')[0]), 'Composition edges contain a cycle.', 'Remove the composition cycle.')], warnings) };
|
|
464
|
+
order.push(ready[0]);
|
|
465
|
+
remaining.delete(ready[0]);
|
|
466
|
+
}
|
|
467
|
+
const ordered = order.map((node) => selected.find((entry) => keyOf(entry.manifest.packId, entry.manifest.version) === node));
|
|
468
|
+
const localPackages = packages;
|
|
469
|
+
const configs = new Map();
|
|
470
|
+
for (const entry of ordered) {
|
|
471
|
+
const request = [selection.root, ...selection.extensions].find((item) => item.packId === entry.manifest.packId && item.versionRange === entry.manifest.version);
|
|
472
|
+
configs.set(entry.manifest.packId, JSON.parse(JSON.stringify(request?.configuration ?? {})));
|
|
473
|
+
}
|
|
474
|
+
const overlay = selection.hostPolicyOverlay;
|
|
475
|
+
if (overlay) {
|
|
476
|
+
const overlayConflict = validateOverlay(overlay);
|
|
477
|
+
if (overlayConflict)
|
|
478
|
+
return { status: 'blocked', report: failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, [overlayConflict], warnings) };
|
|
479
|
+
}
|
|
480
|
+
const disabled = new Set();
|
|
481
|
+
const defaultValues = new Map();
|
|
482
|
+
const appliedOverrides = [];
|
|
483
|
+
const seenTargets = new Map();
|
|
484
|
+
const sortedOverrides = [...(overlay?.overrides ?? [])].sort((left, right) => compareCodeUnits(left.contentHash, right.contentHash) || compareCodeUnits(left.id, right.id));
|
|
485
|
+
for (const override of sortedOverrides) {
|
|
486
|
+
const packDescriptor = ordered.find((entry) => entry.manifest.packId === override.operation.packId);
|
|
487
|
+
const pack = packDescriptor ? localPackages.get(keyOf(packDescriptor.manifest.packId, packDescriptor.manifest.version)) : undefined;
|
|
488
|
+
const point = pack?.contributions.overridePoints.find((candidate) => candidate.id === override.operation.overridePointId);
|
|
489
|
+
const operation = override.operation;
|
|
490
|
+
const expectedKind = operation.kind === 'set_configuration' ? 'configuration' : operation.kind === 'set_declared_default' ? 'declared_default' : 'contribution_activation';
|
|
491
|
+
let reasonCode = '';
|
|
492
|
+
if (!packDescriptor)
|
|
493
|
+
reasonCode = 'PACK_NOT_FOUND';
|
|
494
|
+
else if (!point)
|
|
495
|
+
reasonCode = 'PACK_OVERRIDE_POINT_NOT_FOUND';
|
|
496
|
+
else if (point.targetKind !== expectedKind)
|
|
497
|
+
reasonCode = 'PACK_OVERRIDE_INVALID';
|
|
498
|
+
else if (operation.kind === 'set_configuration' && pointerSegments(point.targetPath) === undefined)
|
|
499
|
+
reasonCode = 'PACK_OVERRIDE_INVALID';
|
|
500
|
+
else if (operation.kind !== 'set_configuration' && !point.targetPath)
|
|
501
|
+
reasonCode = 'PACK_OVERRIDE_INVALID';
|
|
502
|
+
else if (operation.kind === 'set_contribution_activation' && !operation.active && !point.allowDisable)
|
|
503
|
+
reasonCode = 'PACK_OVERRIDE_FORBIDDEN';
|
|
504
|
+
else if (operation.kind !== 'set_contribution_activation' && !schemaAccepts(point, operation.value))
|
|
505
|
+
reasonCode = 'PACK_OVERRIDE_INVALID';
|
|
506
|
+
const target = point ? `${operation.packId}|${point.targetKind}|${point.targetPath}` : `${operation.packId}|${operation.overridePointId}`;
|
|
507
|
+
const operationCanonical = canonicalize(operation);
|
|
508
|
+
const prior = seenTargets.get(target);
|
|
509
|
+
if (!reasonCode && prior && (prior.operation !== operationCanonical || prior.contentHash !== override.contentHash))
|
|
510
|
+
reasonCode = 'PACK_OVERRIDE_INVALID';
|
|
511
|
+
if (reasonCode) {
|
|
512
|
+
overrideTraces.push({ hostOverrideId: override.id, packId: operation.packId, overridePointId: operation.overridePointId, status: 'blocked', reasonCode });
|
|
513
|
+
conflicts.push(conflict(reasonCode, [operation.packId], 'Host override is outside the declared typed override point or conflicts with another override.', 'Use one valid operation per effective target.', [], [override.id]));
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (prior)
|
|
517
|
+
continue;
|
|
518
|
+
seenTargets.set(target, { operation: operationCanonical, contentHash: override.contentHash });
|
|
519
|
+
if (operation.kind === 'set_configuration') {
|
|
520
|
+
const current = configs.get(operation.packId) ?? {};
|
|
521
|
+
const next = setJsonPointer(current, point.targetPath, operation.value);
|
|
522
|
+
if (!next) {
|
|
523
|
+
overrideTraces.push({ hostOverrideId: override.id, packId: operation.packId, overridePointId: operation.overridePointId, status: 'blocked', reasonCode: 'PACK_OVERRIDE_INVALID' });
|
|
524
|
+
conflicts.push(conflict('PACK_OVERRIDE_INVALID', [operation.packId], 'Configuration target path cannot be applied to the current JSON object.', 'Use a valid JSON Pointer target path.', [], [override.id]));
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
configs.set(operation.packId, next);
|
|
528
|
+
}
|
|
529
|
+
else if (operation.kind === 'set_declared_default') {
|
|
530
|
+
defaultValues.set(`${operation.packId}|${point.targetPath}`, operation.value);
|
|
531
|
+
}
|
|
532
|
+
else if (!operation.active) {
|
|
533
|
+
disabled.add(`ontologyVocabulary:${operation.packId}:${point.targetPath}`);
|
|
534
|
+
disabled.add(`rulePacks:${operation.packId}:${point.targetPath}`);
|
|
535
|
+
disabled.add(`interpretationScopes:${operation.packId}:${point.targetPath}`);
|
|
536
|
+
disabled.add(`promptSections:${operation.packId}:${point.targetPath}`);
|
|
537
|
+
disabled.add(`reviewTemplates:${operation.packId}:${point.targetPath}`);
|
|
538
|
+
disabled.add(`defaults:${operation.packId}:${point.targetPath}`);
|
|
539
|
+
}
|
|
540
|
+
overrideTraces.push({ hostOverrideId: override.id, packId: operation.packId, overridePointId: operation.overridePointId, status: 'applied', reasonCode: 'PACK_OVERRIDE_APPLIED' });
|
|
541
|
+
appliedOverrides.push({ packId: operation.packId, overridePointId: operation.overridePointId, hostOverrideId: override.id, contentHash: override.contentHash });
|
|
542
|
+
}
|
|
543
|
+
if (conflicts.length)
|
|
544
|
+
return { status: 'blocked', report: failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, conflicts, warnings) };
|
|
545
|
+
const categories = ['ontologyVocabulary', 'rulePacks', 'interpretationScopes', 'promptSections', 'reviewTemplates', 'defaults'];
|
|
546
|
+
const effectiveValues = { ontologyVocabulary: [], rulePacks: [], interpretationScopes: [], promptSections: [], reviewTemplates: [], defaults: [] };
|
|
547
|
+
for (const category of categories) {
|
|
548
|
+
const composed = composeCategory(category, ordered, localPackages, disabled, defaultValues);
|
|
549
|
+
if (composed.collision)
|
|
550
|
+
return { status: 'blocked', report: failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, [composed.collision], warnings) };
|
|
551
|
+
effectiveValues[category] = composed.values;
|
|
552
|
+
}
|
|
553
|
+
const entries = ordered.map((entry) => ({
|
|
554
|
+
packId: entry.manifest.packId,
|
|
555
|
+
version: entry.manifest.version,
|
|
556
|
+
kind: entry.manifest.kind,
|
|
557
|
+
manifestHash: entry.manifestHash,
|
|
558
|
+
packageDigest: entry.packageDigest,
|
|
559
|
+
configurationHash: sha256(configs.get(entry.manifest.packId) ?? {}),
|
|
560
|
+
resolvedDependencies: entry.manifest.dependencies.map((dependency) => ({ packId: dependency.packId, version: dependency.versionRange, packageDigest: selectedEntry(catalog, { packId: dependency.packId, versionRange: dependency.versionRange }).packageDigest })),
|
|
561
|
+
contributionDigests: Object.fromEntries(categories.flatMap((category) => entry.manifest.contributions[category].map((item) => [`${category}:${item.id}`, item.contentDigest]))),
|
|
562
|
+
}));
|
|
563
|
+
const lockBase = { schemaVersion: 'voce.scenario-pack-lock/v1alpha1', contractVersion: CONTRACT_VERSION, resolverVersion: catalog.resolverVersion, catalogHash: catalog.catalogHash, canonicalization: 'voce.canonical-json/v1alpha1', rootPackId: root.manifest.packId, entries, compositionOrder: order, ...(overlay ? { hostPolicyOverlayHash: overlay.overlayHash } : {}), hostOverrideHashes: appliedOverrides.map((item) => item.contentHash) };
|
|
564
|
+
const lock = { ...lockBase, compositionHash: sha256(json(lockBase)), lockHash: '' };
|
|
565
|
+
lock.lockHash = hashWithoutSelf(lock, 'lockHash');
|
|
566
|
+
const effective = { lockHash: lock.lockHash, rootPackId: root.manifest.packId, extensionPackIds: ordered.filter((entry) => entry.manifest.kind === 'extension').map((entry) => entry.manifest.packId), compositionOrder: order, configurations: Object.fromEntries([...configs].sort((left, right) => compareCodeUnits(left[0], right[0]))), ...effectiveValues, capabilityRequirements: ordered.flatMap((entry) => entry.manifest.capabilityRequirements), declarations: ordered.map((entry) => entry.manifest.declarations), appliedOverrides, effectiveScenarioHash: '' };
|
|
567
|
+
effective.effectiveScenarioHash = hashWithoutSelf(effective, 'effectiveScenarioHash');
|
|
568
|
+
const reportBase = { status: 'resolved', lockHash: lock.lockHash, effectiveScenarioHash: effective.effectiveScenarioHash, selected: selected.map((entry) => ({ packId: entry.manifest.packId, version: entry.manifest.version, kind: entry.manifest.kind, packageDigest: entry.packageDigest, manifestHash: entry.manifestHash })), dependencyTrace, compositionTrace, overrideTraces, conflicts: [], warnings };
|
|
569
|
+
return { status: 'resolved', lock, effectiveScenario: effective, report: { ...reportBase, reportHash: hashWithoutSelf(reportBase, 'reportHash') } };
|
|
570
|
+
}
|
|
571
|
+
export class MemoryScenarioPackRegistry {
|
|
572
|
+
revision = 0;
|
|
573
|
+
packs = new Map();
|
|
574
|
+
descriptors = new Map();
|
|
575
|
+
policies = [];
|
|
576
|
+
register(source) {
|
|
577
|
+
if (source.kind !== 'memory')
|
|
578
|
+
throw new Error('PACK_SOURCE_UNSUPPORTED');
|
|
579
|
+
const definition = cloneData(source.definition);
|
|
580
|
+
const files = cloneData(source.logicalFiles);
|
|
581
|
+
const descriptor = descriptorFor(definition, files);
|
|
582
|
+
const key = keyOf(descriptor.manifest.packId, descriptor.manifest.version);
|
|
583
|
+
const prior = this.descriptors.get(key);
|
|
584
|
+
if (prior && prior.packageDigest !== descriptor.packageDigest)
|
|
585
|
+
throw new Error('PACK_DUPLICATE_ID_VERSION');
|
|
586
|
+
this.descriptors.set(key, cloneData(descriptor));
|
|
587
|
+
this.packs.set(key, definition);
|
|
588
|
+
this.revision += 1;
|
|
589
|
+
return cloneData(descriptor);
|
|
590
|
+
}
|
|
591
|
+
list() {
|
|
592
|
+
return cloneData(sortedDescriptors([...this.descriptors.values()]));
|
|
593
|
+
}
|
|
594
|
+
snapshot() {
|
|
595
|
+
const base = { contractVersion: CONTRACT_VERSION, resolverVersion: RESOLVER_VERSION, registryRevision: this.revision, entries: this.list(), availabilityPolicies: this.policies };
|
|
596
|
+
return cloneData({ ...base, catalogHash: sha256(json(base)) });
|
|
597
|
+
}
|
|
598
|
+
resolve(selection, catalog = this.snapshot()) {
|
|
599
|
+
return cloneData(resolveScenario(selection, catalog, this.packs, this.descriptors));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
export function createScenarioPackRegistry() {
|
|
603
|
+
return new MemoryScenarioPackRegistry();
|
|
604
|
+
}
|
|
605
|
+
export * from './evidence.js';
|
|
606
|
+
export * from './m4.js';
|
|
607
|
+
export * from './m5.js';
|
|
608
|
+
export * from './m6.js';
|