sysprom 1.4.0 → 1.6.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.
@@ -7,9 +7,9 @@ declare const optsSchema: z.ZodObject<{
7
7
  title: z.ZodOptional<z.ZodString>;
8
8
  scope: z.ZodOptional<z.ZodString>;
9
9
  format: z.ZodOptional<z.ZodEnum<{
10
- dir: "dir";
11
10
  json: "json";
12
11
  md: "md";
12
+ dir: "dir";
13
13
  }>>;
14
14
  }, z.core.$strict>;
15
15
  export declare const initCommand: CommandDef<typeof argsSchema, typeof optsSchema>;
@@ -0,0 +1,18 @@
1
+ import type { CommandDef } from "../define-command.js";
2
+ import { type BidirectionalSyncResult, type ConflictStrategy } from "../../operations/index.js";
3
+ interface SyncCommandInput {
4
+ jsonPath: string;
5
+ mdPath: string;
6
+ strategy?: ConflictStrategy;
7
+ dryRun?: boolean;
8
+ }
9
+ /**
10
+ * Synchronise JSON and Markdown representations of a SysProM document.
11
+ * @param input - Configuration for sync operation
12
+ * @returns Result of the synchronisation
13
+ * @example
14
+ * const result = syncCommand({ jsonPath: "doc.spm.json", mdPath: "doc.spm.md" });
15
+ */
16
+ export declare function syncCommand(input: SyncCommandInput): BidirectionalSyncResult;
17
+ export declare const syncCommandDef: CommandDef;
18
+ export {};
@@ -0,0 +1,112 @@
1
+ import * as z from "zod";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { syncDocumentsOp, } from "../../operations/index.js";
5
+ import { detectChanges } from "../../sync.js";
6
+ import { markdownToJson } from "../../md-to-json.js";
7
+ import { jsonToMarkdownSingle } from "../../json-to-md.js";
8
+ import { canonicalise } from "../../canonical-json.js";
9
+ import { SysProMDocument } from "../../schema.js";
10
+ /**
11
+ * Synchronise JSON and Markdown representations of a SysProM document.
12
+ * @param input - Configuration for sync operation
13
+ * @returns Result of the synchronisation
14
+ * @example
15
+ * const result = syncCommand({ jsonPath: "doc.spm.json", mdPath: "doc.spm.md" });
16
+ */
17
+ export function syncCommand(input) {
18
+ const { jsonPath, mdPath, strategy = "json", dryRun = false } = input;
19
+ // Read JSON document
20
+ const jsonContent = readFileSync(jsonPath, "utf8");
21
+ const jsonDoc = JSON.parse(jsonContent);
22
+ if (!SysProMDocument.is(jsonDoc)) {
23
+ throw new Error("JSON file is not a valid SysProM document");
24
+ }
25
+ // Parse Markdown to document
26
+ const mdDoc = markdownToJson(mdPath);
27
+ // Detect which side changed
28
+ const changes = detectChanges(jsonPath, mdPath);
29
+ // Perform sync operation
30
+ const result = syncDocumentsOp({
31
+ jsonDoc,
32
+ mdDoc,
33
+ jsonChanged: changes.jsonChanged,
34
+ mdChanged: changes.mdChanged,
35
+ strategy,
36
+ });
37
+ // Write results if not dry-run
38
+ if (!dryRun) {
39
+ // Write synced document back to both formats
40
+ if (result.jsonChanged || result.mdChanged || result.conflict) {
41
+ // Update JSON
42
+ writeFileSync(jsonPath, canonicalise(result.synced, { indent: "\t" }) + "\n");
43
+ // Update Markdown
44
+ const mdContent = jsonToMarkdownSingle(result.synced);
45
+ writeFileSync(mdPath, mdContent);
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ function isArgs(arg) {
51
+ return (typeof arg === "object" && arg !== null && "input" in arg && "output" in arg);
52
+ }
53
+ function isOpts(opt) {
54
+ return typeof opt === "object" && opt !== null;
55
+ }
56
+ export const syncCommandDef = {
57
+ name: "sync",
58
+ description: "Synchronise JSON and Markdown representations with conflict resolution",
59
+ apiLink: "syncDocuments",
60
+ args: z.object({
61
+ input: z.string().describe("Path to JSON file"),
62
+ output: z.string().describe("Path to Markdown file"),
63
+ }),
64
+ opts: z
65
+ .object({
66
+ preferJson: z
67
+ .boolean()
68
+ .optional()
69
+ .describe("Prefer JSON as source of truth in conflicts"),
70
+ preferMd: z
71
+ .boolean()
72
+ .optional()
73
+ .describe("Prefer Markdown as source of truth in conflicts"),
74
+ dryRun: z
75
+ .boolean()
76
+ .optional()
77
+ .describe("Preview changes without writing files"),
78
+ report: z
79
+ .boolean()
80
+ .optional()
81
+ .describe("Report conflicts without resolving"),
82
+ })
83
+ .strict(),
84
+ action(args, opts) {
85
+ if (!isArgs(args))
86
+ throw new Error("Invalid args");
87
+ if (!isOpts(opts))
88
+ throw new Error("Invalid opts");
89
+ const jsonPath = resolve(args.input);
90
+ const mdPath = resolve(args.output);
91
+ // Determine conflict strategy
92
+ let strategy = "json";
93
+ if (opts.preferMd)
94
+ strategy = "md";
95
+ if (opts.report)
96
+ strategy = "report";
97
+ const result = syncCommand({
98
+ jsonPath,
99
+ mdPath,
100
+ strategy,
101
+ dryRun: opts.dryRun ?? false,
102
+ });
103
+ // Output results
104
+ console.log(`Sync complete:`);
105
+ console.log(` JSON changed: ${String(result.jsonChanged)}`);
106
+ console.log(` Markdown changed: ${String(result.mdChanged)}`);
107
+ console.log(` Conflict: ${String(result.conflict)}`);
108
+ if (result.changedNodes.length > 0) {
109
+ console.log(` Changed nodes: ${result.changedNodes.join(", ")}`);
110
+ }
111
+ },
112
+ };
@@ -16,6 +16,7 @@ import { updateCommand } from "./commands/update.js";
16
16
  import { speckitCommand } from "./commands/speckit.js";
17
17
  import { taskCommand } from "./commands/task.js";
18
18
  import { planCommand } from "./commands/plan.js";
19
+ import { syncCommandDef } from "./commands/sync.js";
19
20
  const VERSION = "1.0.0";
20
21
  export const program = new Command();
21
22
  program
@@ -40,6 +41,7 @@ export const commands = [
40
41
  speckitCommand,
41
42
  taskCommand,
42
43
  planCommand,
44
+ syncCommandDef,
43
45
  ];
44
46
  for (const cmd of commands) {
45
47
  buildCommander(cmd, program);
@@ -261,6 +261,7 @@ function generateReadme(doc, fromIdx) {
261
261
  const lines = [];
262
262
  const title = doc.metadata?.title ?? "SysProM";
263
263
  lines.push(renderFrontMatter({
264
+ ...(doc.$schema ? { $schema: doc.$schema } : {}),
264
265
  title,
265
266
  doc_type: doc.metadata?.doc_type ?? "sysprom",
266
267
  scope: doc.metadata?.scope,
@@ -373,6 +374,7 @@ export function jsonToMarkdownSingle(doc) {
373
374
  const lines = [];
374
375
  const title = doc.metadata?.title ?? "SysProM";
375
376
  lines.push(renderFrontMatter({
377
+ ...(doc.$schema ? { $schema: doc.$schema } : {}),
376
378
  title,
377
379
  doc_type: doc.metadata?.doc_type ?? "sysprom",
378
380
  scope: doc.metadata?.scope,
@@ -35,6 +35,13 @@ function parseText(raw) {
35
35
  const lines = raw.split("\n");
36
36
  return lines.length === 1 ? lines[0] : lines;
37
37
  }
38
+ /** Separate $schema from front matter so it becomes a top-level document key. */
39
+ function extractSchema(front) {
40
+ const schema = typeof front.$schema === "string" ? front.$schema : undefined;
41
+ const metadata = { ...front };
42
+ delete metadata.$schema;
43
+ return { schema, metadata };
44
+ }
38
45
  function parseFrontMatter(content) {
39
46
  if (!content.startsWith("---\n"))
40
47
  return { front: {}, body: content };
@@ -44,7 +51,7 @@ function parseFrontMatter(content) {
44
51
  const yaml = content.slice(4, end);
45
52
  const front = {};
46
53
  for (const line of yaml.split("\n")) {
47
- const match = /^(\w+):\s*(.+)$/.exec(line);
54
+ const match = /^([\w$]+):\s*(.+)$/.exec(line);
48
55
  if (!match)
49
56
  continue;
50
57
  const [, key, raw] = match;
@@ -167,9 +174,22 @@ function parseListItems(body, prefix) {
167
174
  return items;
168
175
  }
169
176
  function parseSingleValue(body, prefix) {
170
- for (const line of body.split("\n")) {
171
- if (line.startsWith(`${prefix}: `)) {
172
- return line.slice(prefix.length + 2);
177
+ const lines = body.split("\n");
178
+ for (let i = 0; i < lines.length; i++) {
179
+ if (lines[i].startsWith(`${prefix}: `)) {
180
+ const firstLine = lines[i].slice(prefix.length + 2);
181
+ const continuationLines = [firstLine];
182
+ for (let j = i + 1; j < lines.length; j++) {
183
+ const next = lines[j];
184
+ if (next === "")
185
+ break;
186
+ if (next.startsWith("- ") || next.startsWith("#"))
187
+ break;
188
+ if (/^[A-Z][a-z]+: /.test(next))
189
+ break;
190
+ continuationLines.push(next);
191
+ }
192
+ return continuationLines.join("\n");
173
193
  }
174
194
  }
175
195
  return undefined;
@@ -450,14 +470,16 @@ export function markdownSingleToJson(content) {
450
470
  const { nodes, rels } = parseDocFile(content, allTypes);
451
471
  const tableRels = parseRelationshipTable(body);
452
472
  const extRefs = parseExternalReferences(body);
473
+ const { schema, metadata: metaFront } = extractSchema(front);
453
474
  const doc = {
454
- metadata: Object.keys(front).length > 0 ? front : undefined,
475
+ ...(schema ? { $schema: schema } : {}),
476
+ metadata: Object.keys(metaFront).length > 0 ? metaFront : undefined,
455
477
  nodes,
456
478
  relationships: [...rels, ...tableRels].length > 0 ? [...rels, ...tableRels] : undefined,
457
479
  external_references: extRefs.length > 0 ? extRefs : undefined,
458
480
  };
459
- if (front.title && typeof front.title === "string") {
460
- doc.metadata = { ...front };
481
+ if (metaFront.title && typeof metaFront.title === "string") {
482
+ doc.metadata = { ...metaFront };
461
483
  }
462
484
  return doc;
463
485
  }
@@ -532,8 +554,10 @@ export function markdownMultiDocToJson(dir) {
532
554
  }
533
555
  }
534
556
  scanForSubsystems(dir);
557
+ const { schema, metadata: metaFront } = extractSchema(front);
535
558
  const doc = {
536
- metadata: Object.keys(front).length > 0 ? front : undefined,
559
+ ...(schema ? { $schema: schema } : {}),
560
+ metadata: Object.keys(metaFront).length > 0 ? metaFront : undefined,
537
561
  nodes,
538
562
  relationships: rels.length > 0 ? rels : undefined,
539
563
  external_references: extRefs.length > 0 ? extRefs : undefined,
@@ -32,6 +32,7 @@ export { graphOp } from "./graph.js";
32
32
  export { renameOp } from "./rename.js";
33
33
  export { jsonToMarkdownOp } from "./json-to-markdown.js";
34
34
  export { markdownToJsonOp } from "./markdown-to-json.js";
35
+ export { syncDocumentsOp, type BidirectionalSyncResult, type ConflictStrategy, } from "./sync.js";
35
36
  export { speckitImportOp } from "./speckit-import.js";
36
37
  export { speckitExportOp } from "./speckit-export.js";
37
38
  export { speckitSyncOp, type SyncResult } from "./speckit-sync.js";
@@ -38,6 +38,8 @@ export { renameOp } from "./rename.js";
38
38
  // Conversion operations
39
39
  export { jsonToMarkdownOp } from "./json-to-markdown.js";
40
40
  export { markdownToJsonOp } from "./markdown-to-json.js";
41
+ // Synchronisation operations
42
+ export { syncDocumentsOp, } from "./sync.js";
41
43
  // Spec-Kit interoperability operations
42
44
  export { speckitImportOp } from "./speckit-import.js";
43
45
  export { speckitExportOp } from "./speckit-export.js";