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 +91 -31
- package/dist/src/cli/commands/infer.d.ts +2 -0
- package/dist/src/cli/commands/infer.js +206 -0
- package/dist/src/cli/commands/init.js +12 -4
- package/dist/src/cli/program.js +2 -0
- package/dist/src/cli/shared.d.ts +12 -7
- package/dist/src/cli/shared.js +60 -12
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/json-to-md.js +71 -16
- package/dist/src/mcp/server.js +91 -13
- package/dist/src/md-to-json.js +8 -4
- package/dist/src/operations/index.d.ts +4 -0
- package/dist/src/operations/index.js +5 -0
- package/dist/src/operations/infer-completeness.d.ts +414 -0
- package/dist/src/operations/infer-completeness.js +131 -0
- package/dist/src/operations/infer-derived.d.ts +375 -0
- package/dist/src/operations/infer-derived.js +158 -0
- package/dist/src/operations/infer-impact.d.ts +1246 -0
- package/dist/src/operations/infer-impact.js +144 -0
- package/dist/src/operations/infer-lifecycle.d.ts +421 -0
- package/dist/src/operations/infer-lifecycle.js +119 -0
- package/package.json +1 -1
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
|
-
|
|
27
|
-
|
|
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 .
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
+
sysprom remove INV23
|
|
49
49
|
|
|
50
50
|
# Update nodes, relationships, and metadata
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 `.
|
|
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(".
|
|
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
|
|
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 (`.
|
|
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
|
-
`.
|
|
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 `./.
|
|
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
|
-
|
|
283
|
+
sysprom add decision --id D23 --name "My Decision" --context "Why this was needed"
|
|
224
284
|
|
|
225
|
-
# Or edit ./.
|
|
226
|
-
|
|
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
|
-
|
|
293
|
+
sysprom json2md --input .SysProM.json --output ./.SysProM
|
|
234
294
|
|
|
235
295
|
# Markdown → JSON
|
|
236
|
-
|
|
296
|
+
sysprom md2json --input ./.SysProM --output .SysProM.json
|
|
237
297
|
```
|
|
238
298
|
|
|
239
|
-
> **Important:** Always keep `.
|
|
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,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: ".
|
|
19
|
-
md: ".
|
|
20
|
-
dir: ".
|
|
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}${
|
|
42
|
+
outputPath: `${resolved}${suffix}`,
|
|
35
43
|
ioFormat: formatToIoFormat(fmt),
|
|
36
44
|
};
|
|
37
45
|
}
|
package/dist/src/cli/program.js
CHANGED
|
@@ -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);
|
package/dist/src/cli/shared.d.ts
CHANGED
|
@@ -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. .
|
|
10
|
-
* 4. .
|
|
11
|
-
* 7.
|
|
12
|
-
* 10. *.
|
|
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
|
-
*
|
|
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 ".
|
|
21
|
-
* resolveInput("my-doc.
|
|
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;
|
package/dist/src/cli/shared.js
CHANGED
|
@@ -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. .
|
|
23
|
-
* 4. .
|
|
24
|
-
* 7.
|
|
25
|
-
* 10. *.
|
|
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
|
-
*
|
|
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 ".
|
|
34
|
-
* resolveInput("my-doc.
|
|
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(".
|
|
53
|
-
|
|
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
|
-
|
|
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) => {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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";
|