Skip to content

Commit db706ec

Browse files
committed
fix(dx): include valid options in error messages
Show available types/statuses/roles when validation fails in CLI commands, MCP server, markdown parser, and relationship operations.
1 parent 3c07cce commit db706ec

6 files changed

Lines changed: 41 additions & 40 deletions

File tree

src/cli/commands/add.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export const addCommand: CommandDef<typeof argsSchema, typeof optsSchema> = {
5959
const type = args.nodeType;
6060

6161
if (!NodeType.is(type)) {
62-
console.error(`Unknown node type: ${type}`);
62+
console.error(
63+
`Unknown node type: "${type}". Valid types: ${NodeType.options.join(", ")}`,
64+
);
6365
process.exit(1);
6466
}
6567

@@ -74,7 +76,9 @@ export const addCommand: CommandDef<typeof argsSchema, typeof optsSchema> = {
7476

7577
if (opts.status) {
7678
if (!NodeStatus.is(opts.status)) {
77-
console.error(`Unknown status: ${opts.status}`);
79+
console.error(
80+
`Unknown status: "${opts.status}". Valid statuses: ${NodeStatus.options.join(", ")}`,
81+
);
7882
process.exit(1);
7983
}
8084
node.status = opts.status;

src/cli/define-command.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,17 @@ export function buildCommander(def: CommandDef, parent: Command): Command {
152152
if (!field || !isZodField(field)) continue;
153153
const desc = fieldDescription(field);
154154
const choices = fieldChoices(field);
155+
const optional = fieldIsOptional(field);
155156
const flagName = camelToKebab(key);
156157

157158
if (choices) {
158-
cmd.addArgument(new Argument(`<${flagName}>`, desc).choices(choices));
159+
const arg = new Argument(
160+
optional ? `[${flagName}]` : `<${flagName}>`,
161+
desc,
162+
).choices(choices);
163+
cmd.addArgument(arg);
159164
} else {
160-
cmd.argument(`<${flagName}>`, desc);
165+
cmd.argument(optional ? `[${flagName}]` : `<${flagName}>`, desc);
161166
}
162167
}
163168
}

src/mcp/server.ts

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
55
import * as z from "zod";
66
import { loadDocument } from "../io.js";
7-
import { RelationshipType } from "../schema.js";
7+
import { NodeType, RelationshipType } from "../schema.js";
88
import {
99
validateOp,
1010
statsOp,
@@ -197,31 +197,11 @@ server.registerTool(
197197
},
198198
({ path, type, id, name, description }) => {
199199
const { doc } = loadDocument(path);
200-
const nodeType = z
201-
.enum([
202-
"intent",
203-
"concept",
204-
"capability",
205-
"element",
206-
"realisation",
207-
"invariant",
208-
"principle",
209-
"policy",
210-
"protocol",
211-
"stage",
212-
"role",
213-
"gate",
214-
"mode",
215-
"artefact",
216-
"artefact_flow",
217-
"decision",
218-
"change",
219-
"view",
220-
"version",
221-
])
222-
.safeParse(type);
200+
const nodeType = NodeType.safeParse(type);
223201
if (!nodeType.success) {
224-
throw new Error(`Invalid node type: ${type}`);
202+
throw new Error(
203+
`Invalid node type: "${type}". Valid types: ${NodeType.options.join(", ")}`,
204+
);
225205
}
226206
const nodeId = id ?? nextIdOp({ doc, type: nodeType.data });
227207
const updated = addNodeOp({
@@ -360,7 +340,9 @@ server.registerTool(
360340
const { doc } = loadDocument(path);
361341
const relType = RelationshipType.safeParse(type);
362342
if (!relType.success) {
363-
throw new Error(`Invalid relationship type: ${type}`);
343+
throw new Error(
344+
`Invalid relationship type: "${type}". Valid types: ${RelationshipType.options.join(", ")}`,
345+
);
364346
}
365347
const updated = addRelationshipOp({
366348
doc,
@@ -404,7 +386,9 @@ server.registerTool(
404386
const { doc } = loadDocument(path);
405387
const relType = RelationshipType.safeParse(type);
406388
if (!relType.success) {
407-
throw new Error(`Invalid relationship type: ${type}`);
389+
throw new Error(
390+
`Invalid relationship type: "${type}". Valid types: ${RelationshipType.options.join(", ")}`,
391+
);
408392
}
409393
const result = removeRelationshipOp({
410394
doc,

src/md-to-json.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,25 @@ const operationType = z.enum(["add", "update", "remove", "link"]);
2525

2626
function parseNodeType(s: string): NodeType {
2727
const result = NodeType.safeParse(s);
28-
if (!result.success) throw new Error(`Unknown node type: ${s}`);
28+
if (!result.success) throw new Error(`Unknown node type: "${s}". Valid types: ${NodeType.options.join(", ")}`);
2929
return result.data;
3030
}
3131

3232
function parseRelType(s: string): RelationshipType {
3333
const result = RelationshipType.safeParse(s);
34-
if (!result.success) throw new Error(`Unknown relationship type: ${s}`);
34+
if (!result.success) throw new Error(`Unknown relationship type: "${s}". Valid types: ${RelationshipType.options.join(", ")}`);
3535
return result.data;
3636
}
3737

3838
function parseNodeStatus(s: string): NodeStatus {
3939
const result = NodeStatus.safeParse(s);
40-
if (!result.success) throw new Error(`Unknown node status: ${s}`);
40+
if (!result.success) throw new Error(`Unknown node status: "${s}". Valid statuses: ${NodeStatus.options.join(", ")}`);
4141
return result.data;
4242
}
4343

4444
function parseExtRefRole(s: string): ExternalReferenceRole {
4545
const result = ExternalReferenceRole.safeParse(s);
46-
if (!result.success) throw new Error(`Unknown external reference role: ${s}`);
46+
if (!result.success) throw new Error(`Unknown external reference role: "${s}". Valid roles: ${ExternalReferenceRole.options.join(", ")}`);
4747
return result.data;
4848
}
4949

@@ -349,7 +349,7 @@ function parseNodeFromSection(
349349
const rawType = parts[0];
350350
const parsed = operationType.safeParse(rawType);
351351
if (!parsed.success) {
352-
throw new Error(`Unknown operation type: ${rawType}`);
352+
throw new Error(`Unknown operation type: "${rawType}". Valid types: ${operationType.options.join(", ")}`);
353353
}
354354
const type = parsed.data;
355355
const rest = parts.slice(1);

src/operations/add-relationship.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as z from "zod";
22
import { defineOperation } from "./define-operation.js";
33
import { SysProMDocument, Relationship } from "../schema.js";
4-
import { isValidEndpointPair } from "../endpoint-types.js";
4+
import {
5+
isValidEndpointPair,
6+
RELATIONSHIP_ENDPOINT_TYPES,
7+
} from "../endpoint-types.js";
58

69
/**
710
* Add a relationship to a SysProM document. Returns a new document with the relationship appended.
@@ -30,8 +33,9 @@ export const addRelationshipOp = defineOperation({
3033

3134
// Validate endpoint types for this relationship
3235
if (!isValidEndpointPair(rel.type, fromNode.type, toNode.type)) {
36+
const endpoints = RELATIONSHIP_ENDPOINT_TYPES[rel.type];
3337
throw new Error(
34-
`Invalid endpoint types for ${rel.type}: ${fromNode.type}${toNode.type}`,
38+
`Invalid endpoint types for ${rel.type}: ${fromNode.type}${toNode.type}. Valid: [${endpoints.from.join(", ")}] → [${endpoints.to.join(", ")}]`,
3539
);
3640
}
3741

src/operations/validate.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as z from "zod";
22
import { defineOperation } from "./define-operation.js";
33
import { SysProMDocument } from "../schema.js";
4-
import { isValidEndpointPair } from "../endpoint-types.js";
4+
import {
5+
isValidEndpointPair,
6+
RELATIONSHIP_ENDPOINT_TYPES,
7+
} from "../endpoint-types.js";
58

69
/** Zod schema for the result of validating a SysProM document. */
710
export const ValidationResult = z.object({
@@ -80,8 +83,9 @@ export const validateOp = defineOperation({
8083
toNode &&
8184
!isValidEndpointPair(r.type, fromNode.type, toNode.type)
8285
) {
86+
const endpoints = RELATIONSHIP_ENDPOINT_TYPES[r.type];
8387
issues.push(
84-
`Invalid endpoint types for ${r.type}: ${fromNode.type}${toNode.type} (${r.from}${r.to})`,
88+
`Invalid endpoint types for ${r.type}: ${fromNode.type}${toNode.type} (${r.from}${r.to}). Valid: [${endpoints.from.join(", ")}] → [${endpoints.to.join(", ")}]`,
8589
);
8690
}
8791
}

0 commit comments

Comments
 (0)