@openship/protocol 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/package-meta.json +2 -2
- package/dist/skill/SKILL.md +1 -1
- package/dist/skill/references/examples/invalid/systems-ownership.json +29 -7
- package/dist/skill/references/examples/invalid/systems.json +63 -12
- package/dist/skill/references/examples/valid/systems-layered.json +332 -0
- package/dist/skill/references/examples/valid/systems.json +140 -25
- package/dist/skill/references/openship-systems.md +70 -69
- package/dist/skill/references/openship.md +1 -1
- package/dist/skill/references/schemas/systems.schema.json +519 -57
- package/package.json +1 -1
- package/src/index.d.ts +11 -4
- package/src/index.js +120 -39
package/src/index.js
CHANGED
|
@@ -250,54 +250,132 @@ export function validateSystems(value, options = {}) {
|
|
|
250
250
|
const system = object(payload.system, "$.system");
|
|
251
251
|
string(system.id, "$.system.id");
|
|
252
252
|
string(system.name, "$.system.name");
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (![
|
|
265
|
-
|
|
253
|
+
if (payload.systemsVersion !== "2.0") fail("$.systemsVersion", "requires layered Systems 2.0; legacy Systems is unsupported", "unsupported_version");
|
|
254
|
+
for (const key of ["nodes", "edges", "rootNodeId"]) if (key in system) fail(`$.system.${key}`, "legacy graph members are unsupported");
|
|
255
|
+
const layers = array(system.layers, "$.system.layers");
|
|
256
|
+
if (!layers.length) fail("$.system.layers", "requires at least one layer");
|
|
257
|
+
const identifier = (value, path) => {
|
|
258
|
+
if (!idPattern.test(string(value, path))) fail(path, "has an invalid identifier");
|
|
259
|
+
return value;
|
|
260
|
+
};
|
|
261
|
+
identifier(system.id, "$.system.id");
|
|
262
|
+
const jsonValue = (value, path, ancestors = new Set()) => {
|
|
263
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) return;
|
|
264
|
+
if (typeof value !== "object" || ancestors.has(value) || (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))) fail(path, "must be a JSON value");
|
|
265
|
+
ancestors.add(value);
|
|
266
|
+
for (const item of Array.isArray(value) ? value : Object.values(value)) jsonValue(item, path, ancestors);
|
|
267
|
+
ancestors.delete(value);
|
|
268
|
+
};
|
|
269
|
+
const configuration = (entries, path) => {
|
|
270
|
+
if (entries === undefined) return;
|
|
271
|
+
const names = [];
|
|
272
|
+
for (const [index, raw] of array(entries, path).entries()) {
|
|
273
|
+
const at = `${path}[${index}]`, entry = object(raw, at);
|
|
274
|
+
names.push(string(entry.name, `${at}.name`));
|
|
275
|
+
string(entry.description, `${at}.description`);
|
|
276
|
+
if (typeof entry.required !== "boolean") fail(`${at}.required`, "must be boolean");
|
|
277
|
+
if (entry.sensitive !== undefined && typeof entry.sensitive !== "boolean") fail(`${at}.sensitive`, "must be boolean");
|
|
278
|
+
if ("value" in entry) jsonValue(entry.value, `${at}.value`);
|
|
279
|
+
if ("value" in entry && (entry.sensitive === true || entry.secretRef !== undefined)) fail(at, "secret configuration cannot contain a literal value");
|
|
280
|
+
if (entry.secretRef !== undefined) {
|
|
281
|
+
const ref = object(entry.secretRef, `${at}.secretRef`);
|
|
282
|
+
identifier(ref.nodeId, `${at}.secretRef.nodeId`);
|
|
283
|
+
string(ref.key, `${at}.secretRef.key`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
unique(names, path);
|
|
287
|
+
};
|
|
288
|
+
const nodeById = new Map(), layerByNode = new Map(), layerById = new Map();
|
|
289
|
+
for (const [index, raw] of layers.entries()) {
|
|
290
|
+
const at = `$.system.layers[${index}]`, layer = object(raw, at);
|
|
291
|
+
identifier(layer.id, `${at}.id`);
|
|
292
|
+
if (layerById.has(layer.id)) fail(`${at}.id`, "must be unique");
|
|
293
|
+
layerById.set(layer.id, layer);
|
|
294
|
+
string(layer.name, `${at}.name`);
|
|
295
|
+
if (!["logical", "technical", "provider", "custom"].includes(layer.role)) fail(`${at}.role`, "invalid layer role");
|
|
296
|
+
const rootNodeId = identifier(layer.rootNodeId, `${at}.rootNodeId`);
|
|
297
|
+
const nodes = array(layer.nodes, `${at}.nodes`), local = new Map();
|
|
298
|
+
for (const [i, rawNode] of nodes.entries()) {
|
|
299
|
+
const path = `${at}.nodes[${i}]`, node = object(rawNode, path);
|
|
300
|
+
identifier(node.id, `${path}.id`);
|
|
301
|
+
if (nodeById.has(node.id)) fail(`${path}.id`, "must be globally unique");
|
|
302
|
+
if (!["Root", "Block", "Store", "Host", "Container", "Process", "Library"].includes(node.kind)) fail(`${path}.kind`, "invalid node kind");
|
|
303
|
+
string(node.name, `${path}.name`);
|
|
304
|
+
const metadata = object(node.metadata, `${path}.metadata`);
|
|
305
|
+
if (!["first_party", "third_party"].includes(metadata.ownership)) fail(`${path}.metadata.ownership`, "must be first_party or third_party");
|
|
306
|
+
configuration(node.configuration, `${path}.configuration`);
|
|
307
|
+
if (node.sourceSelectors !== undefined) {
|
|
308
|
+
unique(array(node.sourceSelectors, `${path}.sourceSelectors`), `${path}.sourceSelectors`);
|
|
309
|
+
for (const selector of node.sourceSelectors) {
|
|
310
|
+
string(selector, `${path}.sourceSelectors`);
|
|
311
|
+
if (!source.manifest.files.some((file) => matchOpenShipPattern(selector, file.path))) fail(`${path}.sourceSelectors`, `${selector} matches no source path`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
nodeById.set(node.id, node); local.set(node.id, node); layerByNode.set(node.id, index);
|
|
315
|
+
}
|
|
316
|
+
const roots = nodes.filter((node) => node.kind === "Root");
|
|
317
|
+
if (roots.length !== 1 || roots[0].id !== rootNodeId || roots[0].parentId !== undefined) fail(`${at}.rootNodeId`, "must identify the one parentless Root");
|
|
318
|
+
for (const node of nodes) if (node.id !== rootNodeId && !local.has(node.parentId)) fail(`${at}.nodes.${node.id}.parentId`, "must reference a parent in this layer");
|
|
319
|
+
assertAcyclic([...local.keys()], nodes.filter((node) => node.parentId).map((node) => [node.parentId, node.id]), `${at}.nodes`);
|
|
320
|
+
const edges = array(layer.edges, `${at}.edges`);
|
|
321
|
+
unique(edges.map((edge) => identifier(object(edge, `${at}.edges`).id, `${at}.edges.id`)), `${at}.edges`);
|
|
322
|
+
for (const edge of edges) {
|
|
323
|
+
const from = local.get(edge.fromNodeId), to = local.get(edge.toNodeId);
|
|
324
|
+
if (!from || !to || from.kind === "Root" || to.kind === "Root") fail(`${at}.edges.${edge.id}`, "endpoints must be non-root nodes in this layer");
|
|
325
|
+
if (!["Runtime", "Dataflow", "Dependency"].includes(edge.type)) fail(`${at}.edges.${edge.id}.type`, "invalid edge type");
|
|
326
|
+
}
|
|
327
|
+
for (const type of ["Dataflow", "Dependency"]) assertAcyclic([...local.keys()], edges.filter((edge) => edge.type === type).map((edge) => [edge.fromNodeId, edge.toNodeId]), `${at}.edges[${type}]`);
|
|
328
|
+
}
|
|
329
|
+
const refinements = array(system.refinements, "$.system.refinements");
|
|
330
|
+
unique(refinements.map((ref) => identifier(object(ref, "$.system.refinements").id, "$.system.refinements.id")), "$.system.refinements");
|
|
331
|
+
for (const ref of refinements) {
|
|
332
|
+
if (!nodeById.has(ref.fromNodeId) || !nodeById.has(ref.toNodeId) || layerByNode.get(ref.fromNodeId) <= layerByNode.get(ref.toNodeId)) fail(`$.system.refinements.${ref.id}`, "must connect a concrete node to a node in an earlier layer");
|
|
266
333
|
}
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
for (const
|
|
277
|
-
|
|
334
|
+
const instances = array(system.instances ?? [], "$.system.instances");
|
|
335
|
+
unique(instances.map((instance) => identifier(object(instance, "$.system.instances").id, "$.system.instances.id")), "$.system.instances");
|
|
336
|
+
for (const instance of instances) {
|
|
337
|
+
const at = `$.system.instances.${instance.id}`;
|
|
338
|
+
string(instance.name, `${at}.name`); string(instance.environment, `${at}.environment`);
|
|
339
|
+
const layer = layerById.get(instance.layerId);
|
|
340
|
+
if (!layer) fail(`${at}.layerId`, "must reference a layer");
|
|
341
|
+
const bindings = array(instance.bindings, `${at}.bindings`);
|
|
342
|
+
unique(bindings.map((binding) => object(binding, `${at}.bindings`).nodeId), `${at}.bindings`);
|
|
343
|
+
for (const binding of bindings) {
|
|
344
|
+
const node = nodeById.get(binding.nodeId);
|
|
345
|
+
if (!node || !layer.nodes.includes(node) || node.kind === "Root") fail(`${at}.bindings`, "must bind non-root nodes in the instance layer");
|
|
346
|
+
if (binding.resourceId !== undefined) string(binding.resourceId, `${at}.resourceId`);
|
|
347
|
+
configuration(binding.configuration, `${at}.configuration`);
|
|
348
|
+
if (binding.state !== undefined) {
|
|
349
|
+
if (node.kind !== "Store") fail(`${at}.state`, "database state requires a Store node");
|
|
350
|
+
const state = object(binding.state, `${at}.state`);
|
|
351
|
+
if (state.appliedMigration !== undefined) string(state.appliedMigration, `${at}.state.appliedMigration`);
|
|
352
|
+
if (state.snapshot !== undefined) {
|
|
353
|
+
const snapshot = object(state.snapshot, `${at}.state.snapshot`);
|
|
354
|
+
string(snapshot.ref, `${at}.state.snapshot.ref`);
|
|
355
|
+
if (typeof snapshot.capturedAt !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(snapshot.capturedAt) || !Number.isFinite(Date.parse(snapshot.capturedAt))) fail(`${at}.state.snapshot.capturedAt`, "must be an RFC3339 timestamp");
|
|
356
|
+
const [year, month, day] = snapshot.capturedAt.slice(0, 10).split("-").map(Number);
|
|
357
|
+
if (day < 1 || day > new Date(Date.UTC(year, month, 0)).getUTCDate()) fail(`${at}.state.snapshot.capturedAt`, "must be a valid calendar date");
|
|
358
|
+
if (snapshot.digest !== undefined && !digestPattern.test(snapshot.digest)) fail(`${at}.state.snapshot.digest`, "must be a sha256 digest");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
278
361
|
}
|
|
279
362
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
unique(edges.map((edge) => edge.id), "$.system.edges[].id");
|
|
283
|
-
for (const edge of edges) {
|
|
284
|
-
const from = nodeById.get(edge.fromNodeId);
|
|
285
|
-
const to = nodeById.get(edge.toNodeId);
|
|
286
|
-
if (!from || !to) fail(`$.system.edges.${edge.id}`, "references a missing node");
|
|
287
|
-
if (from.kind !== "Process") fail(`$.system.edges.${edge.id}.fromNodeId`, "must reference a Process");
|
|
288
|
-
if (edge.type === "Dependency" && to.kind !== "Library") fail(`$.system.edges.${edge.id}.toNodeId`, "Dependency must target a Library");
|
|
289
|
-
if ((edge.type === "Runtime" || edge.type === "Dataflow") && to.kind !== "Process" && to.kind !== "Container") fail(`$.system.edges.${edge.id}.toNodeId`, "must target a Process or Container");
|
|
290
|
-
if (!["Runtime", "Dataflow", "Dependency"].includes(edge.type)) fail(`$.system.edges.${edge.id}.type`, "is not a v1 edge type");
|
|
363
|
+
for (const entries of [...nodeById.values()].map((node) => node.configuration).concat(instances.flatMap((instance) => instance.bindings.map((binding) => binding.configuration)))) {
|
|
364
|
+
for (const entry of entries ?? []) if (entry.secretRef && (!nodeById.has(entry.secretRef.nodeId) || nodeById.get(entry.secretRef.nodeId).kind === "Root")) fail("$.system.configuration.secretRef.nodeId", "must reference a component");
|
|
291
365
|
}
|
|
292
|
-
const ids = nodes.map((node) => node.id);
|
|
293
|
-
assertAcyclic(ids, edges.filter((edge) => edge.type === "Dataflow").map((edge) => [edge.fromNodeId, edge.toNodeId]), "$.system.edges[Dataflow]");
|
|
294
|
-
assertAcyclic(ids, edges.filter((edge) => edge.type === "Dependency").map((edge) => [edge.fromNodeId, edge.toNodeId]), "$.system.edges[Dependency]");
|
|
295
366
|
const context = system.context;
|
|
296
367
|
if (!context) return payload;
|
|
297
368
|
object(context, "$.system.context");
|
|
369
|
+
for (const key of ["concerns", "documents", "matrix", "artifacts", "systemPromptRefs"]) if (context[key] !== undefined) array(context[key], `$.system.context.${key}`);
|
|
370
|
+
unique(context.concerns ?? [], "$.system.context.concerns");
|
|
371
|
+
for (const concern of context.concerns ?? []) string(concern, "$.system.context.concerns");
|
|
298
372
|
const concerns = new Set(context.concerns ?? []);
|
|
299
373
|
const documents = new Map();
|
|
300
374
|
for (const document of context.documents ?? []) {
|
|
375
|
+
object(document, "$.system.context.documents");
|
|
376
|
+
string(document.title, "$.system.context.documents.title");
|
|
377
|
+
string(document.language, "$.system.context.documents.language");
|
|
378
|
+
if (typeof document.text !== "string") fail("$.system.context.documents.text", "must be a string");
|
|
301
379
|
if (!["Document", "Skill", "Prompt"].includes(document.kind)) fail("$.system.context.documents", "contains an invalid document kind");
|
|
302
380
|
const expected = `sha256:${sha256Hex(`${document.kind}\n${document.title}\n${document.language}\n${document.text}`)}`;
|
|
303
381
|
if (document.hash !== expected) fail(`$.system.context.documents.${document.hash}`, "hash does not match canonical document content");
|
|
@@ -306,6 +384,8 @@ export function validateSystems(value, options = {}) {
|
|
|
306
384
|
}
|
|
307
385
|
assertAcyclic([...documents.keys()], [...documents.values()].filter((doc) => doc.supersedes && documents.has(doc.supersedes)).map((doc) => [doc.hash, doc.supersedes]), "$.system.context.documents[].supersedes");
|
|
308
386
|
for (const cell of context.matrix ?? []) {
|
|
387
|
+
object(cell, "$.system.context.matrix");
|
|
388
|
+
for (const key of ["documentRefs", "skillRefs"]) if (cell[key] !== undefined) unique(array(cell[key], `$.system.context.matrix.${key}`), `$.system.context.matrix.${key}`);
|
|
309
389
|
if (!nodeById.has(cell.nodeId)) fail("$.system.context.matrix", `references missing node ${cell.nodeId}`);
|
|
310
390
|
if (!concerns.has(cell.concern)) fail("$.system.context.matrix", `references undeclared concern ${cell.concern}`);
|
|
311
391
|
for (const hash of cell.documentRefs ?? []) if (documents.get(hash)?.kind !== "Document") fail("$.system.context.matrix", `Document reference ${hash} is missing or has the wrong kind`);
|
|
@@ -315,7 +395,8 @@ export function validateSystems(value, options = {}) {
|
|
|
315
395
|
const artifactIds = [];
|
|
316
396
|
const sourcePaths = new Set(source.manifest.files.map((file) => file.path));
|
|
317
397
|
for (const artifact of context.artifacts ?? []) {
|
|
318
|
-
|
|
398
|
+
object(artifact, "$.system.context.artifacts");
|
|
399
|
+
artifactIds.push(identifier(artifact.id, "$.system.context.artifacts.id"));
|
|
319
400
|
if (!nodeById.has(artifact.nodeId)) fail("$.system.context.artifacts", `references missing node ${artifact.nodeId}`);
|
|
320
401
|
if (!concerns.has(artifact.concern)) fail("$.system.context.artifacts", `references undeclared concern ${artifact.concern}`);
|
|
321
402
|
if (artifact.type === "Code") {
|