halfcode-compiler.xnl 0.2.1 → 0.2.3
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 +53 -0
- package/dist/application-assembly.d.ts +2 -2
- package/dist/application-assembly.js +1 -1
- package/dist/index-96qHPZj2.d.ts +126 -0
- package/dist/{index-FdimP_SL.d.ts → index-Cqh9pEBa.d.ts} +10 -1
- package/dist/{index-DKwcVwK6.d.ts → index-D2mpux6L.d.ts} +1 -1
- package/dist/index-DdRHFKSQ.d.ts +192 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +22 -3
- package/dist/kind-definition.d.ts +1 -1
- package/dist/resource-core-Byhqyadq.d.ts +20 -0
- package/dist/resource-core.d.ts +3 -2
- package/dist/resource-core.js +432 -2
- package/dist/resource-mapping.d.ts +14 -1
- package/dist/resource-mapping.js +2 -2
- package/dist/resource-projection.d.ts +1 -1
- package/dist/skill-capsule.d.ts +3 -2
- package/dist/skill-capsule.js +2 -2
- package/dist/src-25wZPUyX.js +305 -0
- package/dist/src-D1Bmq7Lo.js +1205 -0
- package/dist/src-DCRih45n.js +947 -0
- package/dist/{src-4m42imop.js → src-DRIws6tY.js} +43 -3
- package/dist/system-skills/resource-dsl/KindDefinitions/SkillCapsule/manifest.xnl +21 -0
- package/dist/system-skills/resource-dsl/KindDefinitions/WikiPage/manifest.xnl +10 -0
- package/dist/system-skills/resource-dsl/SkillCapsules/System/SKILL.metadata.yaml +2 -0
- package/dist/system-skills/resource-dsl/SkillCapsules/System/SKILL.template.ejs +20 -0
- package/dist/system-skills/resource-dsl/SkillCapsules/System/manifest.xnl +18 -0
- package/dist/system-skills/resource-dsl/SkillCapsules/System/resource-mappings.xnl +10 -0
- package/dist/system-skills/resource-dsl/WikiPages/documents/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/files/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/index/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/language/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/migration/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/projections/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/skill-capsules/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/documents.md +119 -0
- package/dist/system-skills/resource-dsl/examples/function.xnl +12 -0
- package/dist/system-skills/resource-dsl/examples/index.md +17 -0
- package/dist/system-skills/resource-dsl/examples/kind-definition.xnl +12 -0
- package/dist/system-skills/resource-dsl/examples/resource-mappings.xnl +21 -0
- package/dist/system-skills/resource-dsl/examples/resource-package.xnl +19 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-authoring.xnl +11 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-devops.xnl +12 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-kind-definition.xnl +27 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-resource-dsl.xnl +8 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-run.xnl +8 -0
- package/dist/system-skills/resource-dsl/files.md +47 -0
- package/dist/system-skills/resource-dsl/index.md +59 -0
- package/dist/system-skills/resource-dsl/language.md +64 -0
- package/dist/system-skills/resource-dsl/manifest.xnl +10 -0
- package/dist/system-skills/resource-dsl/migration.md +40 -0
- package/dist/system-skills/resource-dsl/projections.md +142 -0
- package/dist/system-skills/resource-dsl/skill-capsules.md +94 -0
- package/dist/system-skills/sys-halfcode-resource-dsl.plan.js +267 -0
- package/package.json +2 -2
- package/dist/index-DqYap2u_.d.ts +0 -81
- package/dist/index-wU-iKJnW.d.ts +0 -38
- package/dist/src-BlKlpg0J.js +0 -180
- package/dist/src-D8xlMdXx.js +0 -561
- package/dist/src-DKkIFGWH.js +0 -507
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { n as wordToString, t as parseXnl } from "./dist-Bkv7YeVi.js";
|
|
2
|
+
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
//#region ../resource-core/src/canonical.ts
|
|
6
|
+
function compareCodeUnits(left, right) {
|
|
7
|
+
if (left < right) return -1;
|
|
8
|
+
if (left > right) return 1;
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
function sha256Digest$1(value) {
|
|
12
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
13
|
+
}
|
|
14
|
+
function digestCanonical(value) {
|
|
15
|
+
return sha256Digest$1(canonicalJson(value));
|
|
16
|
+
}
|
|
17
|
+
function canonicalJson(value) {
|
|
18
|
+
return JSON.stringify(normalizeCanonical(value));
|
|
19
|
+
}
|
|
20
|
+
function normalizeCanonical(value) {
|
|
21
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
22
|
+
if (typeof value === "number") {
|
|
23
|
+
if (!Number.isFinite(value)) throw new TypeError("Canonical values must contain only finite numbers");
|
|
24
|
+
return Object.is(value, -0) ? 0 : value;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value)) return value.map(normalizeCanonical);
|
|
27
|
+
if (value instanceof Map) return [...value.entries()].map(([key, item]) => [String(key), normalizeCanonical(item)]).sort(([left], [right]) => compareCodeUnits(left, right));
|
|
28
|
+
if (typeof value === "object") {
|
|
29
|
+
const normalized = {};
|
|
30
|
+
for (const key of Object.keys(value).sort(compareCodeUnits)) {
|
|
31
|
+
const item = value[key];
|
|
32
|
+
if (item !== void 0) normalized[key] = normalizeCanonical(item);
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
throw new TypeError(`Unsupported canonical value type: ${typeof value}`);
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region ../resource-core/src/composition-error.ts
|
|
40
|
+
var ResourceCompositionError = class extends Error {
|
|
41
|
+
diagnostics;
|
|
42
|
+
constructor(diagnostics) {
|
|
43
|
+
super(`Resource composition failed with ${diagnostics.length} diagnostic(s)`);
|
|
44
|
+
this.name = "ResourceCompositionError";
|
|
45
|
+
this.diagnostics = Object.freeze([...diagnostics]);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region ../resource-core/src/effective-registry-brand.ts
|
|
50
|
+
const authenticEffectiveRegistries = /* @__PURE__ */ new WeakSet();
|
|
51
|
+
const effectiveRegistryLayers = /* @__PURE__ */ new WeakMap();
|
|
52
|
+
function markEffectiveRegistryAuthentic(registry, layers = []) {
|
|
53
|
+
authenticEffectiveRegistries.add(registry);
|
|
54
|
+
effectiveRegistryLayers.set(registry, Object.freeze(layers.map((layer) => Object.freeze({ ...layer }))));
|
|
55
|
+
return registry;
|
|
56
|
+
}
|
|
57
|
+
function isEffectiveRegistryAuthentic(registry) {
|
|
58
|
+
return authenticEffectiveRegistries.has(registry);
|
|
59
|
+
}
|
|
60
|
+
function effectiveRegistryLayerBindings(registry) {
|
|
61
|
+
return effectiveRegistryLayers.get(registry);
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region ../resource-core/src/dependency-snapshot.ts
|
|
65
|
+
function sha256Digest(value) {
|
|
66
|
+
return sha256Digest$1(value);
|
|
67
|
+
}
|
|
68
|
+
function createResourceContentIdentity(input) {
|
|
69
|
+
const diagnostics = [];
|
|
70
|
+
const identity = normalizeContentIdentity(input, diagnostics, {
|
|
71
|
+
requireContributions: false,
|
|
72
|
+
strictCanonicalInput: false,
|
|
73
|
+
validateClaimedDigest: false
|
|
74
|
+
});
|
|
75
|
+
if (diagnostics.length > 0 || !identity) throw new ResourceCompositionError(stableDiagnostics(diagnostics));
|
|
76
|
+
return identity;
|
|
77
|
+
}
|
|
78
|
+
function buildResourceDependencySnapshot(input) {
|
|
79
|
+
const diagnostics = [];
|
|
80
|
+
if (!isEffectiveRegistryAuthentic(input.registry) || input.registry.revision !== input.registry.compositionRevision) throw new ResourceCompositionError(Object.freeze([diagnostic("RESOURCE_EFFECTIVE_REGISTRY_UNTRUSTED", "effective-registry:revision", "Dependency snapshots require an authentic immutable registry returned by composeLayeredResourceRegistry().")]));
|
|
81
|
+
const roots = validateRoots(input.roots, input.registry, diagnostics);
|
|
82
|
+
const edges = validateEdges(input.edges, input.registry, diagnostics);
|
|
83
|
+
validateContentIdentityIndex(input.contentIdentities, diagnostics);
|
|
84
|
+
const validatedIdentities = validateEffectiveContentIdentities(input.registry, input.contentIdentities, diagnostics);
|
|
85
|
+
if (diagnostics.length > 0) throw new ResourceCompositionError(stableDiagnostics(diagnostics));
|
|
86
|
+
const registryRevision = digestCanonical({
|
|
87
|
+
compositionRevision: input.registry.compositionRevision,
|
|
88
|
+
resources: [...validatedIdentities.values()].sort((left, right) => compareCodeUnits(left.resourceId, right.resourceId)).map((identity) => ({
|
|
89
|
+
resourceId: identity.resourceId,
|
|
90
|
+
contentDigest: identity.contentDigest
|
|
91
|
+
}))
|
|
92
|
+
});
|
|
93
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const edge of edges) {
|
|
95
|
+
const values = adjacency.get(edge.fromResourceId) ?? [];
|
|
96
|
+
values.push(edge);
|
|
97
|
+
adjacency.set(edge.fromResourceId, values);
|
|
98
|
+
}
|
|
99
|
+
for (const values of adjacency.values()) values.sort(compareEdges);
|
|
100
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
101
|
+
const participating = /* @__PURE__ */ new Map();
|
|
102
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
103
|
+
const visited = /* @__PURE__ */ new Set();
|
|
104
|
+
const path = [];
|
|
105
|
+
const visit = (resourceId) => {
|
|
106
|
+
if (visiting.has(resourceId)) {
|
|
107
|
+
const cycleStart = path.indexOf(resourceId);
|
|
108
|
+
const cycle = [...path.slice(cycleStart), resourceId];
|
|
109
|
+
diagnostics.push(diagnostic("RESOURCE_DEPENDENCY_CYCLE", dependencyLocation(resourceId), `Resource dependency cycle detected: ${cycle.join(" -> ")}.`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (visited.has(resourceId)) return;
|
|
113
|
+
visiting.add(resourceId);
|
|
114
|
+
path.push(resourceId);
|
|
115
|
+
reachable.add(resourceId);
|
|
116
|
+
for (const edge of adjacency.get(resourceId) ?? []) {
|
|
117
|
+
participating.set(edgeKey(edge), edge);
|
|
118
|
+
visit(edge.toResourceId);
|
|
119
|
+
}
|
|
120
|
+
path.pop();
|
|
121
|
+
visiting.delete(resourceId);
|
|
122
|
+
visited.add(resourceId);
|
|
123
|
+
};
|
|
124
|
+
for (const root of roots) visit(root);
|
|
125
|
+
if (diagnostics.length > 0) throw new ResourceCompositionError(stableDiagnostics(diagnostics));
|
|
126
|
+
const closure = Object.freeze([...reachable].sort(compareCodeUnits).map((resourceId) => {
|
|
127
|
+
const entry = input.registry.byId.get(resourceId);
|
|
128
|
+
const identity = validatedIdentities.get(resourceId);
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
resourceId,
|
|
131
|
+
kind: entry.kind,
|
|
132
|
+
contentDigest: identity.contentDigest,
|
|
133
|
+
origin: cloneOrigin(entry.effectiveOrigin)
|
|
134
|
+
});
|
|
135
|
+
}));
|
|
136
|
+
const snapshotEdges = Object.freeze([...participating.values()].sort(compareEdges));
|
|
137
|
+
const frozenRoots = Object.freeze([...roots]);
|
|
138
|
+
const snapshotRevision = digestCanonical({
|
|
139
|
+
roots: frozenRoots,
|
|
140
|
+
closure: closure.map((resource) => ({
|
|
141
|
+
resourceId: resource.resourceId,
|
|
142
|
+
kind: resource.kind,
|
|
143
|
+
contentDigest: resource.contentDigest,
|
|
144
|
+
origin: stableOriginFact(resource.origin)
|
|
145
|
+
})),
|
|
146
|
+
edges: snapshotEdges,
|
|
147
|
+
registryRevision
|
|
148
|
+
});
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
roots: frozenRoots,
|
|
151
|
+
closure,
|
|
152
|
+
edges: snapshotEdges,
|
|
153
|
+
registryRevision,
|
|
154
|
+
snapshotRevision
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function validateRoots(inputRoots, registry, diagnostics) {
|
|
158
|
+
const roots = [];
|
|
159
|
+
const seen = /* @__PURE__ */ new Set();
|
|
160
|
+
for (const value of inputRoots) {
|
|
161
|
+
const resourceId = value.trim();
|
|
162
|
+
if (resourceId.length === 0) {
|
|
163
|
+
diagnostics.push(diagnostic("RESOURCE_DEPENDENCY_ROOT_EMPTY", "dependency-root:unnamed", "Dependency snapshot roots must be non-empty resource ids."));
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (seen.has(resourceId)) {
|
|
167
|
+
diagnostics.push(diagnostic("RESOURCE_DEPENDENCY_ROOT_DUPLICATE", dependencyLocation(resourceId), `Dependency snapshot root '${resourceId}' is declared more than once.`));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
seen.add(resourceId);
|
|
171
|
+
roots.push(resourceId);
|
|
172
|
+
const entry = registry.byId.get(resourceId);
|
|
173
|
+
if (!entry || !entry.resource) diagnostics.push(diagnostic("RESOURCE_DEPENDENCY_ROOT_MISSING", dependencyLocation(resourceId), `Dependency snapshot root '${resourceId}' is not an effective resource.`));
|
|
174
|
+
}
|
|
175
|
+
if (roots.length === 0 && diagnostics.length === 0) diagnostics.push(diagnostic("RESOURCE_DEPENDENCY_ROOTS_EMPTY", "dependency-root:none", "Dependency snapshot requires at least one explicit root."));
|
|
176
|
+
return roots;
|
|
177
|
+
}
|
|
178
|
+
function validateEdges(inputEdges, registry, diagnostics) {
|
|
179
|
+
const values = /* @__PURE__ */ new Map();
|
|
180
|
+
inputEdges.forEach((inputEdge, index) => {
|
|
181
|
+
const edge = Object.freeze({
|
|
182
|
+
fromResourceId: inputEdge.fromResourceId.trim(),
|
|
183
|
+
toResourceId: inputEdge.toResourceId.trim(),
|
|
184
|
+
relation: inputEdge.relation.trim(),
|
|
185
|
+
declaredBy: inputEdge.declaredBy.trim()
|
|
186
|
+
});
|
|
187
|
+
if (!edge.fromResourceId || !edge.toResourceId || !edge.relation || !edge.declaredBy) {
|
|
188
|
+
diagnostics.push(diagnostic("RESOURCE_DEPENDENCY_EDGE_INVALID", `dependency-edge:${index}`, "Dependency edges require non-empty fromResourceId, toResourceId, relation, and declaredBy fields."));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const source = registry.byId.get(edge.fromResourceId);
|
|
192
|
+
const target = registry.byId.get(edge.toResourceId);
|
|
193
|
+
if (!source?.resource) diagnostics.push(diagnostic("RESOURCE_DEPENDENCY_SOURCE_MISSING", dependencyLocation(edge.fromResourceId), `Dependency edge source '${edge.fromResourceId}' is not an effective resource.`));
|
|
194
|
+
if (!target?.resource) diagnostics.push(diagnostic("RESOURCE_DEPENDENCY_TARGET_MISSING", dependencyLocation(edge.toResourceId), `Dependency edge target '${edge.toResourceId}' is not an effective resource.`));
|
|
195
|
+
values.set(edgeKey(edge), edge);
|
|
196
|
+
});
|
|
197
|
+
return [...values.values()].sort(compareEdges);
|
|
198
|
+
}
|
|
199
|
+
function normalizeContentIdentity(input, diagnostics, options) {
|
|
200
|
+
const initialDiagnosticCount = diagnostics.length;
|
|
201
|
+
if (!isRecord(input)) {
|
|
202
|
+
diagnostics.push(diagnostic("RESOURCE_CONTENT_IDENTITY_INVALID", contentLocation(""), "Resource content identity must be an object fact."));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const rawResourceId = input.resourceId;
|
|
206
|
+
const resourceId = typeof rawResourceId === "string" ? rawResourceId.trim() : "";
|
|
207
|
+
if (resourceId.length === 0) diagnostics.push(diagnostic("RESOURCE_CONTENT_IDENTITY_ID_EMPTY", contentLocation(""), "Resource content identity requires a non-empty resource id."));
|
|
208
|
+
else if (options.strictCanonicalInput && rawResourceId !== resourceId) diagnostics.push(diagnostic("RESOURCE_CONTENT_IDENTITY_NON_CANONICAL", contentLocation(resourceId), `Resource content identity '${resourceId}' must use its canonical id without surrounding whitespace.`));
|
|
209
|
+
const authorityDigest = typeof input.authorityDigest === "string" ? input.authorityDigest : "";
|
|
210
|
+
if (!isSha256Digest(authorityDigest)) diagnostics.push(diagnostic("RESOURCE_CONTENT_AUTHORITY_DIGEST_INVALID", contentLocation(resourceId), `Resource '${resourceId || "unnamed"}' has an invalid authority digest.`));
|
|
211
|
+
const rawContributions = input.contributions;
|
|
212
|
+
let contributionFacts = [];
|
|
213
|
+
if (rawContributions === void 0 && !options.requireContributions) contributionFacts = [];
|
|
214
|
+
else if (!Array.isArray(rawContributions)) diagnostics.push(diagnostic("RESOURCE_DIGEST_CONTRIBUTIONS_INVALID", contentLocation(resourceId), `Resource '${resourceId || "unnamed"}' must provide an explicit digest contribution array.`));
|
|
215
|
+
else contributionFacts = rawContributions;
|
|
216
|
+
const contributions = /* @__PURE__ */ new Map();
|
|
217
|
+
for (const fact of contributionFacts) {
|
|
218
|
+
if (!isRecord(fact)) {
|
|
219
|
+
diagnostics.push(diagnostic("RESOURCE_DIGEST_CONTRIBUTION_INVALID", contentLocation(resourceId), `Resource '${resourceId || "unnamed"}' has a non-object digest contribution.`));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const rawKey = fact.key;
|
|
223
|
+
const key = typeof rawKey === "string" ? rawKey.trim() : "";
|
|
224
|
+
if (key.length === 0) {
|
|
225
|
+
diagnostics.push(diagnostic("RESOURCE_DIGEST_CONTRIBUTION_KEY_EMPTY", contentLocation(resourceId), `Resource '${resourceId || "unnamed"}' has a digest contribution with an empty key.`));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (options.strictCanonicalInput && rawKey !== key) diagnostics.push(diagnostic("RESOURCE_DIGEST_CONTRIBUTION_NON_CANONICAL", contentLocation(resourceId, key), `Digest contribution '${key}' for resource '${resourceId}' must use its canonical key.`));
|
|
229
|
+
const digest = typeof fact.digest === "string" ? fact.digest : "";
|
|
230
|
+
if (!isSha256Digest(digest)) {
|
|
231
|
+
diagnostics.push(diagnostic("RESOURCE_DIGEST_CONTRIBUTION_INVALID", contentLocation(resourceId, key), `Digest contribution '${key}' for resource '${resourceId || "unnamed"}' is invalid.`));
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const sourceUri = fact.sourceUri;
|
|
235
|
+
if (sourceUri !== void 0 && typeof sourceUri !== "string") {
|
|
236
|
+
diagnostics.push(diagnostic("RESOURCE_DIGEST_CONTRIBUTION_SOURCE_INVALID", contentLocation(resourceId, key), `Digest contribution '${key}' for resource '${resourceId}' has an invalid source URI.`));
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const contribution = Object.freeze({
|
|
240
|
+
key,
|
|
241
|
+
digest,
|
|
242
|
+
...sourceUri === void 0 ? {} : { sourceUri }
|
|
243
|
+
});
|
|
244
|
+
const current = contributions.get(key);
|
|
245
|
+
if (current) {
|
|
246
|
+
const conflicting = current.digest !== digest;
|
|
247
|
+
diagnostics.push(diagnostic(conflicting ? "RESOURCE_DIGEST_CONTRIBUTION_CONFLICT" : "RESOURCE_DIGEST_CONTRIBUTION_DUPLICATE", contentLocation(resourceId, key), conflicting ? `Digest contribution '${key}' for resource '${resourceId || "unnamed"}' has conflicting digests.` : `Digest contribution '${key}' for resource '${resourceId || "unnamed"}' is declared more than once.`));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
contributions.set(key, contribution);
|
|
251
|
+
}
|
|
252
|
+
if (diagnostics.length > initialDiagnosticCount) return void 0;
|
|
253
|
+
const ordered = Object.freeze([...contributions.values()].sort(compareContributions));
|
|
254
|
+
const contentDigest = digestCanonical({
|
|
255
|
+
resourceId,
|
|
256
|
+
authorityDigest,
|
|
257
|
+
contributions: ordered.map((contribution) => ({
|
|
258
|
+
key: contribution.key,
|
|
259
|
+
digest: contribution.digest
|
|
260
|
+
}))
|
|
261
|
+
});
|
|
262
|
+
if (options.validateClaimedDigest) {
|
|
263
|
+
const claimedDigest = input.contentDigest;
|
|
264
|
+
if (typeof claimedDigest !== "string" || !isSha256Digest(claimedDigest)) {
|
|
265
|
+
diagnostics.push(diagnostic("RESOURCE_CONTENT_DIGEST_INVALID", contentLocation(resourceId), `Resource '${resourceId}' has an invalid claimed content digest.`));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (claimedDigest !== contentDigest) {
|
|
269
|
+
diagnostics.push(diagnostic("RESOURCE_CONTENT_DIGEST_MISMATCH", contentLocation(resourceId), `Resource '${resourceId}' claimed a content digest that does not match its explicit authority and contributions.`));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return Object.freeze({
|
|
274
|
+
resourceId,
|
|
275
|
+
authorityDigest,
|
|
276
|
+
contributions: ordered,
|
|
277
|
+
contentDigest
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function validateEffectiveContentIdentities(registry, identities, diagnostics) {
|
|
281
|
+
const validated = /* @__PURE__ */ new Map();
|
|
282
|
+
const effectiveEntries = [...registry.byId.values()].filter((entry) => entry.resource !== void 0).sort((left, right) => compareCodeUnits(left.resourceId, right.resourceId));
|
|
283
|
+
for (const entry of effectiveEntries) {
|
|
284
|
+
const input = identities.get(entry.resourceId);
|
|
285
|
+
if (input === void 0) {
|
|
286
|
+
diagnostics.push(diagnostic("RESOURCE_CONTENT_IDENTITY_MISSING", dependencyLocation(entry.resourceId), `Effective resource '${entry.resourceId}' has no explicit content identity.`));
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const identity = normalizeContentIdentity(input, diagnostics, {
|
|
290
|
+
requireContributions: true,
|
|
291
|
+
strictCanonicalInput: true,
|
|
292
|
+
validateClaimedDigest: true
|
|
293
|
+
});
|
|
294
|
+
if (identity) validated.set(entry.resourceId, identity);
|
|
295
|
+
}
|
|
296
|
+
return validated;
|
|
297
|
+
}
|
|
298
|
+
function validateContentIdentityIndex(identities, diagnostics) {
|
|
299
|
+
for (const [key, identity] of [...identities.entries()].sort(([left], [right]) => compareCodeUnits(left, right))) {
|
|
300
|
+
const identityResourceId = isRecord(identity) && typeof identity.resourceId === "string" ? identity.resourceId : void 0;
|
|
301
|
+
if (key !== identityResourceId) diagnostics.push(diagnostic("RESOURCE_CONTENT_IDENTITY_KEY_MISMATCH", dependencyLocation(key), `Content identity index key '${key}' does not match identity '${identityResourceId ?? "invalid"}'.`));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function isSha256Digest(value) {
|
|
305
|
+
return /^sha256:[0-9a-f]{64}$/.test(value);
|
|
306
|
+
}
|
|
307
|
+
function isRecord(value) {
|
|
308
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
309
|
+
}
|
|
310
|
+
function compareEdges(left, right) {
|
|
311
|
+
return compareCodeUnits(left.fromResourceId, right.fromResourceId) || compareCodeUnits(left.toResourceId, right.toResourceId) || compareCodeUnits(left.relation, right.relation) || compareCodeUnits(left.declaredBy, right.declaredBy);
|
|
312
|
+
}
|
|
313
|
+
function compareContributions(left, right) {
|
|
314
|
+
const semanticOrder = compareCodeUnits(left.key, right.key) || compareCodeUnits(left.digest, right.digest);
|
|
315
|
+
if (semanticOrder !== 0) return semanticOrder;
|
|
316
|
+
if (left.sourceUri === right.sourceUri) return 0;
|
|
317
|
+
if (left.sourceUri === void 0) return 1;
|
|
318
|
+
if (right.sourceUri === void 0) return -1;
|
|
319
|
+
return compareCodeUnits(left.sourceUri, right.sourceUri);
|
|
320
|
+
}
|
|
321
|
+
function edgeKey(edge) {
|
|
322
|
+
return JSON.stringify([
|
|
323
|
+
edge.fromResourceId,
|
|
324
|
+
edge.toResourceId,
|
|
325
|
+
edge.relation,
|
|
326
|
+
edge.declaredBy
|
|
327
|
+
]);
|
|
328
|
+
}
|
|
329
|
+
function stableOriginFact(origin) {
|
|
330
|
+
return {
|
|
331
|
+
layerId: origin.layerId,
|
|
332
|
+
layerIndex: origin.layerIndex,
|
|
333
|
+
packageId: origin.packageId,
|
|
334
|
+
documentUri: origin.documentUri,
|
|
335
|
+
logicalPath: origin.logicalPath
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function cloneOrigin(origin) {
|
|
339
|
+
return Object.freeze({
|
|
340
|
+
layerId: origin.layerId,
|
|
341
|
+
layerIndex: origin.layerIndex,
|
|
342
|
+
packageId: origin.packageId,
|
|
343
|
+
documentUri: origin.documentUri,
|
|
344
|
+
...origin.logicalPath === void 0 ? {} : { logicalPath: origin.logicalPath }
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function stableDiagnostics(diagnostics) {
|
|
348
|
+
return Object.freeze([...diagnostics].sort((left, right) => compareCodeUnits(left.location, right.location) || compareCodeUnits(left.code, right.code)));
|
|
349
|
+
}
|
|
350
|
+
function diagnostic(code, location, message) {
|
|
351
|
+
return Object.freeze({
|
|
352
|
+
code,
|
|
353
|
+
location,
|
|
354
|
+
message
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function dependencyLocation(resourceId) {
|
|
358
|
+
return `dependency:${resourceId || "unnamed"}`;
|
|
359
|
+
}
|
|
360
|
+
function contentLocation(resourceId, key) {
|
|
361
|
+
return `content:${resourceId || "unnamed"}${key === void 0 ? "" : `:${key}`}`;
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region ../resource-core/src/loaded-resource-tree-brand.ts
|
|
365
|
+
const authenticLoadedResourceTrees = /* @__PURE__ */ new WeakSet();
|
|
366
|
+
function markLoadedResourceTreeAuthentic(tree) {
|
|
367
|
+
authenticLoadedResourceTrees.add(tree);
|
|
368
|
+
return tree;
|
|
369
|
+
}
|
|
370
|
+
function isLoadedResourceTreeAuthentic(tree) {
|
|
371
|
+
return authenticLoadedResourceTrees.has(tree);
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region ../resource-core/src/readonly-map.ts
|
|
375
|
+
function readonlyMap(entries) {
|
|
376
|
+
const values = new Map(entries);
|
|
377
|
+
return Object.freeze({
|
|
378
|
+
get size() {
|
|
379
|
+
return values.size;
|
|
380
|
+
},
|
|
381
|
+
get(key) {
|
|
382
|
+
return values.get(key);
|
|
383
|
+
},
|
|
384
|
+
has(key) {
|
|
385
|
+
return values.has(key);
|
|
386
|
+
},
|
|
387
|
+
entries() {
|
|
388
|
+
return values.entries();
|
|
389
|
+
},
|
|
390
|
+
keys() {
|
|
391
|
+
return values.keys();
|
|
392
|
+
},
|
|
393
|
+
values() {
|
|
394
|
+
return values.values();
|
|
395
|
+
},
|
|
396
|
+
forEach(callback, thisArg) {
|
|
397
|
+
const view = this;
|
|
398
|
+
values.forEach((value, key) => callback.call(thisArg, value, key, view));
|
|
399
|
+
},
|
|
400
|
+
[Symbol.iterator]() {
|
|
401
|
+
return values[Symbol.iterator]();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region ../resource-core/src/xnl-loader.ts
|
|
407
|
+
async function buildXnlResourceTree(options) {
|
|
408
|
+
const requestedRoot = resolve(options.rootDir);
|
|
409
|
+
const context = {
|
|
410
|
+
rootDir: await realpath(requestedRoot).catch(() => requestedRoot),
|
|
411
|
+
diagnostics: [],
|
|
412
|
+
kindDefinitions: /* @__PURE__ */ new Map(),
|
|
413
|
+
resources: [],
|
|
414
|
+
seenIdentities: /* @__PURE__ */ new Map(),
|
|
415
|
+
authorityFiles: /* @__PURE__ */ new Map(),
|
|
416
|
+
recordAuthorityDigests: /* @__PURE__ */ new WeakMap()
|
|
417
|
+
};
|
|
418
|
+
const manifest = await loadManifest(resolve(context.rootDir, options.manifestPath), "manifest", context, true);
|
|
419
|
+
if (!manifest || context.diagnostics.length > 0) return { diagnostics: context.diagnostics };
|
|
420
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
421
|
+
for (const resource of context.resources) {
|
|
422
|
+
const records = byKind.get(resource.kind) ?? [];
|
|
423
|
+
records.push(resource);
|
|
424
|
+
byKind.set(resource.kind, records);
|
|
425
|
+
}
|
|
426
|
+
const frozenByKind = readonlyMap([...byKind.entries()].sort(([left], [right]) => compareCodeUnits(left, right)).map(([kind, records]) => [kind, Object.freeze([...records].sort((left, right) => compareCodeUnits(left.resourceId, right.resourceId)))]));
|
|
427
|
+
const frozenKindDefinitions = readonlyMap([...context.kindDefinitions.entries()].sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
428
|
+
const contentIdentities = readonlyMap(context.resources.map((resource) => [resource.resourceId, loadedContentIdentity(resource, context)]).sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
429
|
+
return {
|
|
430
|
+
diagnostics: [],
|
|
431
|
+
tree: markLoadedResourceTreeAuthentic(Object.freeze({
|
|
432
|
+
manifest,
|
|
433
|
+
registry: Object.freeze({
|
|
434
|
+
byKind: frozenByKind,
|
|
435
|
+
kindDefinitions: frozenKindDefinitions
|
|
436
|
+
}),
|
|
437
|
+
diagnostics: Object.freeze([]),
|
|
438
|
+
contentIdentities
|
|
439
|
+
}))
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
async function loadManifest(filePath, sourceShape, context, isRoot) {
|
|
443
|
+
const record = await loadXnlRecord(filePath, sourceShape, context);
|
|
444
|
+
if (!record) return void 0;
|
|
445
|
+
if (isRoot && record.kind !== "ResourcePackage") {
|
|
446
|
+
context.diagnostics.push({
|
|
447
|
+
code: "RESOURCE_XNL_ROOT_INVALID",
|
|
448
|
+
location: record.documentUri,
|
|
449
|
+
message: "The root manifest.xnl must contain one ResourcePackage root."
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const catalogs = readCatalogs(record, context.diagnostics);
|
|
454
|
+
for (const catalog of catalogs.filter((item) => item.kind === "KindDefinition")) await loadKindDefinitions(catalog, dirname(filePath), context);
|
|
455
|
+
for (const catalog of catalogs.filter((item) => item.kind !== "KindDefinition")) await loadCatalog(catalog, dirname(filePath), context);
|
|
456
|
+
if (!isRoot) {
|
|
457
|
+
validateIdentity(record, context);
|
|
458
|
+
context.resources.push(record);
|
|
459
|
+
}
|
|
460
|
+
return record;
|
|
461
|
+
}
|
|
462
|
+
async function loadKindDefinitions(catalog, manifestDir, context) {
|
|
463
|
+
const files = await catalogFiles(catalog, manifestDir, context);
|
|
464
|
+
for (const file of files) {
|
|
465
|
+
const record = await loadXnlRecord(file, catalog.shape, context);
|
|
466
|
+
if (!record) continue;
|
|
467
|
+
if (record.kind !== "KindDefinition") {
|
|
468
|
+
context.diagnostics.push(kindMismatch(record.kind, "KindDefinition", record.documentUri));
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const definition = normalizeKindDefinition(record, context.diagnostics);
|
|
472
|
+
if (!definition) continue;
|
|
473
|
+
context.kindDefinitions.set(definition.resourceKind, definition);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function loadCatalog(catalog, manifestDir, context) {
|
|
477
|
+
const definition = context.kindDefinitions.get(catalog.kind);
|
|
478
|
+
if (!definition) {
|
|
479
|
+
context.diagnostics.push({
|
|
480
|
+
code: "KIND_DEFINITION_MISSING",
|
|
481
|
+
location: catalog.location,
|
|
482
|
+
message: `No KindDefinition is registered for catalog kind '${catalog.kind}'.`
|
|
483
|
+
});
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (!definition.sourceShapes.includes(catalog.shape)) {
|
|
487
|
+
context.diagnostics.push({
|
|
488
|
+
code: "RESOURCE_SOURCE_SHAPE_NOT_ALLOWED",
|
|
489
|
+
location: catalog.location,
|
|
490
|
+
message: `Kind '${catalog.kind}' does not allow source shape '${catalog.shape}'.`
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const files = await catalogFiles(catalog, manifestDir, context);
|
|
495
|
+
for (const file of files) {
|
|
496
|
+
if (catalog.shape === "manifest") {
|
|
497
|
+
const record = await loadXnlRecord(file, "manifest", context);
|
|
498
|
+
if (!record) continue;
|
|
499
|
+
if (record.kind !== catalog.kind) {
|
|
500
|
+
context.diagnostics.push(kindMismatch(record.kind, catalog.kind, record.documentUri));
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
validateResourceApiVersion(record, definition, context.diagnostics);
|
|
504
|
+
await validateRequiredFiles(dirname(file), record, definition, context.diagnostics);
|
|
505
|
+
validateIdentity(record, context);
|
|
506
|
+
context.resources.push(record);
|
|
507
|
+
const catalogs = readCatalogs(record, context.diagnostics);
|
|
508
|
+
for (const nested of catalogs.filter((item) => item.kind !== "KindDefinition")) await loadCatalog(nested, dirname(file), context);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (definition.documentCardinality === "many" && catalog.shape !== "single-file") {
|
|
512
|
+
context.diagnostics.push({
|
|
513
|
+
code: "RESOURCE_DOCUMENT_CARDINALITY_UNSUPPORTED",
|
|
514
|
+
location: catalog.location,
|
|
515
|
+
message: `Kind '${catalog.kind}' allows multiple document roots only in a single-file catalog.`
|
|
516
|
+
});
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const records = await loadXnlRecords(file, catalog.shape, context, definition.documentCardinality);
|
|
520
|
+
for (const record of records) {
|
|
521
|
+
if (record.kind !== catalog.kind) {
|
|
522
|
+
context.diagnostics.push(kindMismatch(record.kind, catalog.kind, record.documentUri));
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
validateResourceApiVersion(record, definition, context.diagnostics);
|
|
526
|
+
await validateRequiredFiles(dirname(file), record, definition, context.diagnostics);
|
|
527
|
+
validateIdentity(record, context);
|
|
528
|
+
context.resources.push(record);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async function catalogFiles(catalog, manifestDir, context) {
|
|
533
|
+
const root = await resolveCatalogDirectory(catalog.root, manifestDir, catalog.location, context);
|
|
534
|
+
if (!root) return [];
|
|
535
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => void 0);
|
|
536
|
+
if (!entries) {
|
|
537
|
+
context.diagnostics.push({
|
|
538
|
+
code: "RESOURCE_CATALOG_ROOT_MISSING",
|
|
539
|
+
location: catalog.location,
|
|
540
|
+
message: "Catalog root does not exist or is not readable."
|
|
541
|
+
});
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
if (catalog.shape === "single-file") {
|
|
545
|
+
if (catalog.entry) {
|
|
546
|
+
if (!isPlainEntry(catalog.entry) || !catalog.entry.toLowerCase().endsWith(".xnl")) {
|
|
547
|
+
context.diagnostics.push({
|
|
548
|
+
code: "RESOURCE_CATALOG_ENTRY_INVALID",
|
|
549
|
+
location: catalog.location,
|
|
550
|
+
message: "A single-file catalog entry must be a plain XNL filename."
|
|
551
|
+
});
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
return [join(root, catalog.entry)];
|
|
555
|
+
}
|
|
556
|
+
return entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".xnl")).sort((left, right) => compareCodeUnits(left.name, right.name)).map((entry) => join(root, entry.name));
|
|
557
|
+
}
|
|
558
|
+
if (!catalog.entry || !isPlainEntry(catalog.entry)) {
|
|
559
|
+
context.diagnostics.push({
|
|
560
|
+
code: "RESOURCE_CATALOG_ENTRY_INVALID",
|
|
561
|
+
location: catalog.location,
|
|
562
|
+
message: "Directory and manifest catalogs require a plain XNL entry filename."
|
|
563
|
+
});
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
return entries.filter((entry) => entry.isDirectory()).sort((left, right) => compareCodeUnits(left.name, right.name)).map((entry) => join(root, entry.name, catalog.entry));
|
|
567
|
+
}
|
|
568
|
+
async function loadXnlRecord(filePath, sourceShape, context) {
|
|
569
|
+
return (await loadXnlRecords(filePath, sourceShape, context, "one"))[0];
|
|
570
|
+
}
|
|
571
|
+
async function loadXnlRecords(filePath, sourceShape, context, documentCardinality) {
|
|
572
|
+
const documentUri = documentUriFor(context.rootDir, filePath);
|
|
573
|
+
const authority = await readAuthorityFile(filePath, documentUri, context);
|
|
574
|
+
if (!authority) return [];
|
|
575
|
+
let roots;
|
|
576
|
+
try {
|
|
577
|
+
const parsed = parseXnl(authority.source, { textBlockStyle: true });
|
|
578
|
+
if (parsed.warnings?.length) {
|
|
579
|
+
context.diagnostics.push({
|
|
580
|
+
code: "RESOURCE_XNL_WARNING",
|
|
581
|
+
location: documentUri,
|
|
582
|
+
message: parsed.warnings.map((warning) => warning.message).join("; ")
|
|
583
|
+
});
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
if (parsed.nodes.length === 0 || parsed.nodes.some((node) => !isDataElement(node)) || documentCardinality === "one" && parsed.nodes.length !== 1) {
|
|
587
|
+
context.diagnostics.push({
|
|
588
|
+
code: "RESOURCE_XNL_ROOT_INVALID",
|
|
589
|
+
location: documentUri,
|
|
590
|
+
message: documentCardinality === "one" ? "XNL resource documents must contain exactly one data-element root." : "XNL resource forests must contain one or more data-element roots."
|
|
591
|
+
});
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
roots = parsed.nodes;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
context.diagnostics.push({
|
|
597
|
+
code: "RESOURCE_XNL_SYNTAX",
|
|
598
|
+
location: documentUri,
|
|
599
|
+
message: `Invalid XNL resource document: ${error instanceof Error ? error.message : String(error)}`
|
|
600
|
+
});
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
return roots.flatMap((root) => {
|
|
604
|
+
const record = resourceRecordFromRoot(root, filePath, sourceShape, context);
|
|
605
|
+
if (record) context.recordAuthorityDigests.set(record, authority.authorityDigest);
|
|
606
|
+
return record ? [record] : [];
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
async function readAuthorityFile(filePath, documentUri, context) {
|
|
610
|
+
const canonicalPath = resolve(filePath);
|
|
611
|
+
let pending = context.authorityFiles.get(canonicalPath);
|
|
612
|
+
if (!pending) {
|
|
613
|
+
pending = readFile(canonicalPath).then((bytes) => {
|
|
614
|
+
try {
|
|
615
|
+
return Object.freeze({
|
|
616
|
+
source: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
617
|
+
authorityDigest: sha256Digest(bytes)
|
|
618
|
+
});
|
|
619
|
+
} catch {
|
|
620
|
+
context.diagnostics.push({
|
|
621
|
+
code: "RESOURCE_XNL_UTF8_INVALID",
|
|
622
|
+
location: documentUri,
|
|
623
|
+
message: "XNL resource authority must be valid UTF-8."
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}).catch(() => {
|
|
628
|
+
context.diagnostics.push({
|
|
629
|
+
code: "RESOURCE_FILE_MISSING",
|
|
630
|
+
location: documentUri,
|
|
631
|
+
message: "Resource file does not exist."
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
context.authorityFiles.set(canonicalPath, pending);
|
|
635
|
+
}
|
|
636
|
+
return pending;
|
|
637
|
+
}
|
|
638
|
+
function loadedContentIdentity(resource, context) {
|
|
639
|
+
const authorityDigest = context.recordAuthorityDigests.get(resource);
|
|
640
|
+
if (!authorityDigest) throw new Error(`Loader authority digest is missing for ${resource.resourceId}`);
|
|
641
|
+
return createResourceContentIdentity({
|
|
642
|
+
resourceId: resource.resourceId,
|
|
643
|
+
authorityDigest
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
function resourceRecordFromRoot(root, filePath, sourceShape, context) {
|
|
647
|
+
const documentUri = documentUriFor(context.rootDir, filePath);
|
|
648
|
+
const node = normalizeNode(root);
|
|
649
|
+
const resourceId = node.resourceId;
|
|
650
|
+
const apiVersion = asString(node.metadata.apiVersion);
|
|
651
|
+
const lifecycle = asString(node.properties.lifecycle);
|
|
652
|
+
const description = asString(node.properties.description) ?? node.subdomains.Description?.text;
|
|
653
|
+
if (!resourceId) context.diagnostics.push({
|
|
654
|
+
code: "RESOURCE_IDENTITY_MISSING",
|
|
655
|
+
location: documentUri,
|
|
656
|
+
message: "XNL resource roots must declare a #id."
|
|
657
|
+
});
|
|
658
|
+
if (!apiVersion) context.diagnostics.push({
|
|
659
|
+
code: "RESOURCE_METADATA_FIELD_MISSING",
|
|
660
|
+
location: documentUri,
|
|
661
|
+
message: "XNL resources require metadata apiVersion."
|
|
662
|
+
});
|
|
663
|
+
if (!resourceId || !apiVersion) return void 0;
|
|
664
|
+
const metadata = Object.freeze({
|
|
665
|
+
apiVersion,
|
|
666
|
+
...lifecycle ? { lifecycle } : {},
|
|
667
|
+
version: asString(node.metadata.version)
|
|
668
|
+
});
|
|
669
|
+
const logicalPath = relative(context.rootDir, filePath).split(sep).join("/");
|
|
670
|
+
return Object.freeze({
|
|
671
|
+
kind: node.tag,
|
|
672
|
+
resourceId,
|
|
673
|
+
fqn: resourceId,
|
|
674
|
+
...description ? { description } : {},
|
|
675
|
+
metadata,
|
|
676
|
+
sourceShape,
|
|
677
|
+
logicalPath,
|
|
678
|
+
documentUri,
|
|
679
|
+
format: "xnl",
|
|
680
|
+
node
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
function normalizeNode(node) {
|
|
684
|
+
const subdomains = {};
|
|
685
|
+
if (node.kind === "DataElement" && node.extend) for (const name of node.extend.order) {
|
|
686
|
+
const child = node.extend.children[name];
|
|
687
|
+
if (child) subdomains[name] = normalizeNode(child);
|
|
688
|
+
}
|
|
689
|
+
const resourceId = wordToString(node.id);
|
|
690
|
+
return Object.freeze({
|
|
691
|
+
tag: node.tag,
|
|
692
|
+
...resourceId === void 0 ? {} : { resourceId },
|
|
693
|
+
metadata: Object.freeze(normalizeMap(node.metadata)),
|
|
694
|
+
properties: Object.freeze(normalizeMap(node.attributes)),
|
|
695
|
+
body: Object.freeze(node.kind === "DataElement" ? (node.body ?? []).map(normalizeValue) : []),
|
|
696
|
+
subdomains: Object.freeze(subdomains),
|
|
697
|
+
...node.kind === "TextElement" ? { text: node.text ?? "" } : {}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function normalizeMap(map) {
|
|
701
|
+
return Object.fromEntries(Object.entries(map ?? {}).map(([key, value]) => [key, normalizeValue(value)]));
|
|
702
|
+
}
|
|
703
|
+
function normalizeValue(value) {
|
|
704
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
705
|
+
if (Array.isArray(value)) return Object.freeze(value.map(normalizeValue));
|
|
706
|
+
if (isElement(value)) return normalizeNode(value);
|
|
707
|
+
if (isWord(value)) return wordToString(value) ?? "";
|
|
708
|
+
if (isComment(value)) return value.value;
|
|
709
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child]) => [key, normalizeValue(child)])));
|
|
710
|
+
}
|
|
711
|
+
function readCatalogs(record, diagnostics) {
|
|
712
|
+
const catalogs = record.node.subdomains.Catalogs;
|
|
713
|
+
if (!catalogs) return [];
|
|
714
|
+
const out = [];
|
|
715
|
+
for (const value of catalogs.body) {
|
|
716
|
+
if (!isResourceNode(value) || value.tag !== "Catalog") continue;
|
|
717
|
+
const id = value.resourceId;
|
|
718
|
+
const kind = asString(value.properties.kind);
|
|
719
|
+
const root = asString(value.properties.root);
|
|
720
|
+
const entry = asString(value.properties.entry);
|
|
721
|
+
const shapeValue = asString(value.properties.shape) ?? (entry ? "directory" : "single-file");
|
|
722
|
+
const location = `${record.documentUri}#Catalog:${id ?? "unknown"}`;
|
|
723
|
+
if (!id || !kind || !root || !isXnlShape(shapeValue)) {
|
|
724
|
+
diagnostics.push({
|
|
725
|
+
code: "RESOURCE_CATALOG_INVALID",
|
|
726
|
+
location,
|
|
727
|
+
message: "Catalog requires #id, kind, root and a valid shape."
|
|
728
|
+
});
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
out.push({
|
|
732
|
+
id,
|
|
733
|
+
kind,
|
|
734
|
+
root,
|
|
735
|
+
entry,
|
|
736
|
+
shape: shapeValue,
|
|
737
|
+
location
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
function normalizeKindDefinition(record, diagnostics) {
|
|
743
|
+
const resourceKind = asString(record.node.properties.resourceKind) ?? record.resourceId.split(".").at(-1);
|
|
744
|
+
const shapes = record.node.properties.sourceShapes;
|
|
745
|
+
const sourceShapes = Array.isArray(shapes) ? shapes.filter(isXnlShape) : [];
|
|
746
|
+
const currentApiVersion = asString(record.node.properties.currentApiVersion);
|
|
747
|
+
const versions = record.node.properties.supportedApiVersions;
|
|
748
|
+
const supportedApiVersions = Array.isArray(versions) ? versions.filter((value) => typeof value === "string" && Boolean(value.trim())) : [];
|
|
749
|
+
const cardinalityValue = asString(record.node.properties.documentCardinality) ?? "one";
|
|
750
|
+
if (!resourceKind || sourceShapes.length === 0 || !currentApiVersion || supportedApiVersions.length === 0 || !isDocumentCardinality(cardinalityValue)) {
|
|
751
|
+
diagnostics.push({
|
|
752
|
+
code: "KIND_DEFINITION_INVALID",
|
|
753
|
+
location: record.documentUri,
|
|
754
|
+
message: "KindDefinition requires resourceKind (or an id suffix), sourceShapes, currentApiVersion, and supportedApiVersions."
|
|
755
|
+
});
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (!supportedApiVersions.includes(currentApiVersion)) {
|
|
759
|
+
diagnostics.push({
|
|
760
|
+
code: "KIND_DEFINITION_VERSION_INVALID",
|
|
761
|
+
location: record.documentUri,
|
|
762
|
+
message: `Kind '${resourceKind}' currentApiVersion '${currentApiVersion}' must be included in supportedApiVersions.`
|
|
763
|
+
});
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (cardinalityValue === "many" && sourceShapes.some((shape) => shape !== "single-file")) {
|
|
767
|
+
diagnostics.push({
|
|
768
|
+
code: "KIND_DEFINITION_CARDINALITY_INVALID",
|
|
769
|
+
location: record.documentUri,
|
|
770
|
+
message: `Kind '${resourceKind}' documentCardinality 'many' requires only the single-file source shape.`
|
|
771
|
+
});
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const requiredFiles = ((record.node.subdomains.DescriptorContract?.subdomains.RequiredFiles)?.body ?? []).filter(isResourceNode).filter((node) => node.tag === "File").map((node) => asString(node.properties.name)).filter((value) => Boolean(value));
|
|
775
|
+
return Object.freeze({
|
|
776
|
+
resourceId: record.resourceId,
|
|
777
|
+
resourceKind,
|
|
778
|
+
sourceShapes: Object.freeze([...sourceShapes]),
|
|
779
|
+
requiredFiles: Object.freeze([...requiredFiles]),
|
|
780
|
+
currentApiVersion,
|
|
781
|
+
supportedApiVersions: Object.freeze([...new Set(supportedApiVersions)]),
|
|
782
|
+
documentCardinality: cardinalityValue,
|
|
783
|
+
documentUri: record.documentUri
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function validateResourceApiVersion(record, definition, diagnostics) {
|
|
787
|
+
if (definition.supportedApiVersions.includes(record.metadata.apiVersion)) return;
|
|
788
|
+
diagnostics.push({
|
|
789
|
+
code: "RESOURCE_API_VERSION_UNSUPPORTED",
|
|
790
|
+
location: record.documentUri,
|
|
791
|
+
message: `Kind '${record.kind}' does not support apiVersion '${record.metadata.apiVersion}'.`,
|
|
792
|
+
hint: `Current apiVersion is '${definition.currentApiVersion}'.`
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
async function validateRequiredFiles(resourceDir, record, definition, diagnostics) {
|
|
796
|
+
for (const name of definition.requiredFiles) {
|
|
797
|
+
if (!isPlainEntry(name)) {
|
|
798
|
+
diagnostics.push({
|
|
799
|
+
code: "KIND_DEFINITION_REQUIRED_FILE_INVALID",
|
|
800
|
+
location: record.documentUri,
|
|
801
|
+
message: `Invalid required file '${name}'.`
|
|
802
|
+
});
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (!await stat(join(resourceDir, name)).then((info) => info.isFile()).catch(() => false)) diagnostics.push({
|
|
806
|
+
code: "RESOURCE_REQUIRED_FILE_MISSING",
|
|
807
|
+
location: `${record.documentUri}#${name}`,
|
|
808
|
+
message: `Resource is missing required material '${name}'.`
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async function resolveCatalogDirectory(uri, manifestDir, location, context) {
|
|
813
|
+
const relativeMatch = /^vfs:\/\/(\.\/|@\/)(.*)$/.exec(uri);
|
|
814
|
+
if (!relativeMatch || !uri.endsWith("/")) {
|
|
815
|
+
context.diagnostics.push({
|
|
816
|
+
code: "RESOURCE_REF_UNSUPPORTED",
|
|
817
|
+
location,
|
|
818
|
+
message: "Catalog roots must be directory vfs://./ or vfs://@/ references."
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const rawPath = relativeMatch[2];
|
|
823
|
+
let decoded;
|
|
824
|
+
try {
|
|
825
|
+
decoded = decodeURIComponent(rawPath);
|
|
826
|
+
} catch {
|
|
827
|
+
context.diagnostics.push({
|
|
828
|
+
code: "RESOURCE_REF_CONTAINMENT",
|
|
829
|
+
location,
|
|
830
|
+
message: "VFS reference contains invalid encoding."
|
|
831
|
+
});
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (isAbsolute(decoded) || decoded.split(/[\\/]+/).includes("..") || /%2f|%5c/i.test(rawPath)) {
|
|
835
|
+
context.diagnostics.push({
|
|
836
|
+
code: "RESOURCE_REF_CONTAINMENT",
|
|
837
|
+
location,
|
|
838
|
+
message: "VFS reference escapes the owning package boundary."
|
|
839
|
+
});
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const base = relativeMatch[1] === "@/" ? context.rootDir : manifestDir;
|
|
843
|
+
const boundary = await realpath(context.rootDir);
|
|
844
|
+
const candidate = resolve(base, decoded);
|
|
845
|
+
const resolved = await realpath(candidate).catch(() => candidate);
|
|
846
|
+
if (resolved !== boundary && !resolved.startsWith(`${boundary}${sep}`)) {
|
|
847
|
+
context.diagnostics.push({
|
|
848
|
+
code: "RESOURCE_REF_CONTAINMENT",
|
|
849
|
+
location,
|
|
850
|
+
message: "VFS reference resolves outside the owning package boundary."
|
|
851
|
+
});
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
return resolved;
|
|
855
|
+
}
|
|
856
|
+
function validateIdentity(record, context) {
|
|
857
|
+
const prior = context.seenIdentities.get(record.resourceId);
|
|
858
|
+
if (prior) {
|
|
859
|
+
context.diagnostics.push({
|
|
860
|
+
code: "RESOURCE_IDENTITY_DUPLICATE",
|
|
861
|
+
location: record.documentUri,
|
|
862
|
+
message: `Duplicate resource identity '${record.resourceId}'.`,
|
|
863
|
+
hint: `First seen at ${prior}.`
|
|
864
|
+
});
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
context.seenIdentities.set(record.resourceId, record.documentUri);
|
|
868
|
+
}
|
|
869
|
+
function documentUriFor(rootDir, filePath) {
|
|
870
|
+
return `vfs://@/${relative(rootDir, filePath).split(sep).join("/")}`;
|
|
871
|
+
}
|
|
872
|
+
function kindMismatch(actual, expected, location) {
|
|
873
|
+
return {
|
|
874
|
+
code: "RESOURCE_KIND_MISMATCH",
|
|
875
|
+
location,
|
|
876
|
+
message: `Resource kind '${actual}' does not match catalog kind '${expected}'.`
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function isXnlShape(value) {
|
|
880
|
+
return value === "single-file" || value === "directory" || value === "manifest";
|
|
881
|
+
}
|
|
882
|
+
function isDocumentCardinality(value) {
|
|
883
|
+
return value === "one" || value === "many";
|
|
884
|
+
}
|
|
885
|
+
function isPlainEntry(value) {
|
|
886
|
+
return Boolean(value) && !value.includes("/") && !value.includes("\\") && !value.includes("..");
|
|
887
|
+
}
|
|
888
|
+
function asString(value) {
|
|
889
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
890
|
+
}
|
|
891
|
+
function isResourceNode(value) {
|
|
892
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "tag" in value;
|
|
893
|
+
}
|
|
894
|
+
function isDataElement(value) {
|
|
895
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value && value.kind === "DataElement";
|
|
896
|
+
}
|
|
897
|
+
function isElement(value) {
|
|
898
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value && (value.kind === "DataElement" || value.kind === "TextElement");
|
|
899
|
+
}
|
|
900
|
+
function isWord(value) {
|
|
901
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value && value.kind === "Word";
|
|
902
|
+
}
|
|
903
|
+
function isComment(value) {
|
|
904
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value && value.kind === "Comment";
|
|
905
|
+
}
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region ../resource-core/src/index.ts
|
|
908
|
+
var ResourceValidationError = class extends Error {
|
|
909
|
+
diagnostics;
|
|
910
|
+
constructor(diagnostics) {
|
|
911
|
+
super(`Resource validation failed with ${diagnostics.length} diagnostic(s)`);
|
|
912
|
+
this.name = "ResourceValidationError";
|
|
913
|
+
this.diagnostics = diagnostics;
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
async function loadResourceTree(options) {
|
|
917
|
+
const unsupported = unsupportedAuthorityDiagnostic(options);
|
|
918
|
+
if (unsupported) throw new ResourceValidationError([unsupported]);
|
|
919
|
+
const manifestPath = options.manifestPath ?? "manifest.xnl";
|
|
920
|
+
const result = await buildXnlResourceTree({
|
|
921
|
+
...options,
|
|
922
|
+
rootDir: resolve(options.rootDir),
|
|
923
|
+
manifestPath
|
|
924
|
+
});
|
|
925
|
+
if (result.diagnostics.length > 0 || !result.tree) throw new ResourceValidationError(result.diagnostics);
|
|
926
|
+
return result.tree;
|
|
927
|
+
}
|
|
928
|
+
async function validateResourceTree(options) {
|
|
929
|
+
const unsupported = unsupportedAuthorityDiagnostic(options);
|
|
930
|
+
if (unsupported) return [unsupported];
|
|
931
|
+
const manifestPath = options.manifestPath ?? "manifest.xnl";
|
|
932
|
+
return (await buildXnlResourceTree({
|
|
933
|
+
...options,
|
|
934
|
+
rootDir: resolve(options.rootDir),
|
|
935
|
+
manifestPath
|
|
936
|
+
})).diagnostics;
|
|
937
|
+
}
|
|
938
|
+
function unsupportedAuthorityDiagnostic(options) {
|
|
939
|
+
const manifestPath = options.manifestPath ?? "manifest.xnl";
|
|
940
|
+
if (!manifestPath.toLowerCase().endsWith(".xnl")) return {
|
|
941
|
+
code: "RESOURCE_AUTHORITY_FORMAT_UNSUPPORTED",
|
|
942
|
+
location: `vfs://@/${manifestPath}`,
|
|
943
|
+
message: "Resource package manifests must use XNL authority."
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
//#endregion
|
|
947
|
+
export { isLoadedResourceTreeAuthentic as a, sha256Digest as c, markEffectiveRegistryAuthentic as d, ResourceCompositionError as f, readonlyMap as i, effectiveRegistryLayerBindings as l, digestCanonical as m, loadResourceTree as n, buildResourceDependencySnapshot as o, compareCodeUnits as p, validateResourceTree as r, createResourceContentIdentity as s, ResourceValidationError as t, isEffectiveRegistryAuthentic as u };
|