sysprom 1.11.0 → 1.13.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.
@@ -46,7 +46,7 @@ export const addCommand = {
46
46
  const { doc } = loaded;
47
47
  const type = args.nodeType;
48
48
  if (!NodeType.is(type)) {
49
- console.error(`Unknown node type: ${type}`);
49
+ console.error(`Unknown node type: "${type}". Valid types: ${NodeType.options.join(", ")}`);
50
50
  process.exit(1);
51
51
  }
52
52
  const id = opts.id ?? nextIdOp({ doc, type });
@@ -57,7 +57,7 @@ export const addCommand = {
57
57
  }
58
58
  if (opts.status) {
59
59
  if (!NodeStatus.is(opts.status)) {
60
- console.error(`Unknown status: ${opts.status}`);
60
+ console.error(`Unknown status: "${opts.status}". Valid statuses: ${NodeStatus.options.join(", ")}`);
61
61
  process.exit(1);
62
62
  }
63
63
  node.status = opts.status;
@@ -101,12 +101,14 @@ export function buildCommander(def, parent) {
101
101
  continue;
102
102
  const desc = fieldDescription(field);
103
103
  const choices = fieldChoices(field);
104
+ const optional = fieldIsOptional(field);
104
105
  const flagName = camelToKebab(key);
105
106
  if (choices) {
106
- cmd.addArgument(new Argument(`<${flagName}>`, desc).choices(choices));
107
+ const arg = new Argument(optional ? `[${flagName}]` : `<${flagName}>`, desc).choices(choices);
108
+ cmd.addArgument(arg);
107
109
  }
108
110
  else {
109
- cmd.argument(`<${flagName}>`, desc);
111
+ cmd.argument(optional ? `[${flagName}]` : `<${flagName}>`, desc);
110
112
  }
111
113
  }
112
114
  }
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import * as z from "zod";
5
5
  import { loadDocument } from "../io.js";
6
- import { RelationshipType } from "../schema.js";
6
+ import { NodeType, RelationshipType } from "../schema.js";
7
7
  import { validateOp, statsOp, queryNodesOp, queryNodeOp, queryRelationshipsOp, traceFromNodeOp, addNodeOp, removeNodeOp, updateNodeOp, addRelationshipOp, removeRelationshipOp, nextIdOp, } from "../operations/index.js";
8
8
  // Create MCP server instance
9
9
  const server = new McpServer({
@@ -146,31 +146,9 @@ server.registerTool("add-node", {
146
146
  }),
147
147
  }, ({ path, type, id, name, description }) => {
148
148
  const { doc } = loadDocument(path);
149
- const nodeType = z
150
- .enum([
151
- "intent",
152
- "concept",
153
- "capability",
154
- "element",
155
- "realisation",
156
- "invariant",
157
- "principle",
158
- "policy",
159
- "protocol",
160
- "stage",
161
- "role",
162
- "gate",
163
- "mode",
164
- "artefact",
165
- "artefact_flow",
166
- "decision",
167
- "change",
168
- "view",
169
- "version",
170
- ])
171
- .safeParse(type);
149
+ const nodeType = NodeType.safeParse(type);
172
150
  if (!nodeType.success) {
173
- throw new Error(`Invalid node type: ${type}`);
151
+ throw new Error(`Invalid node type: "${type}". Valid types: ${NodeType.options.join(", ")}`);
174
152
  }
175
153
  const nodeId = id ?? nextIdOp({ doc, type: nodeType.data });
176
154
  const updated = addNodeOp({
@@ -280,7 +258,7 @@ server.registerTool("add-relationship", {
280
258
  const { doc } = loadDocument(path);
281
259
  const relType = RelationshipType.safeParse(type);
282
260
  if (!relType.success) {
283
- throw new Error(`Invalid relationship type: ${type}`);
261
+ throw new Error(`Invalid relationship type: "${type}". Valid types: ${RelationshipType.options.join(", ")}`);
284
262
  }
285
263
  const updated = addRelationshipOp({
286
264
  doc,
@@ -315,7 +293,7 @@ server.registerTool("remove-relationship", {
315
293
  const { doc } = loadDocument(path);
316
294
  const relType = RelationshipType.safeParse(type);
317
295
  if (!relType.success) {
318
- throw new Error(`Invalid relationship type: ${type}`);
296
+ throw new Error(`Invalid relationship type: "${type}". Valid types: ${RelationshipType.options.join(", ")}`);
319
297
  }
320
298
  const result = removeRelationshipOp({
321
299
  doc,
@@ -7,25 +7,25 @@ const operationType = z.enum(["add", "update", "remove", "link"]);
7
7
  function parseNodeType(s) {
8
8
  const result = NodeType.safeParse(s);
9
9
  if (!result.success)
10
- throw new Error(`Unknown node type: ${s}`);
10
+ throw new Error(`Unknown node type: "${s}". Valid types: ${NodeType.options.join(", ")}`);
11
11
  return result.data;
12
12
  }
13
13
  function parseRelType(s) {
14
14
  const result = RelationshipType.safeParse(s);
15
15
  if (!result.success)
16
- throw new Error(`Unknown relationship type: ${s}`);
16
+ throw new Error(`Unknown relationship type: "${s}". Valid types: ${RelationshipType.options.join(", ")}`);
17
17
  return result.data;
18
18
  }
19
19
  function parseNodeStatus(s) {
20
20
  const result = NodeStatus.safeParse(s);
21
21
  if (!result.success)
22
- throw new Error(`Unknown node status: ${s}`);
22
+ throw new Error(`Unknown node status: "${s}". Valid statuses: ${NodeStatus.options.join(", ")}`);
23
23
  return result.data;
24
24
  }
25
25
  function parseExtRefRole(s) {
26
26
  const result = ExternalReferenceRole.safeParse(s);
27
27
  if (!result.success)
28
- throw new Error(`Unknown external reference role: ${s}`);
28
+ throw new Error(`Unknown external reference role: "${s}". Valid roles: ${ExternalReferenceRole.options.join(", ")}`);
29
29
  return result.data;
30
30
  }
31
31
  // ---------------------------------------------------------------------------
@@ -286,7 +286,7 @@ function parseNodeFromSection(section) {
286
286
  const rawType = parts[0];
287
287
  const parsed = operationType.safeParse(rawType);
288
288
  if (!parsed.success) {
289
- throw new Error(`Unknown operation type: ${rawType}`);
289
+ throw new Error(`Unknown operation type: "${rawType}". Valid types: ${operationType.options.join(", ")}`);
290
290
  }
291
291
  const type = parsed.data;
292
292
  const rest = parts.slice(1);
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
3
  import { SysProMDocument, Relationship } from "../schema.js";
4
- import { isValidEndpointPair } from "../endpoint-types.js";
4
+ import { isValidEndpointPair, RELATIONSHIP_ENDPOINT_TYPES, } from "../endpoint-types.js";
5
5
  /**
6
6
  * Add a relationship to a SysProM document. Returns a new document with the relationship appended.
7
7
  * @throws {Error} If either endpoint node does not exist, endpoint types are invalid, or the relationship is a duplicate.
@@ -26,7 +26,8 @@ export const addRelationshipOp = defineOperation({
26
26
  }
27
27
  // Validate endpoint types for this relationship
28
28
  if (!isValidEndpointPair(rel.type, fromNode.type, toNode.type)) {
29
- throw new Error(`Invalid endpoint types for ${rel.type}: ${fromNode.type} → ${toNode.type}`);
29
+ const endpoints = RELATIONSHIP_ENDPOINT_TYPES[rel.type];
30
+ throw new Error(`Invalid endpoint types for ${rel.type}: ${fromNode.type} → ${toNode.type}. Valid: [${endpoints.from.join(", ")}] → [${endpoints.to.join(", ")}]`);
30
31
  }
31
32
  // Check for duplicate relationship
32
33
  const isDuplicate = (doc.relationships ?? []).some((r) => r.from === rel.from && r.to === rel.to && r.type === rel.type);
@@ -1,6 +1,6 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
- import { SysProMDocument, Node } from "../schema.js";
3
+ import { SysProMDocument, NodeBase } from "../schema.js";
4
4
  /**
5
5
  * Update specified fields on a node, merging the provided fields into the existing node.
6
6
  * @throws {Error} If the node ID is not found.
@@ -11,7 +11,7 @@ export const updateNodeOp = defineOperation({
11
11
  input: z.object({
12
12
  doc: SysProMDocument,
13
13
  id: z.string(),
14
- fields: Node.partial(),
14
+ fields: NodeBase.partial(),
15
15
  }),
16
16
  output: SysProMDocument,
17
17
  fn({ doc, id, fields }) {
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
3
  import { SysProMDocument } from "../schema.js";
4
- import { isValidEndpointPair } from "../endpoint-types.js";
4
+ import { isValidEndpointPair, RELATIONSHIP_ENDPOINT_TYPES, } from "../endpoint-types.js";
5
5
  /** Zod schema for the result of validating a SysProM document. */
6
6
  export const ValidationResult = z.object({
7
7
  valid: z.boolean(),
@@ -67,7 +67,8 @@ export const validateOp = defineOperation({
67
67
  if (fromNode &&
68
68
  toNode &&
69
69
  !isValidEndpointPair(r.type, fromNode.type, toNode.type)) {
70
- issues.push(`Invalid endpoint types for ${r.type}: ${fromNode.type} → ${toNode.type} (${r.from} → ${r.to})`);
70
+ const endpoints = RELATIONSHIP_ENDPOINT_TYPES[r.type];
71
+ issues.push(`Invalid endpoint types for ${r.type}: ${fromNode.type} → ${toNode.type} (${r.from} → ${r.to}). Valid: [${endpoints.from.join(", ")}] → [${endpoints.to.join(", ")}]`);
71
72
  }
72
73
  }
73
74
  // Operational relationships to retired nodes
@@ -412,6 +412,144 @@ declare const SysProMDocumentSchema: z.ZodObject<{
412
412
  };
413
413
  }>>;
414
414
  }, z.core.$strip>;
415
+ /** Base node object schema without ID-prefix refinement. Supports .partial(). */
416
+ export declare const NodeBase: z.ZodObject<{
417
+ id: z.ZodString;
418
+ type: z.ZodEnum<{
419
+ intent: "intent";
420
+ concept: "concept";
421
+ capability: "capability";
422
+ element: "element";
423
+ realisation: "realisation";
424
+ invariant: "invariant";
425
+ principle: "principle";
426
+ policy: "policy";
427
+ protocol: "protocol";
428
+ stage: "stage";
429
+ role: "role";
430
+ gate: "gate";
431
+ mode: "mode";
432
+ artefact: "artefact";
433
+ artefact_flow: "artefact_flow";
434
+ decision: "decision";
435
+ change: "change";
436
+ view: "view";
437
+ milestone: "milestone";
438
+ version: "version";
439
+ }> & {
440
+ is(value: unknown): value is "intent" | "concept" | "capability" | "element" | "realisation" | "invariant" | "principle" | "policy" | "protocol" | "stage" | "role" | "gate" | "mode" | "artefact" | "artefact_flow" | "decision" | "change" | "view" | "milestone" | "version";
441
+ };
442
+ name: z.ZodString;
443
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
444
+ is(value: unknown): value is string | string[];
445
+ }>;
446
+ status: z.ZodOptional<z.ZodEnum<{
447
+ deprecated: "deprecated";
448
+ proposed: "proposed";
449
+ accepted: "accepted";
450
+ active: "active";
451
+ implemented: "implemented";
452
+ adopted: "adopted";
453
+ defined: "defined";
454
+ introduced: "introduced";
455
+ in_progress: "in_progress";
456
+ complete: "complete";
457
+ consolidated: "consolidated";
458
+ experimental: "experimental";
459
+ retired: "retired";
460
+ superseded: "superseded";
461
+ abandoned: "abandoned";
462
+ deferred: "deferred";
463
+ }> & {
464
+ is(value: unknown): value is "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred";
465
+ }>;
466
+ lifecycle: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
467
+ context: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
468
+ is(value: unknown): value is string | string[];
469
+ }>;
470
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
471
+ id: z.ZodString;
472
+ description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
473
+ is(value: unknown): value is string | string[];
474
+ };
475
+ }, z.core.$loose> & {
476
+ is(value: unknown): value is {
477
+ [x: string]: unknown;
478
+ id: string;
479
+ description: string | string[];
480
+ };
481
+ }>>;
482
+ selected: z.ZodOptional<z.ZodString>;
483
+ rationale: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
484
+ is(value: unknown): value is string | string[];
485
+ }>;
486
+ scope: z.ZodOptional<z.ZodArray<z.ZodString>>;
487
+ operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
488
+ type: z.ZodEnum<{
489
+ link: "link";
490
+ add: "add";
491
+ update: "update";
492
+ remove: "remove";
493
+ }>;
494
+ target: z.ZodOptional<z.ZodString>;
495
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
496
+ is(value: unknown): value is string | string[];
497
+ }>;
498
+ }, z.core.$loose> & {
499
+ is(value: unknown): value is {
500
+ [x: string]: unknown;
501
+ type: "link" | "add" | "update" | "remove";
502
+ target?: string | undefined;
503
+ description?: string | string[] | undefined;
504
+ };
505
+ }>>;
506
+ plan: z.ZodOptional<z.ZodArray<z.ZodObject<{
507
+ description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
508
+ is(value: unknown): value is string | string[];
509
+ };
510
+ done: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
511
+ }, z.core.$loose> & {
512
+ is(value: unknown): value is {
513
+ [x: string]: unknown;
514
+ description: string | string[];
515
+ done?: boolean | undefined;
516
+ };
517
+ }>>;
518
+ propagation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
519
+ includes: z.ZodOptional<z.ZodArray<z.ZodString>>;
520
+ input: z.ZodOptional<z.ZodString>;
521
+ output: z.ZodOptional<z.ZodString>;
522
+ external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
523
+ role: z.ZodEnum<{
524
+ output: "output";
525
+ input: "input";
526
+ context: "context";
527
+ evidence: "evidence";
528
+ source: "source";
529
+ standard: "standard";
530
+ prior_art: "prior_art";
531
+ }> & {
532
+ is(value: unknown): value is "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
533
+ };
534
+ identifier: z.ZodString;
535
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
536
+ is(value: unknown): value is string | string[];
537
+ }>;
538
+ node_id: z.ZodOptional<z.ZodString>;
539
+ internalised: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
540
+ is(value: unknown): value is string | string[];
541
+ }>;
542
+ }, z.core.$strip> & {
543
+ is(value: unknown): value is {
544
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
545
+ identifier: string;
546
+ description?: string | string[] | undefined;
547
+ node_id?: string | undefined;
548
+ internalised?: string | string[] | undefined;
549
+ };
550
+ }>>;
551
+ readonly subsystem: z.ZodOptional<typeof SysProMDocumentSchema>;
552
+ }, z.core.$loose>;
415
553
  declare const NodeSchema: z.ZodObject<{
416
554
  id: z.ZodString;
417
555
  type: z.ZodEnum<{
@@ -250,7 +250,8 @@ const SysProMDocumentSchema = z
250
250
  title: "SysProM: System Provenance Model",
251
251
  description: "JSON Schema for SysProM — a recursive, decision-driven model for recording system provenance.",
252
252
  });
253
- const NodeSchema = z
253
+ /** Base node object schema without ID-prefix refinement. Supports .partial(). */
254
+ export const NodeBase = z
254
255
  .looseObject({
255
256
  id: z.string().describe("Unique identifier for this node."),
256
257
  type: NodeType,
@@ -308,6 +309,19 @@ const NodeSchema = z
308
309
  },
309
310
  })
310
311
  .describe("A uniquely identifiable entity within the system.");
312
+ const NodeSchema = NodeBase.superRefine((node, ctx) => {
313
+ const prefix = NODE_ID_PREFIX[node.type];
314
+ if (!prefix)
315
+ return; // Unknown type — skip validation
316
+ const pattern = new RegExp(`^${prefix}\\d+(-[A-Z][A-Z0-9_]*)*$`);
317
+ if (!pattern.test(node.id)) {
318
+ ctx.addIssue({
319
+ code: z.ZodIssueCode.custom,
320
+ path: ["id"],
321
+ message: `Node ID "${node.id}" does not match required pattern for type "${node.type}": expected ${prefix}<number> with optional -SUFFIX segments (e.g. ${prefix}1, ${prefix}1-MY-LABEL)`,
322
+ });
323
+ }
324
+ });
311
325
  // Attach .is() type guards after both schemas are declared
312
326
  /**
313
327
  * Zod schema for a complete SysProM document — the root container holding
@@ -344,13 +358,13 @@ export const NODE_FILE_MAP = {
344
358
  };
345
359
  /** Conventional ID prefix for each node type. */
346
360
  export const NODE_ID_PREFIX = {
347
- intent: "I",
348
- concept: "CN",
349
- capability: "CP",
350
- element: "EL",
351
- realisation: "R",
361
+ intent: "INT",
362
+ concept: "CON",
363
+ capability: "CAP",
364
+ element: "ELEM",
365
+ realisation: "REAL",
352
366
  invariant: "INV",
353
- principle: "PR",
367
+ principle: "PRIN",
354
368
  policy: "POL",
355
369
  protocol: "PROT",
356
370
  stage: "STG",
@@ -358,11 +372,11 @@ export const NODE_ID_PREFIX = {
358
372
  gate: "GATE",
359
373
  mode: "MODE",
360
374
  artefact: "ART",
361
- artefact_flow: "AF",
362
- decision: "D",
363
- change: "CH",
364
- view: "V",
365
- milestone: "MS",
375
+ artefact_flow: "FLOW",
376
+ decision: "DEC",
377
+ change: "CHG",
378
+ view: "VIEW",
379
+ milestone: "MILE",
366
380
  version: "VER",
367
381
  };
368
382
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sysprom",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "SysProM — System Provenance Model CLI and library",
5
5
  "author": "ExaDev",
6
6
  "homepage": "https://exadev.github.io/SysProM",