sysprom 1.13.2 → 1.15.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 CHANGED
@@ -17,44 +17,98 @@ npm install -g github:ExaDev/SysProM
17
17
  npx sysprom --help
18
18
  ```
19
19
 
20
- Both `sysprom` and `spm` are available as commands.
20
+ Both `sysprom` and `spm` are available as commands — use `sysprom` for new projects.
21
21
 
22
22
  ## CLI
23
23
 
24
24
  ```sh
25
25
  # Convert between formats
26
- spm json2md --input .spm.json --output ./.spm
27
- spm md2json --input ./.spm --output output.spm.json
26
+ sysprom json2md --input .SysProM.json --output ./.SysProM
27
+ sysprom md2json --input ./.SysProM --output output.SysProM.json
28
28
 
29
- # Validate and summarise (auto-detects .spm.json in current directory)
30
- spm validate
31
- spm stats
29
+ # Validate and summarise (auto-detects .SysProM.json in current directory)
30
+ sysprom validate
31
+ sysprom stats
32
32
 
33
33
  # Query nodes and relationships
34
- spm query nodes --type decision
35
- spm query node D1
36
- spm query rels --from D1
37
- spm query trace I1
38
- spm query timeline
39
- spm query state-at 2026-03-22
34
+ sysprom query nodes --type decision
35
+ sysprom query node D1
36
+ sysprom query rels --from D1
37
+ sysprom query trace I1
38
+ sysprom query timeline
39
+ sysprom query state-at 2026-03-22
40
40
 
41
41
  # Add nodes (ID auto-generated from type prefix if --id omitted)
42
- spm add invariant --name "New Rule" --description "Must hold"
43
- spm add decision --name "Choose X" \
42
+ sysprom add invariant --name "New Rule" --description "Must hold"
43
+ sysprom add decision --name "Choose X" \
44
44
  --option "OPT-A:Use framework X" --option "OPT-B:Use framework Y" \
45
45
  --selected OPT-A --rationale "Lower migration effort"
46
46
 
47
47
  # Remove nodes
48
- spm remove INV23
48
+ sysprom remove INV23
49
49
 
50
50
  # Update nodes, relationships, and metadata
51
- spm update node D1 --status deprecated
52
- spm update add-rel D1 affects EL5
53
- spm update remove-rel D1 affects EL5
54
- spm update meta --fields version=2
51
+ sysprom update node D1 --status deprecated
52
+ sysprom update add-rel D1 affects EL5
53
+ sysprom update remove-rel D1 affects EL5
54
+ sysprom update meta --fields version=2
55
+
56
+ # Inference operations (deterministic graph analysis)
57
+ sysprom infer completeness # Score node completeness (0-1)
58
+ sysprom infer lifecycle # Infer lifecycle phases
59
+ sysprom infer impact I1 # Trace impact from node
60
+ sysprom infer derived # Compute transitive closure
61
+ sysprom infer all # Run all analyses
55
62
  ```
56
63
 
57
- All commands auto-detect the document — they search the current directory for `.spm.json`, `.spm.md`, or `.spm/` (in that priority order), then fall back to `*.spm.json`, `*.spm.md`, or `*.spm/`. Use `--path` to specify an explicit path.
64
+ All commands auto-detect the document — they search the current directory for `.SysProM.json`, `.SysProM.md`, or `.SysProM/` (in that priority order), then fall back to `.spm.json`, `.spm.md`, or `.spm/`. Use `--path` to specify an explicit path. Note: `spm` is an alias for `sysprom` for backwards compatibility.
65
+
66
+ ## MCP Server
67
+
68
+ SysProM includes an MCP (Model Context Protocol) server exposing 15 tools over stdio transport. Any MCP-compatible agent — Cursor, Windsurf, VS Code Copilot, Cline, or custom clients — can use it.
69
+
70
+ ### Configuration
71
+
72
+ Add the following to your MCP client's configuration (e.g. `.cursor/mcp.json`, `.vscode/mcp.json`, `cline_mcp_settings.json`, or equivalent):
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "sysprom": {
78
+ "command": "npx",
79
+ "args": ["-y", "sysprom", "mcp"]
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ Or via the CLI subcommand (equivalent):
86
+
87
+ ```sh
88
+ sysprom mcp # starts the MCP server on stdio
89
+ ```
90
+
91
+ ### Available Tools
92
+
93
+ | Tool | Description |
94
+ |------|-------------|
95
+ | `validate` | Validate a SysProM document and return issues |
96
+ | `stats` | Return document statistics |
97
+ | `query-nodes` | Query nodes by type, status, or text |
98
+ | `query-node` | Retrieve a single node by ID |
99
+ | `query-relationships` | Query relationships by source, target, or type |
100
+ | `trace` | Trace refinement chains from a node |
101
+ | `add-node` | Add a new node to the document |
102
+ | `remove-node` | Remove a node by ID |
103
+ | `update-node` | Update fields on an existing node |
104
+ | `add-relationship` | Add a relationship between nodes |
105
+ | `remove-relationship` | Remove a relationship |
106
+ | `infer-completeness` | Score node completeness (0-1) based on refinement relationships |
107
+ | `infer-lifecycle` | Infer lifecycle phase from status and lifecycle fields |
108
+ | `infer-impact` | Trace impact propagation from a starting node |
109
+ | `infer-derived` | Compute transitive closure and inverse relationships |
110
+
111
+ All tools accept a `path` parameter to specify the SysProM document location.
58
112
 
59
113
  ## Programmatic API
60
114
 
@@ -91,6 +145,12 @@ import {
91
145
  removeRelationship,
92
146
  updateMetadata,
93
147
 
148
+ // Inference
149
+ inferCompletenessOp,
150
+ inferLifecycleOp,
151
+ inferImpactOp,
152
+ inferDerivedOp,
153
+
94
154
  // File I/O
95
155
  loadDocument,
96
156
  saveDocument,
@@ -101,7 +161,7 @@ import {
101
161
  } from "sysprom";
102
162
 
103
163
  // Validate
104
- const doc = JSON.parse(fs.readFileSync(".spm.json", "utf8"));
164
+ const doc = JSON.parse(fs.readFileSync(".SysProM.json", "utf8"));
105
165
  const result = validate(doc);
106
166
  console.log(result.valid, result.issues);
107
167
 
@@ -169,7 +229,7 @@ SysProM models systems as directed graphs across abstraction layers — intent,
169
229
  <tr><td><a href="https://github.com/Priivacy-ai/spec-kitty">Spec Kitty</a></td><td>✅</td><td>🔶</td><td>✅</td><td>🔶</td><td></td><td>🔶</td><td>🔶</td><td>🔶</td><td></td><td></td><td>🔶</td><td>✅</td><td>✅</td><td>✅</td></tr>
170
230
  <tr><td><a href="https://github.com/shotgun-sh/shotgun">Shotgun</a></td><td>✅</td><td>🔶</td><td>🔶</td><td></td><td></td><td>🔶</td><td></td><td>🔶</td><td></td><td></td><td>🔶</td><td>🔶</td><td>✅</td><td>🔶</td></tr>
171
231
  <tr><td><a href="https://github.com/obra/superpowers">Superpowers</a></td><td>✅</td><td>🔶</td><td>🔶</td><td>🔶</td><td></td><td>🔶</td><td>✅</td><td>🔶</td><td></td><td>✅</td><td>🔶</td><td>✅</td><td>✅</td><td>✅</td></tr>
172
- <tr><td><strong>SysProM</strong></td><td>✅</td><td>✅</td><td>✅</td><td>✅</td><td>🔶</td><td>✅</td><td>✅</td><td>✅</td><td>✅</td><td></td><td>🔶</td><td>✅</td><td>✅</td><td>✅</td></tr>
232
+ <tr><td><strong>SysProM</strong></td><td>✅</td><td>✅</td><td>✅</td><td>✅</td><td>🔶</td><td>✅</td><td>✅</td><td>✅</td><td>✅</td><td>✅</td><td>🔶</td><td>✅</td><td>✅</td><td>✅</td></tr>
173
233
  </tbody>
174
234
  </table>
175
235
 
@@ -194,7 +254,7 @@ SysProM models systems as directed graphs across abstraction layers — intent,
194
254
  SysProM is format-agnostic. This repository includes:
195
255
 
196
256
  - **JSON** — validated against `schema.json`, supports recursive subsystems
197
- - **Markdown** — single file (`.spm.md`), multi-document folder, or recursive nested folders with automatic grouping by type
257
+ - **Markdown** — single file (`.SysProM.md`), multi-document folder, or recursive nested folders with automatic grouping by type
198
258
 
199
259
  Round-trip conversion between JSON and Markdown is supported with zero information loss.
200
260
 
@@ -214,29 +274,29 @@ pnpm spm <command> # Run the CLI from source (e.g. pnpm spm validate ...)
214
274
 
215
275
  ## Self-Description
216
276
 
217
- `.spm.json` is SysProM describing itself — the specification, its decisions, invariants, changes, and worked examples are all encoded as a SysProM document. The `./.spm/` folder contains the same content as human-readable Markdown.
277
+ `.SysProM.json` is SysProM describing itself — the specification, its decisions, invariants, changes, and worked examples are all encoded as a SysProM document. The `./.SysProM/` folder contains the same content as human-readable Markdown.
218
278
 
219
- All significant activity — decisions, changes, new capabilities, and invariants — should be recorded in the self-describing document. Updates can be made either by editing the Markdown files in `./.spm/` directly or by using the CLI:
279
+ All significant activity — decisions, changes, new capabilities, and invariants — should be recorded in the self-describing document. Updates can be made either by editing the Markdown files in `./.SysProM/` directly or by using the CLI:
220
280
 
221
281
  ```sh
222
282
  # Add a decision via the CLI
223
- spm add decision --id D23 --name "My Decision" --context "Why this was needed"
283
+ sysprom add decision --id D23 --name "My Decision" --context "Why this was needed"
224
284
 
225
- # Or edit ./.spm/DECISIONS.md directly, then sync
226
- spm md2json --input ./.spm --output .spm.json
285
+ # Or edit ./.SysProM/DECISIONS.md directly, then sync
286
+ sysprom md2json --input ./.SysProM --output .SysProM.json
227
287
  ```
228
288
 
229
289
  Keep both representations in sync after any change:
230
290
 
231
291
  ```sh
232
292
  # JSON → Markdown
233
- spm json2md --input .spm.json --output ./.spm
293
+ sysprom json2md --input .SysProM.json --output ./.SysProM
234
294
 
235
295
  # Markdown → JSON
236
- spm md2json --input ./.spm --output .spm.json
296
+ sysprom md2json --input ./.SysProM --output .SysProM.json
237
297
  ```
238
298
 
239
- > **Important:** Always keep `.spm.json` and `./.spm/` up to date with current activity and in sync with each other. Record all decisions, changes, and new capabilities as they happen. After any change to either representation, run the appropriate conversion command above. Validate with `spm validate` before committing.
299
+ > **Important:** Always keep `.SysProM.json` and `./.SysProM/` up to date with current activity and in sync with each other. Record all decisions, changes, and new capabilities as they happen. After any change to either representation, run the appropriate conversion command above. Validate with `sysprom validate` before committing.
240
300
 
241
301
  ## Claude Code Plugin
242
302
 
@@ -0,0 +1,2 @@
1
+ import type { CommandDef } from "../define-command.js";
2
+ export declare const inferCommand: CommandDef;
@@ -0,0 +1,206 @@
1
+ import pc from "picocolors";
2
+ import * as z from "zod";
3
+ import { readOpts, loadDoc } from "../shared.js";
4
+ import { inferCompletenessOp, inferLifecycleOp, inferImpactOp, inferDerivedOp, } from "../../operations/index.js";
5
+ // ---------------------------------------------------------------------------
6
+ // Presentation helpers
7
+ // ---------------------------------------------------------------------------
8
+ function printCompletenessNode(r) {
9
+ const scoreColour = r.score === 1 ? pc.green : r.score >= 0.5 ? pc.yellow : pc.red;
10
+ console.log(`${pc.cyan(r.id.padEnd(12))} ${pc.dim(r.type.padEnd(16))} ${pc.bold(r.name)} ${scoreColour(`[${(r.score * 100).toFixed(0)}%]`)}`);
11
+ for (const issue of r.issues) {
12
+ console.log(` ${pc.dim("•")} ${pc.red(issue)}`);
13
+ }
14
+ }
15
+ function printLifecycleNode(r) {
16
+ const phaseColours = {
17
+ early: pc.blue,
18
+ middle: pc.yellow,
19
+ late: pc.green,
20
+ terminal: pc.red,
21
+ unknown: pc.dim,
22
+ };
23
+ const colour = phaseColours[r.inferredPhase] ?? pc.dim;
24
+ console.log(`${pc.cyan(r.id.padEnd(12))} ${pc.dim(r.type.padEnd(16))} ${pc.bold(r.name)} ${colour(`[${r.inferredPhase}]`)} ${pc.dim(r.inferredState)}`);
25
+ }
26
+ function printImpactNode(r) {
27
+ const typeColours = {
28
+ direct: pc.red,
29
+ transitive: pc.yellow,
30
+ potential: pc.blue,
31
+ };
32
+ const colour = typeColours[r.impactType] ?? pc.dim;
33
+ const nodeName = r.node ? r.node.name : "(unknown)";
34
+ const indent = " ".repeat(r.distance);
35
+ console.log(`${indent}${pc.cyan(r.id)} ${pc.dim(`(${String(r.distance)})`)} ${colour(`[${r.impactType}]`)} ${pc.bold(nodeName)}`);
36
+ }
37
+ function printDerivedRelationship(r) {
38
+ const typeColours = {
39
+ transitive: pc.yellow,
40
+ composite: pc.blue,
41
+ inverse: pc.green,
42
+ };
43
+ const colour = typeColours[r.derivationType] ?? pc.dim;
44
+ console.log(`${pc.cyan(r.from.padEnd(12))} ${colour(r.type.padEnd(20))} ${pc.cyan(r.to)} ${pc.dim(`[${r.derivationType}]`)}`);
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Arg/opt schemas
48
+ // ---------------------------------------------------------------------------
49
+ const impactArgs = z.object({
50
+ id: z.string().describe("node ID to start impact analysis from"),
51
+ });
52
+ // ---------------------------------------------------------------------------
53
+ // Subcommands
54
+ // ---------------------------------------------------------------------------
55
+ const completenessSubcommand = {
56
+ name: "completeness",
57
+ description: inferCompletenessOp.def.description,
58
+ apiLink: inferCompletenessOp.def.name,
59
+ opts: readOpts,
60
+ action(_rawArgs, rawOpts) {
61
+ const opts = readOpts.parse(rawOpts);
62
+ const { doc } = loadDoc(opts.path);
63
+ const result = inferCompletenessOp({ doc });
64
+ if (opts.json) {
65
+ console.log(JSON.stringify(result, null, 2));
66
+ }
67
+ else {
68
+ console.log(pc.bold("\nCompleteness Analysis\n") +
69
+ pc.dim(`Average score: ${(result.averageScore * 100).toFixed(1)}% | `) +
70
+ pc.green(`${String(result.completeNodes)} complete`) +
71
+ pc.dim(" | ") +
72
+ pc.red(`${String(result.incompleteNodes)} incomplete`) +
73
+ "\n");
74
+ // Show incomplete nodes first
75
+ const incomplete = result.nodes.filter((n) => n.score < 1);
76
+ const complete = result.nodes.filter((n) => n.score === 1);
77
+ if (incomplete.length > 0) {
78
+ console.log(pc.dim("Incomplete nodes:"));
79
+ for (const n of incomplete)
80
+ printCompletenessNode(n);
81
+ console.log();
82
+ }
83
+ console.log(pc.dim(`${String(complete.length)} fully complete nodes`));
84
+ }
85
+ },
86
+ };
87
+ const lifecycleSubcommand = {
88
+ name: "lifecycle",
89
+ description: inferLifecycleOp.def.description,
90
+ apiLink: inferLifecycleOp.def.name,
91
+ opts: readOpts,
92
+ action(_rawArgs, rawOpts) {
93
+ const opts = readOpts.parse(rawOpts);
94
+ const { doc } = loadDoc(opts.path);
95
+ const result = inferLifecycleOp({ doc });
96
+ if (opts.json) {
97
+ console.log(JSON.stringify(result, null, 2));
98
+ }
99
+ else {
100
+ console.log(pc.bold("\nLifecycle Analysis\n") +
101
+ pc.dim(`Early: ${String(result.summary.early)} | Middle: ${String(result.summary.middle)} | Late: ${String(result.summary.late)} | Terminal: ${String(result.summary.terminal)} | Unknown: ${String(result.summary.unknown)}`) +
102
+ "\n");
103
+ for (const n of result.nodes)
104
+ printLifecycleNode(n);
105
+ }
106
+ },
107
+ };
108
+ const impactSubcommand = {
109
+ name: "impact",
110
+ description: inferImpactOp.def.description,
111
+ apiLink: inferImpactOp.def.name,
112
+ args: impactArgs,
113
+ opts: readOpts,
114
+ action(rawArgs, rawOpts) {
115
+ const args = impactArgs.parse(rawArgs);
116
+ const opts = readOpts.parse(rawOpts);
117
+ const { doc } = loadDoc(opts.path);
118
+ const result = inferImpactOp({ doc, startId: args.id });
119
+ if (opts.json) {
120
+ console.log(JSON.stringify(result, null, 2));
121
+ }
122
+ else {
123
+ console.log(pc.bold(`\nImpact Analysis from ${args.id}\n`) +
124
+ pc.dim(`Direct: ${String(result.summary.direct)} | Transitive: ${String(result.summary.transitive)} | Potential: ${String(result.summary.potential)} | Total: ${String(result.summary.total)}`) +
125
+ "\n");
126
+ if (result.impactedNodes.length === 0) {
127
+ console.log(pc.dim("No impacted nodes found"));
128
+ }
129
+ else {
130
+ for (const n of result.impactedNodes)
131
+ printImpactNode(n);
132
+ }
133
+ }
134
+ },
135
+ };
136
+ const derivedSubcommand = {
137
+ name: "derived",
138
+ description: inferDerivedOp.def.description,
139
+ apiLink: inferDerivedOp.def.name,
140
+ opts: readOpts,
141
+ action(_rawArgs, rawOpts) {
142
+ const opts = readOpts.parse(rawOpts);
143
+ const { doc } = loadDoc(opts.path);
144
+ const result = inferDerivedOp({ doc });
145
+ if (opts.json) {
146
+ console.log(JSON.stringify(result, null, 2));
147
+ }
148
+ else {
149
+ console.log(pc.bold("\nDerived Relationships\n") +
150
+ pc.dim(`Transitive: ${String(result.summary.transitive)} | Composite: ${String(result.summary.composite)} | Inverse: ${String(result.summary.inverse)} | Total: ${String(result.summary.total)}`) +
151
+ "\n");
152
+ if (result.derivedRelationships.length === 0) {
153
+ console.log(pc.dim("No derived relationships found"));
154
+ }
155
+ else {
156
+ for (const r of result.derivedRelationships)
157
+ printDerivedRelationship(r);
158
+ }
159
+ }
160
+ },
161
+ };
162
+ const allSubcommand = {
163
+ name: "all",
164
+ description: "Run all inference analyses",
165
+ opts: readOpts,
166
+ action(_rawArgs, rawOpts) {
167
+ const opts = readOpts.parse(rawOpts);
168
+ const { doc } = loadDoc(opts.path);
169
+ // Completeness
170
+ console.log(pc.bold("\n=== Completeness ===\n"));
171
+ const completeness = inferCompletenessOp({ doc });
172
+ console.log(pc.dim(`Average score: ${(completeness.averageScore * 100).toFixed(1)}% | `) +
173
+ pc.green(`${String(completeness.completeNodes)} complete`) +
174
+ pc.dim(" | ") +
175
+ pc.red(`${String(completeness.incompleteNodes)} incomplete`));
176
+ // Lifecycle
177
+ console.log(pc.bold("\n=== Lifecycle ===\n"));
178
+ const lifecycle = inferLifecycleOp({ doc });
179
+ console.log(pc.dim(`Early: ${String(lifecycle.summary.early)} | Middle: ${String(lifecycle.summary.middle)} | Late: ${String(lifecycle.summary.late)} | Terminal: ${String(lifecycle.summary.terminal)} | Unknown: ${String(lifecycle.summary.unknown)}`));
180
+ // Derived
181
+ console.log(pc.bold("\n=== Derived Relationships ===\n"));
182
+ const derived = inferDerivedOp({ doc });
183
+ console.log(pc.dim(`Transitive: ${String(derived.summary.transitive)} | Composite: ${String(derived.summary.composite)} | Inverse: ${String(derived.summary.inverse)} | Total: ${String(derived.summary.total)}`));
184
+ if (opts.json) {
185
+ console.log(JSON.stringify({
186
+ completeness,
187
+ lifecycle,
188
+ derived,
189
+ }, null, 2));
190
+ }
191
+ },
192
+ };
193
+ // ---------------------------------------------------------------------------
194
+ // Main command
195
+ // ---------------------------------------------------------------------------
196
+ export const inferCommand = {
197
+ name: "infer",
198
+ description: "Infer completeness, lifecycle, impact, and derived relationships",
199
+ subcommands: [
200
+ completenessSubcommand,
201
+ lifecycleSubcommand,
202
+ impactSubcommand,
203
+ derivedSubcommand,
204
+ allSubcommand,
205
+ ],
206
+ };
@@ -15,9 +15,9 @@ function formatToIoFormat(fmt) {
15
15
  }
16
16
  }
17
17
  const suffixMap = {
18
- json: ".spm.json",
19
- md: ".spm.md",
20
- dir: ".spm",
18
+ json: ".SysProM.json",
19
+ md: ".SysProM.md",
20
+ dir: ".SysProM",
21
21
  };
22
22
  function resolveInitTarget(pathArg, format) {
23
23
  const resolved = resolve(pathArg);
@@ -30,8 +30,16 @@ function resolveInitTarget(pathArg, format) {
30
30
  };
31
31
  }
32
32
  const fmt = format ?? "dir";
33
+ const suffix = suffixMap[fmt];
34
+ // If path already ends with the correct suffix, use it as-is
35
+ if (resolved.endsWith(suffix)) {
36
+ return {
37
+ outputPath: resolved,
38
+ ioFormat: formatToIoFormat(fmt),
39
+ };
40
+ }
33
41
  return {
34
- outputPath: `${resolved}${suffixMap[fmt]}`,
42
+ outputPath: `${resolved}${suffix}`,
35
43
  ioFormat: formatToIoFormat(fmt),
36
44
  };
37
45
  }
@@ -43,6 +43,7 @@ import { speckitCommand } from "./commands/speckit.js";
43
43
  import { taskCommand } from "./commands/task.js";
44
44
  import { planCommand } from "./commands/plan.js";
45
45
  import { syncCommandDef } from "./commands/sync.js";
46
+ import { inferCommand } from "./commands/infer.js";
46
47
  export const program = new Command();
47
48
  program
48
49
  .name("sysprom")
@@ -71,6 +72,7 @@ export const commands = [
71
72
  taskCommand,
72
73
  planCommand,
73
74
  syncCommandDef,
75
+ inferCommand,
74
76
  ];
75
77
  for (const cmd of commands) {
76
78
  buildCommander(cmd, program);
@@ -6,19 +6,24 @@ export declare const noArgs: z.ZodObject<{}, z.core.$strict>;
6
6
  /**
7
7
  * Resolve a SysProM document path. If no explicit path is given, search the
8
8
  * working directory by priority:
9
- * 1. .spm.json 2. .spm.md 3. .spm/
10
- * 4. .sysprom.json 5. .sysprom.md 6. .sysprom/
11
- * 7. *.spm.json 8. *.spm.md 9. *.spm/
12
- * 10. *.sysprom.json 11. *.sysprom.md 12. *.sysprom/
9
+ * 1. .SysProM.json 2. .SysProM.md 3. .SysProM/
10
+ * 4. .spm.json 5. .spm.md 6. .spm/
11
+ * 7. .sysprom.json 8. .sysprom.md 9. .sysprom/
12
+ * 10. *.SysProM.json 11. *.SysProM.md 12. *.SysProM/
13
+ * 13. *.spm.json 14. *.spm.md 15. *.spm/
14
+ * 16. *.sysprom.json 17. *.sysprom.md 18. *.sysprom/
13
15
  *
14
- * All matching is case-insensitive. Glob tiers must have exactly one match.
16
+ * Matching is case-insensitive for base names and directory matches.
17
+ * Files like .SysProM.json, .sysprom.json, and .SYSPROM.JSON are treated
18
+ * as equivalent and all match the .SysProM.json priority tier.
19
+ * Glob tiers must have exactly one match.
15
20
  * @param input - Explicit document path, or undefined for auto-detection.
16
21
  * @param cwd - Working directory to search from (defaults to `.`).
17
22
  * @returns The resolved document path.
18
23
  * @example
19
24
  * ```ts
20
- * resolveInput() // => auto-detects ".spm.json" in cwd
21
- * resolveInput("my-doc.spm.json") // => "my-doc.spm.json"
25
+ * resolveInput() // => auto-detects ".SysProM.json" in cwd
26
+ * resolveInput("my-doc.SysProM.json") // => "my-doc.SysProM.json"
22
27
  * ```
23
28
  */
24
29
  export declare function resolveInput(input?: string, cwd?: string): string;
@@ -19,27 +19,34 @@ const pathOpt = z
19
19
  /**
20
20
  * Resolve a SysProM document path. If no explicit path is given, search the
21
21
  * working directory by priority:
22
- * 1. .spm.json 2. .spm.md 3. .spm/
23
- * 4. .sysprom.json 5. .sysprom.md 6. .sysprom/
24
- * 7. *.spm.json 8. *.spm.md 9. *.spm/
25
- * 10. *.sysprom.json 11. *.sysprom.md 12. *.sysprom/
22
+ * 1. .SysProM.json 2. .SysProM.md 3. .SysProM/
23
+ * 4. .spm.json 5. .spm.md 6. .spm/
24
+ * 7. .sysprom.json 8. .sysprom.md 9. .sysprom/
25
+ * 10. *.SysProM.json 11. *.SysProM.md 12. *.SysProM/
26
+ * 13. *.spm.json 14. *.spm.md 15. *.spm/
27
+ * 16. *.sysprom.json 17. *.sysprom.md 18. *.sysprom/
26
28
  *
27
- * All matching is case-insensitive. Glob tiers must have exactly one match.
29
+ * Matching is case-insensitive for base names and directory matches.
30
+ * Files like .SysProM.json, .sysprom.json, and .SYSPROM.JSON are treated
31
+ * as equivalent and all match the .SysProM.json priority tier.
32
+ * Glob tiers must have exactly one match.
28
33
  * @param input - Explicit document path, or undefined for auto-detection.
29
34
  * @param cwd - Working directory to search from (defaults to `.`).
30
35
  * @returns The resolved document path.
31
36
  * @example
32
37
  * ```ts
33
- * resolveInput() // => auto-detects ".spm.json" in cwd
34
- * resolveInput("my-doc.spm.json") // => "my-doc.spm.json"
38
+ * resolveInput() // => auto-detects ".SysProM.json" in cwd
39
+ * resolveInput("my-doc.SysProM.json") // => "my-doc.SysProM.json"
35
40
  * ```
36
41
  */
37
42
  export function resolveInput(input, cwd) {
38
43
  if (input)
39
44
  return input;
40
45
  const dir = resolve(cwd ?? ".");
41
- // Exact names to check, in priority order (case-insensitive)
42
46
  const exactNames = [
47
+ ".SysProM.json",
48
+ ".SysProM.md",
49
+ ".SysProM",
43
50
  ".spm.json",
44
51
  ".spm.md",
45
52
  ".spm",
@@ -48,9 +55,42 @@ export function resolveInput(input, cwd) {
48
55
  ".sysprom",
49
56
  ];
50
57
  const entries = readdirSync(dir);
58
+ // Phase 1: Try exact case-sensitive matches first
51
59
  for (const name of exactNames) {
52
- const isDirSuffix = name.endsWith(".spm") || name.endsWith(".sysprom");
53
- const found = entries.filter((e) => e.toLowerCase() === name);
60
+ const isDirSuffix = name.endsWith(".SysProM") ||
61
+ name.endsWith(".spm") ||
62
+ name.endsWith(".sysprom");
63
+ const found = entries.filter((e) => e === name);
64
+ if (found.length === 1) {
65
+ // Before returning, check for case-variant collisions on case-sensitive
66
+ // filesystems (e.g. both .spm.json and .SPM.json exist).
67
+ const nameLower = name.toLowerCase();
68
+ const caseVariants = entries.filter((e) => e !== name && e.toLowerCase() === nameLower);
69
+ if (caseVariants.length > 0) {
70
+ throw new Error(`Multiple SysProM documents found: ${[name, ...caseVariants].join(", ")}. Specify one explicitly.`);
71
+ }
72
+ const candidate = join(dir, found[0]);
73
+ if (isDirSuffix) {
74
+ try {
75
+ if (statSync(candidate).isDirectory())
76
+ return candidate;
77
+ }
78
+ catch {
79
+ /* skip */
80
+ }
81
+ }
82
+ else {
83
+ return candidate;
84
+ }
85
+ }
86
+ }
87
+ // Phase 2: Try case-insensitive matches (supports case variations like .SPM.JSON)
88
+ for (const name of exactNames) {
89
+ const isDirSuffix = name.endsWith(".SysProM") ||
90
+ name.endsWith(".spm") ||
91
+ name.endsWith(".sysprom");
92
+ const nameLower = name.toLowerCase();
93
+ const found = entries.filter((e) => e.toLowerCase() === nameLower);
54
94
  if (found.length > 1) {
55
95
  throw new Error(`Multiple SysProM documents found: ${found.join(", ")}. Specify one explicitly.`);
56
96
  }
@@ -72,6 +112,9 @@ export function resolveInput(input, cwd) {
72
112
  }
73
113
  // Glob suffixes in priority order (case-insensitive)
74
114
  const globSuffixes = [
115
+ ".SysProM.json",
116
+ ".SysProM.md",
117
+ ".SysProM",
75
118
  ".spm.json",
76
119
  ".spm.md",
77
120
  ".spm",
@@ -80,11 +123,16 @@ export function resolveInput(input, cwd) {
80
123
  ".sysprom",
81
124
  ];
82
125
  for (const suffix of globSuffixes) {
83
- const isDirSuffix = suffix === ".spm" || suffix === ".sysprom";
126
+ const isDirSuffix = suffix === ".SysProM" || suffix === ".spm" || suffix === ".sysprom";
127
+ const suffixLower = suffix.toLowerCase();
84
128
  const matches = entries
85
129
  .filter((e) => {
86
130
  const lower = e.toLowerCase();
87
- return lower.endsWith(suffix) && lower !== suffix;
131
+ // Match files ending with this suffix (case-insensitive) that don't start with "."
132
+ // and are not exact matches (those were already checked)
133
+ return (lower.endsWith(suffixLower) &&
134
+ lower !== suffixLower &&
135
+ !e.startsWith("."));
88
136
  })
89
137
  .map((e) => join(dir, e))
90
138
  .filter((p) => {
@@ -6,7 +6,7 @@
6
6
  * @packageDocumentation
7
7
  */
8
8
  export { SysProMDocument, Node, Relationship, NodeType, NodeStatus, RelationshipType, Text, Option, Operation, Task, ExternalReference, ExternalReferenceRole, Metadata, NODE_TYPE_LABELS, NODE_LABEL_TO_TYPE, RELATIONSHIP_TYPE_LABELS, RELATIONSHIP_LABEL_TO_TYPE, EXTERNAL_REFERENCE_ROLE_LABELS, EXTERNAL_REFERENCE_LABEL_TO_ROLE, NODE_STATUSES, NODE_FILE_MAP, NODE_ID_PREFIX, toJSONSchema, } from "./schema.js";
9
- export { defineOperation, type OperationDef, type DefinedOperation, addNodeOp, removeNodeOp, updateNodeOp, addRelationshipOp, removeRelationshipOp, updateMetadataOp, nextIdOp, initDocumentOp, addPlanTaskOp, updatePlanTaskOp, markTaskDoneOp, markTaskUndoneOp, taskListOp, planInitOp, planAddTaskOp, planStatusOp, planProgressOp, planGateOp, queryNodesOp, queryNodeOp, queryRelationshipsOp, traceFromNodeOp, timelineOp, nodeHistoryOp, stateAtOp, validateOp, statsOp, searchOp, checkOp, graphOp, renameOp, jsonToMarkdownOp, markdownToJsonOp, speckitImportOp, speckitExportOp, speckitSyncOp, speckitDiffOp, type RemoveResult, type ValidationResult, type DocumentStats, type NodeDetail, type TraceNode, type TimelineEvent, type NodeState, type PlanStatusResult, type PhaseProgressResult, type GateResultOutput, type SyncResult, type DiffResult, } from "./operations/index.js";
9
+ export { defineOperation, type OperationDef, type DefinedOperation, addNodeOp, removeNodeOp, updateNodeOp, addRelationshipOp, removeRelationshipOp, updateMetadataOp, nextIdOp, initDocumentOp, addPlanTaskOp, updatePlanTaskOp, markTaskDoneOp, markTaskUndoneOp, taskListOp, planInitOp, planAddTaskOp, planStatusOp, planProgressOp, planGateOp, queryNodesOp, queryNodeOp, queryRelationshipsOp, traceFromNodeOp, timelineOp, nodeHistoryOp, stateAtOp, validateOp, statsOp, searchOp, checkOp, graphOp, renameOp, jsonToMarkdownOp, markdownToJsonOp, speckitImportOp, speckitExportOp, speckitSyncOp, speckitDiffOp, inferCompletenessOp, inferLifecycleOp, inferImpactOp, inferDerivedOp, type RemoveResult, type ValidationResult, type DocumentStats, type NodeDetail, type TraceNode, type TimelineEvent, type NodeState, type PlanStatusResult, type PhaseProgressResult, type GateResultOutput, type SyncResult, type DiffResult, type CompletenessOutput, type LifecycleOutput, type ImpactOutput, type DerivedOutput, } from "./operations/index.js";
10
10
  export { jsonToMarkdownSingle, jsonToMarkdownMultiDoc, jsonToMarkdown, type ConvertOptions, } from "./json-to-md.js";
11
11
  export { markdownSingleToJson, markdownMultiDocToJson, markdownToJson, } from "./md-to-json.js";
12
12
  export { RELATIONSHIP_ENDPOINT_TYPES, isValidEndpointPair, } from "./endpoint-types.js";
package/dist/src/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // Schema types and validators
9
9
  export { SysProMDocument, Node, Relationship, NodeType, NodeStatus, RelationshipType, Text, Option, Operation, Task, ExternalReference, ExternalReferenceRole, Metadata, NODE_TYPE_LABELS, NODE_LABEL_TO_TYPE, RELATIONSHIP_TYPE_LABELS, RELATIONSHIP_LABEL_TO_TYPE, EXTERNAL_REFERENCE_ROLE_LABELS, EXTERNAL_REFERENCE_LABEL_TO_ROLE, NODE_STATUSES, NODE_FILE_MAP, NODE_ID_PREFIX, toJSONSchema, } from "./schema.js";
10
10
  // Operations (single source of truth for domain logic + metadata)
11
- export { defineOperation, addNodeOp, removeNodeOp, updateNodeOp, addRelationshipOp, removeRelationshipOp, updateMetadataOp, nextIdOp, initDocumentOp, addPlanTaskOp, updatePlanTaskOp, markTaskDoneOp, markTaskUndoneOp, taskListOp, planInitOp, planAddTaskOp, planStatusOp, planProgressOp, planGateOp, queryNodesOp, queryNodeOp, queryRelationshipsOp, traceFromNodeOp, timelineOp, nodeHistoryOp, stateAtOp, validateOp, statsOp, searchOp, checkOp, graphOp, renameOp, jsonToMarkdownOp, markdownToJsonOp, speckitImportOp, speckitExportOp, speckitSyncOp, speckitDiffOp, } from "./operations/index.js";
11
+ export { defineOperation, addNodeOp, removeNodeOp, updateNodeOp, addRelationshipOp, removeRelationshipOp, updateMetadataOp, nextIdOp, initDocumentOp, addPlanTaskOp, updatePlanTaskOp, markTaskDoneOp, markTaskUndoneOp, taskListOp, planInitOp, planAddTaskOp, planStatusOp, planProgressOp, planGateOp, queryNodesOp, queryNodeOp, queryRelationshipsOp, traceFromNodeOp, timelineOp, nodeHistoryOp, stateAtOp, validateOp, statsOp, searchOp, checkOp, graphOp, renameOp, jsonToMarkdownOp, markdownToJsonOp, speckitImportOp, speckitExportOp, speckitSyncOp, speckitDiffOp, inferCompletenessOp, inferLifecycleOp, inferImpactOp, inferDerivedOp, } from "./operations/index.js";
12
12
  // Conversion
13
13
  export { jsonToMarkdownSingle, jsonToMarkdownMultiDoc, jsonToMarkdown, } from "./json-to-md.js";
14
14
  export { markdownSingleToJson, markdownMultiDocToJson, markdownToJson, } from "./md-to-json.js";