@openship/protocol 0.0.3 → 0.1.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/README.md +4 -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 +353 -0
- package/dist/skill/references/examples/valid/systems.json +140 -25
- package/dist/skill/references/openship-systems.md +87 -69
- package/dist/skill/references/openship.md +1 -1
- package/dist/skill/references/schemas/systems.schema.json +554 -57
- package/package.json +1 -1
- package/src/index.d.ts +12 -4
- package/src/index.js +130 -39
package/src/index.d.ts
CHANGED
|
@@ -10,12 +10,20 @@ export interface SourcesManifest { openship: "1.0"; capability: "sources"; diges
|
|
|
10
10
|
export interface SourcesBundle { openship: "1.0"; capability: "sources"; digest: string; files: Record<string, { encoding: OpenShipEncoding; content: string; [key: string]: unknown }>; [key: string]: unknown }
|
|
11
11
|
export interface DiscoveryAgent { summary: string; instructions: string; skill: string; [key: string]: unknown }
|
|
12
12
|
export interface DiscoveryDocument { openship: "1.0"; capability: "discovery"; project: { name: string; description: string; [key: string]: unknown }; agent: DiscoveryAgent; page?: string; capabilities: { sources: { description: string; manifest: string; bundle: string; mcp?: string; [key: string]: unknown }; systems?: { description: string; document: string; [key: string]: unknown }; changes?: { description: string; policy: string; submit: string; status: string; [key: string]: unknown }; [key: string]: unknown }; [key: string]: unknown }
|
|
13
|
-
export type SystemsNodeKind = "Root" | "Host" | "Container" | "Process" | "Library";
|
|
13
|
+
export type SystemsNodeKind = "Root" | "Block" | "Store" | "Host" | "Container" | "Process" | "Library";
|
|
14
14
|
export type SystemsNodeOwnership = "first_party" | "third_party";
|
|
15
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
16
|
+
export interface SystemsConfiguration { name: string; description: string; required: boolean; sensitive?: boolean; value?: JsonValue; secretRef?: { nodeId: string; key: string }; }
|
|
15
17
|
export interface SystemsNodeMetadata { ownership: SystemsNodeOwnership; [key: string]: unknown }
|
|
16
|
-
export interface SystemsNode { id: string; kind: SystemsNodeKind; name: string; parentId?: string; sourceSelectors?: string[]; metadata: SystemsNodeMetadata; [key: string]: unknown }
|
|
17
|
-
export interface
|
|
18
|
-
export interface
|
|
18
|
+
export interface SystemsNode { id: string; kind: SystemsNodeKind; name: string; parentId?: string; sourceSelectors?: string[]; metadata: SystemsNodeMetadata; configuration?: SystemsConfiguration[]; [key: string]: unknown }
|
|
19
|
+
export interface SystemsEdge { id: string; type: "Runtime" | "Dataflow" | "Dependency"; fromNodeId: string; toNodeId: string; metadata?: Record<string, unknown>; [key: string]: unknown }
|
|
20
|
+
export interface SystemsLayer { id: string; name: string; role: "logical" | "technical" | "provider" | "custom"; rootNodeId: string; nodes: SystemsNode[]; edges: SystemsEdge[]; [key: string]: unknown }
|
|
21
|
+
export interface SystemsRefinement { id: string; fromNodeId: string; toNodeId: string; [key: string]: unknown }
|
|
22
|
+
export interface SystemsBinding { nodeId: string; resourceId?: string; configuration?: SystemsConfiguration[]; state?: { appliedMigration?: string; snapshot?: { ref: string; capturedAt: string; digest?: string } }; [key: string]: unknown }
|
|
23
|
+
export interface SystemsInstance { id: string; name: string; environment: string; layerId: string; bindings: SystemsBinding[]; [key: string]: unknown }
|
|
24
|
+
export interface SystemsDomain { id: string; name: string; description?: string; nodeIds: string[]; [key: string]: unknown }
|
|
25
|
+
export interface SystemsGraph { id: string; name: string; layers: SystemsLayer[]; refinements: SystemsRefinement[]; domains?: SystemsDomain[]; instances?: SystemsInstance[]; metadata?: Record<string, unknown>; context?: Record<string, unknown>; [key: string]: unknown }
|
|
26
|
+
export interface SystemsDocument { openship: "1.0"; capability: "systems"; systemsVersion: "2.0"; source: { manifest: SourcesManifest; bundle: SourcesBundle; [key: string]: unknown }; system: SystemsGraph; [key: string]: unknown }
|
|
19
27
|
export interface VerifiedSourceFile { metadata: SourceFileMetadata; bytes: Uint8Array }
|
|
20
28
|
export interface VerifiedSources { manifest: SourcesManifest; bundle: SourcesBundle; files: VerifiedSourceFile[]; decodedBytes: number }
|
|
21
29
|
export interface FetchedOpenShip { origin: string; discovery: DiscoveryDocument; snapshot: { kind: "systems"; document: SystemsDocument } | { kind: "sources"; manifest: SourcesManifest; bundle: SourcesBundle }; verified: VerifiedSources }
|
package/src/index.js
CHANGED
|
@@ -250,54 +250,142 @@ 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 domains = array(system.domains === undefined ? [] : system.domains, "$.system.domains");
|
|
330
|
+
unique(domains.map((domain) => identifier(object(domain, "$.system.domains").id, "$.system.domains.id")), "$.system.domains");
|
|
331
|
+
for (const domain of domains) {
|
|
332
|
+
const at = `$.system.domains.${domain.id}`;
|
|
333
|
+
string(domain.name, `${at}.name`);
|
|
334
|
+
if (domain.description !== undefined) string(domain.description, `${at}.description`);
|
|
335
|
+
const members = array(domain.nodeIds, `${at}.nodeIds`);
|
|
336
|
+
unique(members, `${at}.nodeIds`);
|
|
337
|
+
for (const nodeId of members) if (!nodeById.has(nodeId)) fail(`${at}.nodeIds`, "must reference existing system nodes");
|
|
338
|
+
}
|
|
339
|
+
const refinements = array(system.refinements, "$.system.refinements");
|
|
340
|
+
unique(refinements.map((ref) => identifier(object(ref, "$.system.refinements").id, "$.system.refinements.id")), "$.system.refinements");
|
|
341
|
+
for (const ref of refinements) {
|
|
342
|
+
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
343
|
}
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
for (const
|
|
277
|
-
|
|
344
|
+
const instances = array(system.instances ?? [], "$.system.instances");
|
|
345
|
+
unique(instances.map((instance) => identifier(object(instance, "$.system.instances").id, "$.system.instances.id")), "$.system.instances");
|
|
346
|
+
for (const instance of instances) {
|
|
347
|
+
const at = `$.system.instances.${instance.id}`;
|
|
348
|
+
string(instance.name, `${at}.name`); string(instance.environment, `${at}.environment`);
|
|
349
|
+
const layer = layerById.get(instance.layerId);
|
|
350
|
+
if (!layer) fail(`${at}.layerId`, "must reference a layer");
|
|
351
|
+
const bindings = array(instance.bindings, `${at}.bindings`);
|
|
352
|
+
unique(bindings.map((binding) => object(binding, `${at}.bindings`).nodeId), `${at}.bindings`);
|
|
353
|
+
for (const binding of bindings) {
|
|
354
|
+
const node = nodeById.get(binding.nodeId);
|
|
355
|
+
if (!node || !layer.nodes.includes(node) || node.kind === "Root") fail(`${at}.bindings`, "must bind non-root nodes in the instance layer");
|
|
356
|
+
if (binding.resourceId !== undefined) string(binding.resourceId, `${at}.resourceId`);
|
|
357
|
+
configuration(binding.configuration, `${at}.configuration`);
|
|
358
|
+
if (binding.state !== undefined) {
|
|
359
|
+
if (node.kind !== "Store") fail(`${at}.state`, "database state requires a Store node");
|
|
360
|
+
const state = object(binding.state, `${at}.state`);
|
|
361
|
+
if (state.appliedMigration !== undefined) string(state.appliedMigration, `${at}.state.appliedMigration`);
|
|
362
|
+
if (state.snapshot !== undefined) {
|
|
363
|
+
const snapshot = object(state.snapshot, `${at}.state.snapshot`);
|
|
364
|
+
string(snapshot.ref, `${at}.state.snapshot.ref`);
|
|
365
|
+
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");
|
|
366
|
+
const [year, month, day] = snapshot.capturedAt.slice(0, 10).split("-").map(Number);
|
|
367
|
+
if (day < 1 || day > new Date(Date.UTC(year, month, 0)).getUTCDate()) fail(`${at}.state.snapshot.capturedAt`, "must be a valid calendar date");
|
|
368
|
+
if (snapshot.digest !== undefined && !digestPattern.test(snapshot.digest)) fail(`${at}.state.snapshot.digest`, "must be a sha256 digest");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
278
371
|
}
|
|
279
372
|
}
|
|
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");
|
|
373
|
+
for (const entries of [...nodeById.values()].map((node) => node.configuration).concat(instances.flatMap((instance) => instance.bindings.map((binding) => binding.configuration)))) {
|
|
374
|
+
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
375
|
}
|
|
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
376
|
const context = system.context;
|
|
296
377
|
if (!context) return payload;
|
|
297
378
|
object(context, "$.system.context");
|
|
379
|
+
for (const key of ["concerns", "documents", "matrix", "artifacts", "systemPromptRefs"]) if (context[key] !== undefined) array(context[key], `$.system.context.${key}`);
|
|
380
|
+
unique(context.concerns ?? [], "$.system.context.concerns");
|
|
381
|
+
for (const concern of context.concerns ?? []) string(concern, "$.system.context.concerns");
|
|
298
382
|
const concerns = new Set(context.concerns ?? []);
|
|
299
383
|
const documents = new Map();
|
|
300
384
|
for (const document of context.documents ?? []) {
|
|
385
|
+
object(document, "$.system.context.documents");
|
|
386
|
+
string(document.title, "$.system.context.documents.title");
|
|
387
|
+
string(document.language, "$.system.context.documents.language");
|
|
388
|
+
if (typeof document.text !== "string") fail("$.system.context.documents.text", "must be a string");
|
|
301
389
|
if (!["Document", "Skill", "Prompt"].includes(document.kind)) fail("$.system.context.documents", "contains an invalid document kind");
|
|
302
390
|
const expected = `sha256:${sha256Hex(`${document.kind}\n${document.title}\n${document.language}\n${document.text}`)}`;
|
|
303
391
|
if (document.hash !== expected) fail(`$.system.context.documents.${document.hash}`, "hash does not match canonical document content");
|
|
@@ -306,6 +394,8 @@ export function validateSystems(value, options = {}) {
|
|
|
306
394
|
}
|
|
307
395
|
assertAcyclic([...documents.keys()], [...documents.values()].filter((doc) => doc.supersedes && documents.has(doc.supersedes)).map((doc) => [doc.hash, doc.supersedes]), "$.system.context.documents[].supersedes");
|
|
308
396
|
for (const cell of context.matrix ?? []) {
|
|
397
|
+
object(cell, "$.system.context.matrix");
|
|
398
|
+
for (const key of ["documentRefs", "skillRefs"]) if (cell[key] !== undefined) unique(array(cell[key], `$.system.context.matrix.${key}`), `$.system.context.matrix.${key}`);
|
|
309
399
|
if (!nodeById.has(cell.nodeId)) fail("$.system.context.matrix", `references missing node ${cell.nodeId}`);
|
|
310
400
|
if (!concerns.has(cell.concern)) fail("$.system.context.matrix", `references undeclared concern ${cell.concern}`);
|
|
311
401
|
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 +405,8 @@ export function validateSystems(value, options = {}) {
|
|
|
315
405
|
const artifactIds = [];
|
|
316
406
|
const sourcePaths = new Set(source.manifest.files.map((file) => file.path));
|
|
317
407
|
for (const artifact of context.artifacts ?? []) {
|
|
318
|
-
|
|
408
|
+
object(artifact, "$.system.context.artifacts");
|
|
409
|
+
artifactIds.push(identifier(artifact.id, "$.system.context.artifacts.id"));
|
|
319
410
|
if (!nodeById.has(artifact.nodeId)) fail("$.system.context.artifacts", `references missing node ${artifact.nodeId}`);
|
|
320
411
|
if (!concerns.has(artifact.concern)) fail("$.system.context.artifacts", `references undeclared concern ${artifact.concern}`);
|
|
321
412
|
if (artifact.type === "Code") {
|