@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/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
- const rootNodeId = string(system.rootNodeId, "$.system.rootNodeId");
254
- const nodes = array(system.nodes, "$.system.nodes");
255
- const nodeById = new Map();
256
- for (const [index, raw] of nodes.entries()) {
257
- const node = object(raw, `$.system.nodes[${index}]`);
258
- const id = string(node.id, `$.system.nodes[${index}].id`);
259
- if (!idPattern.test(id)) fail(`$.system.nodes[${index}].id`, "has an invalid identifier");
260
- if (nodeById.has(id)) fail(`$.system.nodes[${index}].id`, "must be unique");
261
- if (!["Root", "Host", "Container", "Process", "Library"].includes(node.kind)) fail(`$.system.nodes[${index}].kind`, "is not a v1 node kind");
262
- string(node.name, `$.system.nodes[${index}].name`);
263
- const metadata = object(node.metadata, `$.system.nodes[${index}].metadata`);
264
- if (!["first_party", "third_party"].includes(metadata.ownership)) fail(`$.system.nodes[${index}].metadata.ownership`, "must be first_party or third_party");
265
- nodeById.set(id, node);
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 roots = nodes.filter((node) => node.kind === "Root");
268
- if (roots.length !== 1 || roots[0].id !== rootNodeId) fail("$.system.rootNodeId", "must identify the one Root node");
269
- if (roots[0].parentId !== undefined) fail("$.system.nodes", "the Root must not have a parent");
270
- for (const node of nodes) {
271
- const parent = node.parentId ? nodeById.get(node.parentId) : undefined;
272
- if (node.kind === "Host" && parent?.kind !== "Root") fail(`$.system.nodes.${node.id}.parentId`, "Host must have the Root parent");
273
- if (node.kind === "Container" && parent?.kind !== "Host") fail(`$.system.nodes.${node.id}.parentId`, "Container must have a Host parent");
274
- if (node.kind === "Process" && parent?.kind !== "Host" && parent?.kind !== "Container") fail(`$.system.nodes.${node.id}.parentId`, "Process must have a Host or Container parent");
275
- if (node.kind === "Library" && node.parentId !== undefined) fail(`$.system.nodes.${node.id}.parentId`, "Library must not have a parent");
276
- for (const selector of node.sourceSelectors ?? []) {
277
- if (!source.manifest.files.some((file) => matchOpenShipPattern(selector, file.path))) fail(`$.system.nodes.${node.id}.sourceSelectors`, `${selector} matches no source path`);
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
- assertAcyclic(nodes.map((node) => node.id), nodes.filter((node) => node.parentId).map((node) => [node.parentId, node.id]), "$.system.nodes");
281
- const edges = array(system.edges, "$.system.edges");
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
- artifactIds.push(artifact.id);
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") {