sysprom 1.0.6 → 1.1.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.
Files changed (115) hide show
  1. package/README.md +29 -28
  2. package/dist/src/canonical-json.d.ts +2 -0
  3. package/dist/src/cli/commands/add.d.ts +1 -1
  4. package/dist/src/cli/commands/add.js +2 -3
  5. package/dist/src/cli/commands/check.d.ts +6 -8
  6. package/dist/src/cli/commands/check.js +3 -8
  7. package/dist/src/cli/commands/graph.d.ts +3 -4
  8. package/dist/src/cli/commands/graph.js +3 -7
  9. package/dist/src/cli/commands/init.d.ts +15 -1
  10. package/dist/src/cli/commands/init.js +57 -27
  11. package/dist/src/cli/commands/plan.js +21 -39
  12. package/dist/src/cli/commands/query.js +10 -28
  13. package/dist/src/cli/commands/remove.d.ts +1 -1
  14. package/dist/src/cli/commands/remove.js +2 -3
  15. package/dist/src/cli/commands/rename.d.ts +1 -1
  16. package/dist/src/cli/commands/rename.js +2 -3
  17. package/dist/src/cli/commands/search.d.ts +1 -1
  18. package/dist/src/cli/commands/search.js +2 -3
  19. package/dist/src/cli/commands/stats.d.ts +6 -8
  20. package/dist/src/cli/commands/stats.js +3 -8
  21. package/dist/src/cli/commands/task.js +19 -24
  22. package/dist/src/cli/commands/update.js +6 -14
  23. package/dist/src/cli/commands/validate.d.ts +6 -8
  24. package/dist/src/cli/commands/validate.js +3 -8
  25. package/dist/src/cli/shared.d.ts +14 -3
  26. package/dist/src/cli/shared.js +61 -3
  27. package/dist/src/index.d.ts +1 -33
  28. package/dist/src/index.js +5 -82
  29. package/dist/src/io.d.ts +5 -0
  30. package/dist/src/json-to-md.d.ts +1 -0
  31. package/dist/src/operations/add-node.d.ts +5 -0
  32. package/dist/src/operations/add-node.js +5 -0
  33. package/dist/src/operations/add-plan-task.d.ts +5 -0
  34. package/dist/src/operations/add-plan-task.js +5 -0
  35. package/dist/src/operations/add-relationship.d.ts +5 -0
  36. package/dist/src/operations/add-relationship.js +5 -0
  37. package/dist/src/operations/check.d.ts +5 -0
  38. package/dist/src/operations/check.js +5 -0
  39. package/dist/src/operations/define-operation.d.ts +31 -0
  40. package/dist/src/operations/define-operation.js +8 -0
  41. package/dist/src/operations/graph.d.ts +1 -0
  42. package/dist/src/operations/graph.js +1 -0
  43. package/dist/src/operations/init-document.d.ts +1 -0
  44. package/dist/src/operations/init-document.js +1 -0
  45. package/dist/src/operations/json-to-markdown.d.ts +1 -0
  46. package/dist/src/operations/json-to-markdown.js +1 -0
  47. package/dist/src/operations/mark-task-done.d.ts +5 -0
  48. package/dist/src/operations/mark-task-done.js +5 -0
  49. package/dist/src/operations/mark-task-undone.d.ts +5 -0
  50. package/dist/src/operations/mark-task-undone.js +5 -0
  51. package/dist/src/operations/markdown-to-json.d.ts +1 -0
  52. package/dist/src/operations/markdown-to-json.js +1 -0
  53. package/dist/src/operations/next-id.d.ts +6 -0
  54. package/dist/src/operations/next-id.js +6 -0
  55. package/dist/src/operations/node-history.d.ts +3 -0
  56. package/dist/src/operations/node-history.js +2 -0
  57. package/dist/src/operations/plan-add-task.d.ts +1 -0
  58. package/dist/src/operations/plan-add-task.js +1 -0
  59. package/dist/src/operations/plan-gate.d.ts +4 -0
  60. package/dist/src/operations/plan-gate.js +2 -0
  61. package/dist/src/operations/plan-init.d.ts +1 -0
  62. package/dist/src/operations/plan-init.js +1 -0
  63. package/dist/src/operations/plan-progress.d.ts +2 -0
  64. package/dist/src/operations/plan-progress.js +1 -0
  65. package/dist/src/operations/plan-status.d.ts +2 -0
  66. package/dist/src/operations/plan-status.js +1 -0
  67. package/dist/src/operations/query-node.d.ts +3 -0
  68. package/dist/src/operations/query-node.js +2 -0
  69. package/dist/src/operations/query-nodes.d.ts +1 -0
  70. package/dist/src/operations/query-nodes.js +1 -0
  71. package/dist/src/operations/query-relationships.d.ts +1 -0
  72. package/dist/src/operations/query-relationships.js +1 -0
  73. package/dist/src/operations/remove-node.d.ts +8 -0
  74. package/dist/src/operations/remove-node.js +7 -0
  75. package/dist/src/operations/remove-relationship.d.ts +5 -0
  76. package/dist/src/operations/remove-relationship.js +5 -0
  77. package/dist/src/operations/rename.d.ts +7 -0
  78. package/dist/src/operations/rename.js +7 -0
  79. package/dist/src/operations/search.d.ts +1 -0
  80. package/dist/src/operations/search.js +1 -0
  81. package/dist/src/operations/speckit-diff.d.ts +3 -0
  82. package/dist/src/operations/speckit-diff.js +2 -0
  83. package/dist/src/operations/speckit-export.d.ts +1 -0
  84. package/dist/src/operations/speckit-export.js +1 -0
  85. package/dist/src/operations/speckit-import.d.ts +1 -0
  86. package/dist/src/operations/speckit-import.js +1 -0
  87. package/dist/src/operations/speckit-sync.d.ts +3 -0
  88. package/dist/src/operations/speckit-sync.js +2 -0
  89. package/dist/src/operations/state-at.d.ts +3 -0
  90. package/dist/src/operations/state-at.js +2 -0
  91. package/dist/src/operations/stats.d.ts +3 -0
  92. package/dist/src/operations/stats.js +2 -0
  93. package/dist/src/operations/task-list.d.ts +6 -0
  94. package/dist/src/operations/task-list.js +6 -0
  95. package/dist/src/operations/timeline.d.ts +3 -0
  96. package/dist/src/operations/timeline.js +2 -0
  97. package/dist/src/operations/trace-from-node.d.ts +3 -0
  98. package/dist/src/operations/trace-from-node.js +2 -0
  99. package/dist/src/operations/update-metadata.d.ts +1 -0
  100. package/dist/src/operations/update-metadata.js +1 -0
  101. package/dist/src/operations/update-node.d.ts +5 -0
  102. package/dist/src/operations/update-node.js +5 -0
  103. package/dist/src/operations/update-plan-task.d.ts +5 -0
  104. package/dist/src/operations/update-plan-task.js +5 -0
  105. package/dist/src/operations/validate.d.ts +10 -0
  106. package/dist/src/operations/validate.js +9 -0
  107. package/dist/src/schema.d.ts +44 -0
  108. package/dist/src/schema.js +31 -0
  109. package/dist/src/speckit/generate.d.ts +6 -0
  110. package/dist/src/speckit/generate.js +6 -0
  111. package/dist/src/speckit/parse.d.ts +9 -0
  112. package/dist/src/speckit/parse.js +6 -0
  113. package/dist/src/speckit/plan.d.ts +5 -0
  114. package/dist/src/speckit/project.d.ts +6 -0
  115. package/package.json +1 -1
package/README.md CHANGED
@@ -23,38 +23,38 @@ Both `sysprom` and `spm` are available as commands.
23
23
 
24
24
  ```sh
25
25
  # Convert between formats
26
- spm json2md sysprom.spm.json ./SysProM
27
- spm md2json ./SysProM output.spm.json
26
+ spm json2md .spm.json ./.spm
27
+ spm md2json ./.spm output.spm.json
28
28
 
29
- # Validate and summarise
30
- spm validate sysprom.spm.json
31
- spm stats sysprom.spm.json
29
+ # Validate and summarise (auto-detects .spm.json in current directory)
30
+ spm validate
31
+ spm stats
32
32
 
33
33
  # Query nodes and relationships
34
- spm query nodes sysprom.spm.json --type decision
35
- spm query node sysprom.spm.json D1
36
- spm query rels sysprom.spm.json --from D1
37
- spm query trace sysprom.spm.json I1
38
- spm query timeline sysprom.spm.json
39
- spm query state-at sysprom.spm.json --time 2026-03-22
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
40
40
 
41
41
  # Add nodes (ID auto-generated from type prefix if --id omitted)
42
- spm add sysprom.spm.json invariant --name "New Rule" --description "Must hold"
43
- spm add sysprom.spm.json decision --name "Choose X" \
42
+ spm add invariant --name "New Rule" --description "Must hold"
43
+ spm 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 sysprom.spm.json INV23
48
+ spm remove INV23
49
49
 
50
50
  # Update nodes, relationships, and metadata
51
- spm update node sysprom.spm.json D1 --status deprecated
52
- spm update add-rel sysprom.spm.json D1 affects EL5
53
- spm update remove-rel sysprom.spm.json D1 affects EL5
54
- spm update meta sysprom.spm.json --meta version=2
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
55
55
  ```
56
56
 
57
- All commands auto-detect format — they work on `.spm.json` files, `.spm.md` files, and multi-document folders.
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.
58
58
 
59
59
  ## Programmatic API
60
60
 
@@ -101,7 +101,7 @@ import {
101
101
  } from "sysprom";
102
102
 
103
103
  // Validate
104
- const doc = JSON.parse(fs.readFileSync("sysprom.spm.json", "utf8"));
104
+ const doc = JSON.parse(fs.readFileSync(".spm.json", "utf8"));
105
105
  const result = validate(doc);
106
106
  console.log(result.valid, result.issues);
107
107
 
@@ -139,6 +139,7 @@ SysProM models systems as directed graphs across abstraction layers — intent,
139
139
  | [RFC Processes](https://www.rfc-editor.org/rfc/rfc2026) | ✅ | | | ✅ | 🔶 | | | | | | | | 🔶 | 🔶 |
140
140
  | [Ralplan](https://github.com/yeachan-heo/oh-my-claudecode/blob/main/skills/ralplan/SKILL.md) | ✅ | | 🔶 | ✅ | | 🔶 | | | | | | | ✅ | 🔶 |
141
141
  | [GSD](https://github.com/gsd-build/get-shit-done) | ✅ | | | 🔶 | | 🔶 | | | | | | | | |
142
+ | [Superpowers](https://github.com/obra/superpowers) | ✅ | 🔶 | 🔶 | 🔶 | 🔶 | ✅ | 🔶 | | ✅ | 🔶 | | ✅ | ✅ | ✅ |
142
143
  | **SysProM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🔶 | | 🔶 | ✅ | ✅ | ✅ | ✅ |
143
144
 
144
145
  ✅ = first-class support. 🔶 = partial or implicit.
@@ -182,26 +183,26 @@ pnpm spm <command> # Run the CLI from source (e.g. pnpm spm validate ...)
182
183
 
183
184
  ## Self-Description
184
185
 
185
- `sysprom.spm.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.
186
+ `.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.
186
187
 
187
- 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:
188
+ 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:
188
189
 
189
190
  ```sh
190
191
  # Add a decision via the CLI
191
- spm add sysprom.spm.json decision --id D23 --name "My Decision" --context "Why this was needed"
192
+ spm add decision --id D23 --name "My Decision" --context "Why this was needed"
192
193
 
193
- # Or edit ./SysProM/DECISIONS.md directly, then sync
194
- spm md2json ./SysProM sysprom.spm.json
194
+ # Or edit ./.spm/DECISIONS.md directly, then sync
195
+ spm md2json ./.spm .spm.json
195
196
  ```
196
197
 
197
198
  Keep both representations in sync after any change:
198
199
 
199
200
  ```sh
200
201
  # JSON → Markdown
201
- spm json2md sysprom.spm.json ./SysProM
202
+ spm json2md .spm.json ./.spm
202
203
 
203
204
  # Markdown → JSON
204
- spm md2json ./SysProM sysprom.spm.json
205
+ spm md2json ./.spm .spm.json
205
206
  ```
206
207
 
207
- > **Important:** Always keep `sysprom.spm.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 `spm validate sysprom.spm.json` before committing.
208
+ > **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.
@@ -10,7 +10,9 @@
10
10
  * - null, true, false as literals
11
11
  * - undefined values omitted from objects
12
12
  */
13
+ /** Options for controlling canonical JSON output formatting. */
13
14
  export interface FormatOptions {
15
+ /** Indentation string (e.g. `"\t"` or `" "`). Empty string produces compact output. */
14
16
  indent?: string;
15
17
  }
16
18
  /**
@@ -1,10 +1,10 @@
1
1
  import * as z from "zod";
2
2
  import type { CommandDef } from "../define-command.js";
3
3
  declare const argsSchema: z.ZodObject<{
4
- input: z.ZodString;
5
4
  nodeType: z.ZodString;
6
5
  }, z.core.$strip>;
7
6
  declare const optsSchema: z.ZodObject<{
7
+ path: z.ZodOptional<z.ZodString>;
8
8
  json: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
9
9
  dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
10
10
  sync: z.ZodOptional<z.ZodString>;
@@ -1,9 +1,8 @@
1
1
  import * as z from "zod";
2
2
  import { NodeType, NodeStatus } from "../../schema.js";
3
3
  import { addNodeOp, nextIdOp } from "../../operations/index.js";
4
- import { inputArg, mutationOpts, loadDoc, persistDoc } from "../shared.js";
4
+ import { mutationOpts, loadDoc, persistDoc } from "../shared.js";
5
5
  const argsSchema = z.object({
6
- input: inputArg,
7
6
  nodeType: z.string().describe("Node type to add"),
8
7
  });
9
8
  const optsSchema = mutationOpts.extend({
@@ -31,7 +30,7 @@ export const addCommand = {
31
30
  console.error("--name is required.");
32
31
  process.exit(1);
33
32
  }
34
- const loaded = loadDoc(args.input);
33
+ const loaded = loadDoc(opts.path);
35
34
  const { doc } = loaded;
36
35
  const type = args.nodeType;
37
36
  if (!NodeType.is(type)) {
@@ -1,10 +1,8 @@
1
- import * as z from "zod";
2
1
  import type { CommandDef } from "../define-command.js";
3
- declare const argsSchema: z.ZodObject<{
4
- input: z.ZodString;
5
- }, z.core.$strip>;
6
- declare const optsSchema: z.ZodObject<{
7
- json: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
8
- }, z.core.$strip>;
9
- export declare const checkCommand: CommandDef<typeof argsSchema, typeof optsSchema>;
2
+ import { noArgs } from "../shared.js";
3
+ declare const optsSchema: import("zod").ZodObject<{
4
+ path: import("zod").ZodOptional<import("zod").ZodString>;
5
+ json: import("zod").ZodDefault<import("zod").ZodOptional<import("zod").ZodBoolean>>;
6
+ }, import("zod/v4/core").$strip>;
7
+ export declare const checkCommand: CommandDef<typeof noArgs, typeof optsSchema>;
10
8
  export {};
@@ -1,18 +1,13 @@
1
- import * as z from "zod";
2
1
  import { checkOp } from "../../operations/index.js";
3
- import { inputArg, readOpts, loadDoc } from "../shared.js";
4
- const argsSchema = z.object({
5
- input: inputArg,
6
- });
2
+ import { readOpts, loadDoc } from "../shared.js";
7
3
  const optsSchema = readOpts;
8
4
  export const checkCommand = {
9
5
  name: "check",
10
6
  description: checkOp.def.description,
11
7
  apiLink: checkOp.def.name,
12
- args: argsSchema,
13
8
  opts: optsSchema,
14
- action(args, opts) {
15
- const { doc } = loadDoc(args.input);
9
+ action(_args, opts) {
10
+ const { doc } = loadDoc(opts.path);
16
11
  const result = checkOp({ doc });
17
12
  if (opts.json) {
18
13
  console.log(JSON.stringify(result, null, 2));
@@ -1,9 +1,8 @@
1
1
  import * as z from "zod";
2
2
  import type { CommandDef } from "../define-command.js";
3
- declare const argsSchema: z.ZodObject<{
4
- input: z.ZodString;
5
- }, z.core.$strip>;
3
+ import { noArgs } from "../shared.js";
6
4
  declare const optsSchema: z.ZodObject<{
5
+ path: z.ZodOptional<z.ZodString>;
7
6
  json: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
8
7
  format: z.ZodOptional<z.ZodEnum<{
9
8
  dot: "dot";
@@ -11,5 +10,5 @@ declare const optsSchema: z.ZodObject<{
11
10
  }>>;
12
11
  type: z.ZodOptional<z.ZodString>;
13
12
  }, z.core.$strip>;
14
- export declare const graphCommand: CommandDef<typeof argsSchema, typeof optsSchema>;
13
+ export declare const graphCommand: CommandDef<typeof noArgs, typeof optsSchema>;
15
14
  export {};
@@ -1,9 +1,6 @@
1
1
  import * as z from "zod";
2
2
  import { graphOp } from "../../operations/index.js";
3
- import { inputArg, readOpts, loadDoc } from "../shared.js";
4
- const argsSchema = z.object({
5
- input: inputArg,
6
- });
3
+ import { readOpts, loadDoc } from "../shared.js";
7
4
  const optsSchema = readOpts.extend({
8
5
  format: z.enum(["mermaid", "dot"]).optional().describe("Output format"),
9
6
  type: z.string().optional().describe("Filter by relationship type"),
@@ -12,11 +9,10 @@ export const graphCommand = {
12
9
  name: "graph",
13
10
  description: graphOp.def.description,
14
11
  apiLink: graphOp.def.name,
15
- args: argsSchema,
16
12
  opts: optsSchema,
17
- action(args, opts) {
13
+ action(_args, opts) {
18
14
  try {
19
- const { doc } = loadDoc(args.input);
15
+ const { doc } = loadDoc(opts.path);
20
16
  const output = graphOp({
21
17
  doc,
22
18
  format: opts.format ?? "mermaid",
@@ -1,2 +1,16 @@
1
+ import * as z from "zod";
1
2
  import type { CommandDef } from "../define-command.js";
2
- export declare const initCommand: CommandDef;
3
+ declare const argsSchema: z.ZodObject<{
4
+ path: z.ZodDefault<z.ZodOptional<z.ZodString>>;
5
+ }, z.core.$strip>;
6
+ declare const optsSchema: z.ZodObject<{
7
+ title: z.ZodOptional<z.ZodString>;
8
+ scope: z.ZodOptional<z.ZodString>;
9
+ format: z.ZodOptional<z.ZodEnum<{
10
+ dir: "dir";
11
+ json: "json";
12
+ md: "md";
13
+ }>>;
14
+ }, z.core.$strict>;
15
+ export declare const initCommand: CommandDef<typeof argsSchema, typeof optsSchema>;
16
+ export {};
@@ -1,44 +1,74 @@
1
1
  import * as z from "zod";
2
- import { writeFileSync, existsSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { canonicalise } from "../../canonical-json.js";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { resolve, join } from "node:path";
5
4
  import { initDocumentOp } from "../../operations/index.js";
6
- function isArgs(arg) {
7
- return typeof arg === "object" && arg !== null && "output" in arg;
5
+ import { saveDocument } from "../../io.js";
6
+ const formatChoices = ["json", "md", "dir"];
7
+ function formatToIoFormat(fmt) {
8
+ switch (fmt) {
9
+ case "json":
10
+ return "json";
11
+ case "md":
12
+ return "single-md";
13
+ case "dir":
14
+ return "multi-md";
15
+ }
8
16
  }
9
- function isOpts(opt) {
10
- return typeof opt === "object" && opt !== null;
17
+ const suffixMap = {
18
+ json: ".spm.json",
19
+ md: ".spm.md",
20
+ dir: ".spm",
21
+ };
22
+ function resolveInitTarget(pathArg, format) {
23
+ const resolved = resolve(pathArg);
24
+ const isExistingDir = existsSync(resolved) && statSync(resolved).isDirectory();
25
+ if (isExistingDir) {
26
+ const fmt = format ?? "json";
27
+ return {
28
+ outputPath: join(resolved, suffixMap[fmt]),
29
+ ioFormat: formatToIoFormat(fmt),
30
+ };
31
+ }
32
+ const fmt = format ?? "dir";
33
+ return {
34
+ outputPath: `${resolved}${suffixMap[fmt]}`,
35
+ ioFormat: formatToIoFormat(fmt),
36
+ };
11
37
  }
38
+ const argsSchema = z.object({
39
+ path: z
40
+ .string()
41
+ .optional()
42
+ .default(".")
43
+ .describe("Target path (default: current directory)"),
44
+ });
45
+ const optsSchema = z
46
+ .object({
47
+ title: z.string().optional().describe("Document title"),
48
+ scope: z.string().optional().describe("Document scope"),
49
+ format: z
50
+ .enum(formatChoices)
51
+ .optional()
52
+ .describe("Output format: json, md, or dir"),
53
+ })
54
+ .strict();
12
55
  export const initCommand = {
13
56
  name: "init",
14
57
  description: initDocumentOp.def.description,
15
58
  apiLink: initDocumentOp.def.name,
16
- args: z.object({
17
- output: z.string().describe("Output file path"),
18
- }),
19
- opts: z
20
- .object({
21
- title: z.string().optional().describe("Document title"),
22
- scope: z.string().optional().describe("Document scope"),
23
- })
24
- .strict(),
59
+ args: argsSchema,
60
+ opts: optsSchema,
25
61
  action(args, opts) {
26
- if (!isArgs(args))
27
- throw new Error("Invalid args");
28
- if (!isOpts(opts))
29
- throw new Error("Invalid opts");
30
- const typedArgs = args;
31
- const typedOpts = opts;
32
- const outputPath = resolve(typedArgs.output);
62
+ const { outputPath, ioFormat } = resolveInitTarget(args.path, opts.format);
33
63
  if (existsSync(outputPath)) {
34
- console.error(`File already exists: ${outputPath}`);
64
+ console.error(`Already exists: ${outputPath}`);
35
65
  process.exit(1);
36
66
  }
37
67
  const doc = initDocumentOp({
38
- title: typedOpts.title ?? "Untitled",
39
- scope: typedOpts.scope ?? "system",
68
+ title: opts.title ?? "Untitled",
69
+ scope: opts.scope ?? "system",
40
70
  });
41
- writeFileSync(outputPath, canonicalise(doc, { indent: "\t" }) + "\n");
71
+ saveDocument(doc, ioFormat, outputPath);
42
72
  console.log(`Created ${outputPath}`);
43
73
  },
44
74
  };
@@ -1,6 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { existsSync } from "node:fs";
3
- import { loadDocument, saveDocument } from "../../io.js";
3
+ import { saveDocument } from "../../io.js";
4
+ import { loadDoc, mutationOpts, persistDoc } from "../shared.js";
4
5
  import { planInitOp, planAddTaskOp, planStatusOp, planProgressOp, planGateOp, } from "../../operations/index.js";
5
6
  // ============================================================================
6
7
  // Subcommands
@@ -37,10 +38,7 @@ const initSubcommand = {
37
38
  }
38
39
  },
39
40
  };
40
- const addTaskArgs = z.object({
41
- input: z.string().describe("Path to SysProM document"),
42
- });
43
- const addTaskOpts = z.object({
41
+ const addTaskOpts = mutationOpts.pick({ path: true }).extend({
44
42
  prefix: z.string().describe("Plan prefix"),
45
43
  name: z.string().optional().describe("Task name"),
46
44
  parent: z.string().optional().describe("Parent task ID"),
@@ -49,17 +47,20 @@ const addTaskSubcommand = {
49
47
  name: "add-task",
50
48
  description: planAddTaskOp.def.description,
51
49
  apiLink: planAddTaskOp.def.name,
52
- args: addTaskArgs,
53
50
  opts: addTaskOpts,
54
- action(args, opts) {
55
- const inputPath = args.input;
51
+ action(_args, opts) {
56
52
  const prefix = opts.prefix;
57
53
  const name = opts.name;
58
54
  const parentId = opts.parent;
59
55
  try {
60
- const { doc, format, path } = loadDocument(inputPath);
61
- const newDoc = planAddTaskOp({ doc, prefix, name, parent: parentId });
62
- saveDocument(newDoc, format, path);
56
+ const loaded = loadDoc(opts.path);
57
+ const newDoc = planAddTaskOp({
58
+ doc: loaded.doc,
59
+ prefix,
60
+ name,
61
+ parent: parentId,
62
+ });
63
+ persistDoc(newDoc, loaded, { ...opts, json: false, dryRun: false });
63
64
  const target = parentId ? `to ${parentId}` : `to ${prefix}-PROT-IMPL`;
64
65
  console.log(`Added task ${target}`);
65
66
  }
@@ -69,10 +70,7 @@ const addTaskSubcommand = {
69
70
  }
70
71
  },
71
72
  };
72
- const statusArgs = z.object({
73
- input: z.string().describe("Path to SysProM document"),
74
- });
75
- const statusOpts = z.object({
73
+ const statusOpts = mutationOpts.pick({ path: true }).extend({
76
74
  prefix: z.string().describe("Plan prefix"),
77
75
  json: z.boolean().optional().describe("Output as JSON"),
78
76
  });
@@ -80,20 +78,17 @@ const statusSubcommand = {
80
78
  name: "status",
81
79
  description: planStatusOp.def.description,
82
80
  apiLink: planStatusOp.def.name,
83
- args: statusArgs,
84
81
  opts: statusOpts,
85
- action(args, opts) {
86
- const inputPath = args.input;
82
+ action(_args, opts) {
87
83
  const prefix = opts.prefix;
88
84
  const asJson = opts.json === true;
89
85
  try {
90
- const { doc } = loadDocument(inputPath);
86
+ const { doc } = loadDoc(opts.path);
91
87
  const status = planStatusOp({ doc, prefix });
92
88
  if (asJson) {
93
89
  console.log(JSON.stringify(status, null, 2));
94
90
  return;
95
91
  }
96
- // Format: Constitution, Spec, Plan, Tasks, Checklist status report
97
92
  const formatBoolean = (defined) => defined ? "✅ defined" : "❌ not defined";
98
93
  console.log(`Constitution: ${formatBoolean(status.constitution.defined)} (${String(status.constitution.principleCount)} principles)`);
99
94
  console.log(`Spec: ${formatBoolean(status.spec.defined)} (${String(status.spec.userStoryCount)} user stories)`);
@@ -109,10 +104,7 @@ const statusSubcommand = {
109
104
  }
110
105
  },
111
106
  };
112
- const progressArgs = z.object({
113
- input: z.string().describe("Path to SysProM document"),
114
- });
115
- const progressOpts = z.object({
107
+ const progressOpts = mutationOpts.pick({ path: true }).extend({
116
108
  prefix: z.string().describe("Plan prefix"),
117
109
  json: z.boolean().optional().describe("Output as JSON"),
118
110
  });
@@ -120,21 +112,17 @@ const progressSubcommand = {
120
112
  name: "progress",
121
113
  description: planProgressOp.def.description,
122
114
  apiLink: planProgressOp.def.name,
123
- args: progressArgs,
124
115
  opts: progressOpts,
125
- action(args, opts) {
126
- const inputPath = args.input;
116
+ action(_args, opts) {
127
117
  const prefix = opts.prefix;
128
118
  const asJson = opts.json === true;
129
119
  try {
130
- const { doc } = loadDocument(inputPath);
120
+ const { doc } = loadDoc(opts.path);
131
121
  const progress = planProgressOp({ doc, prefix });
132
122
  if (asJson) {
133
123
  console.log(JSON.stringify(progress, null, 2));
134
124
  return;
135
125
  }
136
- // Format: ASCII progress bars
137
- // Bar width: 10 chars. Filled: █, empty: ░. Name padded to 20 chars. Percent right-aligned to 3 chars.
138
126
  for (const phase of progress) {
139
127
  const filledCount = Math.round((phase.percent / 100) * 10);
140
128
  const emptyCount = 10 - filledCount;
@@ -151,10 +139,7 @@ const progressSubcommand = {
151
139
  }
152
140
  },
153
141
  };
154
- const gateArgs = z.object({
155
- input: z.string().describe("Path to SysProM document"),
156
- });
157
- const gateOpts = z.object({
142
+ const gateOpts = mutationOpts.pick({ path: true }).extend({
158
143
  prefix: z.string().describe("Plan prefix"),
159
144
  phase: z.string().describe("Phase number"),
160
145
  json: z.boolean().optional().describe("Output as JSON"),
@@ -163,10 +148,8 @@ const gateSubcommand = {
163
148
  name: "gate",
164
149
  description: planGateOp.def.description,
165
150
  apiLink: planGateOp.def.name,
166
- args: gateArgs,
167
151
  opts: gateOpts,
168
- action(args, opts) {
169
- const inputPath = args.input;
152
+ action(_args, opts) {
170
153
  const prefix = opts.prefix;
171
154
  const phaseNum = parseInt(opts.phase, 10);
172
155
  const asJson = opts.json === true;
@@ -175,13 +158,12 @@ const gateSubcommand = {
175
158
  process.exit(1);
176
159
  }
177
160
  try {
178
- const { doc } = loadDocument(inputPath);
161
+ const { doc } = loadDoc(opts.path);
179
162
  const result = planGateOp({ doc, prefix, phase: phaseNum });
180
163
  if (asJson) {
181
164
  console.log(JSON.stringify(result, null, 2));
182
165
  return;
183
166
  }
184
- // Format: Gate check result with detailed issues
185
167
  if (result.ready) {
186
168
  console.log(`Gate check for phase ${String(phaseNum)}: ✅ READY`);
187
169
  }
@@ -1,7 +1,7 @@
1
1
  import pc from "picocolors";
2
2
  import * as z from "zod";
3
3
  import { textToString } from "../../text.js";
4
- import { inputArg, readOpts, loadDoc } from "../shared.js";
4
+ import { readOpts, loadDoc } from "../shared.js";
5
5
  import { queryNodesOp, queryNodeOp, queryRelationshipsOp, traceFromNodeOp, timelineOp, nodeHistoryOp, stateAtOp, } from "../../operations/index.js";
6
6
  import { NodeType, NodeStatus } from "../../schema.js";
7
7
  // ---------------------------------------------------------------------------
@@ -65,39 +65,27 @@ function printTraceTree(tn, depth) {
65
65
  // ---------------------------------------------------------------------------
66
66
  // Arg/opt schemas
67
67
  // ---------------------------------------------------------------------------
68
- const nodesArgs = z.object({
69
- input: inputArg,
70
- });
71
68
  const nodesOpts = readOpts.extend({
72
69
  type: NodeType.optional().describe("filter by node type"),
73
70
  status: NodeStatus.optional().describe("filter by node status"),
74
71
  });
75
72
  const nodeArgs = z.object({
76
- input: inputArg,
77
73
  id: z.string().describe("node ID to retrieve"),
78
74
  });
79
75
  const nodeOpts = readOpts;
80
- const relsArgs = z.object({
81
- input: inputArg,
82
- });
83
76
  const relsOpts = readOpts.extend({
84
77
  from: z.string().optional().describe("filter relationships by source node"),
85
78
  to: z.string().optional().describe("filter relationships by target node"),
86
79
  type: z.string().optional().describe("filter by relationship type"),
87
80
  });
88
81
  const traceArgs = z.object({
89
- input: inputArg,
90
82
  id: z.string().describe("node ID to start trace from"),
91
83
  });
92
84
  const traceOpts = readOpts;
93
- const timelineArgs = z.object({
94
- input: inputArg,
95
- });
96
85
  const timelineOpts = readOpts.extend({
97
86
  node: z.string().optional().describe("filter events to a specific node"),
98
87
  });
99
88
  const stateAtArgs = z.object({
100
- input: inputArg,
101
89
  time: z.string().describe("ISO timestamp to query"),
102
90
  });
103
91
  const stateAtOpts = readOpts;
@@ -108,12 +96,10 @@ const nodesSubcommand = {
108
96
  name: "nodes",
109
97
  description: queryNodesOp.def.description,
110
98
  apiLink: queryNodesOp.def.name,
111
- args: nodesArgs,
112
99
  opts: nodesOpts,
113
- action(rawArgs, rawOpts) {
114
- const args = nodesArgs.parse(rawArgs);
100
+ action(_rawArgs, rawOpts) {
115
101
  const opts = nodesOpts.parse(rawOpts);
116
- const { doc } = loadDoc(args.input);
102
+ const { doc } = loadDoc(opts.path);
117
103
  const nodes = queryNodesOp({ doc, type: opts.type, status: opts.status });
118
104
  if (opts.json) {
119
105
  console.log(JSON.stringify(nodes, null, 2));
@@ -134,7 +120,7 @@ const nodeSubcommand = {
134
120
  action(rawArgs, rawOpts) {
135
121
  const args = nodeArgs.parse(rawArgs);
136
122
  const opts = nodeOpts.parse(rawOpts);
137
- const { doc } = loadDoc(args.input);
123
+ const { doc } = loadDoc(opts.path);
138
124
  const result = queryNodeOp({ doc, id: args.id });
139
125
  if (!result) {
140
126
  console.error(`Node not found: ${args.id}`);
@@ -162,12 +148,10 @@ const relsSubcommand = {
162
148
  name: "rels",
163
149
  description: queryRelationshipsOp.def.description,
164
150
  apiLink: queryRelationshipsOp.def.name,
165
- args: relsArgs,
166
151
  opts: relsOpts,
167
- action(rawArgs, rawOpts) {
168
- const args = relsArgs.parse(rawArgs);
152
+ action(_rawArgs, rawOpts) {
169
153
  const opts = relsOpts.parse(rawOpts);
170
- const { doc } = loadDoc(args.input);
154
+ const { doc } = loadDoc(opts.path);
171
155
  const rels = queryRelationshipsOp({
172
156
  doc,
173
157
  from: opts.from,
@@ -194,7 +178,7 @@ const traceSubcommand = {
194
178
  action(rawArgs, rawOpts) {
195
179
  const args = traceArgs.parse(rawArgs);
196
180
  const opts = traceOpts.parse(rawOpts);
197
- const { doc } = loadDoc(args.input);
181
+ const { doc } = loadDoc(opts.path);
198
182
  const trace = traceFromNodeOp({ doc, startId: args.id });
199
183
  if (opts.json) {
200
184
  console.log(JSON.stringify(trace, null, 2));
@@ -208,12 +192,10 @@ const timelineSubcommand = {
208
192
  name: "timeline",
209
193
  description: timelineOp.def.description,
210
194
  apiLink: timelineOp.def.name,
211
- args: timelineArgs,
212
195
  opts: timelineOpts,
213
- action(rawArgs, rawOpts) {
214
- const args = timelineArgs.parse(rawArgs);
196
+ action(_rawArgs, rawOpts) {
215
197
  const opts = timelineOpts.parse(rawOpts);
216
- const { doc } = loadDoc(args.input);
198
+ const { doc } = loadDoc(opts.path);
217
199
  const events = opts.node
218
200
  ? nodeHistoryOp({ doc, nodeId: opts.node })
219
201
  : timelineOp({ doc });
@@ -241,7 +223,7 @@ const stateAtSubcommand = {
241
223
  action(rawArgs, rawOpts) {
242
224
  const args = stateAtArgs.parse(rawArgs);
243
225
  const opts = stateAtOpts.parse(rawOpts);
244
- const { doc } = loadDoc(args.input);
226
+ const { doc } = loadDoc(opts.path);
245
227
  const result = stateAtOp({ doc, timestamp: args.time });
246
228
  if (opts.json) {
247
229
  console.log(JSON.stringify(result, null, 2));
@@ -1,10 +1,10 @@
1
1
  import * as z from "zod";
2
2
  import type { CommandDef } from "../define-command.js";
3
3
  declare const argsSchema: z.ZodObject<{
4
- input: z.ZodString;
5
4
  nodeId: z.ZodString;
6
5
  }, z.core.$strip>;
7
6
  declare const optsSchema: z.ZodObject<{
7
+ path: z.ZodOptional<z.ZodString>;
8
8
  json: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
9
9
  dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
10
10
  sync: z.ZodOptional<z.ZodString>;