dotdog 0.3.6 → 0.4.1

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 (3) hide show
  1. package/README.md +106 -52
  2. package/dist/cli.js +150 -65
  3. package/package.json +19 -6
package/README.md CHANGED
@@ -1,76 +1,130 @@
1
- # spec-platform
1
+ # dotdog
2
2
 
3
- Monorepo for the Spec Platform — a knowledge graph system where specs ARE the database and LLMs ARE the query engine.
3
+ [![npm version](https://img.shields.io/npm/v/dotdog)](https://www.npmjs.com/package/dotdog)
4
+ [![npm downloads](https://img.shields.io/npm/dm/dotdog)](https://www.npmjs.com/package/dotdog)
5
+ [![License: MIT](https://img.shields.io/npm/l/dotdog)](https://github.com/specdog/dotdog/blob/main/LICENSE)
6
+ [![CI](https://github.com/specdog/dotdog/actions/workflows/test.yml/badge.svg)](https://github.com/specdog/dotdog/actions)
4
7
 
5
- ## The Flywheel
8
+ > **Feed the dog. Ship with specs.** Write .dog specs. Dog checks them. AI agents fetch them.
6
9
 
7
- ```
8
- spec → validate → app → data → better spec → better app → ...
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install -g dotdog
9
14
  ```
10
15
 
11
- The spec describes the platform. The platform validates the spec. The validation report improves the spec. Each cycle adds granularity.
16
+ Requires Node.js >= 20.
12
17
 
13
- ## Structure
18
+ ## Quick Start
14
19
 
20
+ ```bash
21
+ dotdog init my-project # scaffold a spec genome
22
+ dotdog validate # score completeness (0-100%)
23
+ dotdog analyze # deep analysis : gaps, suggestions, entity audit
15
24
  ```
16
- spec-platform/
17
- ├── packages/
18
- │ ├── spec-engine/ # Core types and ontology (shared by everything)
19
- │ ├── spec-mcp/ # MCP Server — AI agents query specs via stdio
20
- │ └── spec-cli/ # CLI — spec validate, init, simulate, list
21
- ├── projects/ # Spec genomes (dogfooding)
22
- │ └── spec-platform/
23
- │ └── specs/
24
- │ ├── SPEC.dog # Product spec screens, flows, stories
25
- │ ├── constitution.dog # Immutable rules
26
- │ └── data-model.dog # Graph ontology nodes, edges, tasks, predictions, vectors
27
- ├── templates/ # Spec genome templates for new projects
28
- └── package.json # Bun workspace root
25
+
26
+ ## Commands
27
+
28
+ | Command | Description |
29
+ |---------|-------------|
30
+ | `dotdog validate [dir]` | Score spec completeness. Checks file existence, entity descriptions, section counts. |
31
+ | `dotdog analyze [dir]` | Deep analysis. Detects domain, stack, gaps with severity, entity quality audit. |
32
+ | `dotdog parse <file>` | Parse a `.dog` file into sections. |
33
+ | `dotdog compile [dir]` | Compile `.dog` files into a `.dag` graph (JSON). |
34
+ | `dotdog visualize [dir]` | Output Mermaid graph from `.dag`. `--save` writes `.md` for GitHub rendering. |
35
+ | `dotdog serve [dir]` | Start MCP server over stdio. AI agents query specs without hallucination. |
36
+ | `dotdog staleness [dir]` | Detect drift between spec and reality. Compares plan.dog tasks against code. |
37
+ | `dotdog generate [dir]` | Generate missing spec files from SPEC.dog (data-model, COPY, INDEX). |
38
+ | `dotdog simulate <scenario>` | Run a simulation scenario. Reads SPEC.dog scenarios, checks pre/postconditions. |
39
+ | `dotdog init <project>` | Scaffold a new spec genome project with templates. |
40
+ | `dotdog list` | List all projects and their `.dog` file counts. |
41
+
42
+ ## File Formats
43
+
44
+ ### `.dog` : Human-Written Spec Genome
45
+
46
+ Markdown prose + YAML structured blocks. Free and open source. Define entities, relationships, events, predictions, and copy in a single format that both humans and parsers understand.
47
+
48
+ ```markdown
49
+ ### Entity: User
50
+
51
+ A person who uses the app.
52
+
53
+ ` ``yaml
54
+ entity: User
55
+ type: entity
56
+ properties:
57
+ id:
58
+ type: string
59
+ required: true
60
+ email:
61
+ type: string
62
+ required: true
63
+ states: [active, suspended]
64
+ lifecycle: active → suspended
65
+ ` ``
29
66
  ```
30
67
 
31
- ## Quick Start
68
+ ### `.dag` : Machine-Compiled Graph
32
69
 
33
- ```bash
34
- bun install
35
- cd projects/spec-platform/specs
70
+ JSON graph compiled from `.dog` files. Nodes, edges, properties, and states in a deterministic structure. 85% token savings vs raw `.dog` files for AI agents.
36
71
 
37
- # Validate our own spec (dogfood)
38
- bun ../../../packages/spec-cli/src/index.ts validate ../..
72
+ ## MCP Server : AI Agent Integration
39
73
 
40
- # List projects
41
- bun ../../../packages/spec-cli/src/index.ts list
42
- ```
74
+ `dotdog serve` exposes specs to any MCP-compatible AI agent over stdio. Six tools:
43
75
 
44
- ## $0 Stack
76
+ | Tool | Description |
77
+ |------|-------------|
78
+ | `getEntity` | Exact entity with properties, states, lifecycle, and connected edges |
79
+ | `traverse` | BFS subgraph from any starting node to any depth |
80
+ | `search` | Find entities by name or type |
81
+ | `schema` | Property definitions only : zero prose, agent-optimized |
82
+ | `summary` | Node count, edge count, file count, compile time |
83
+ | `listProjects` | Array of project names |
45
84
 
46
- | Component | Technology | Cost |
47
- |-----------|-----------|------|
48
- | Runtime | Bun | $0 |
49
- | Database | bun:sqlite (embedded) | $0 |
50
- | Types | TypeScript (strict) | $0 |
51
- | CLI | Commander.js + chalk | $0 |
52
- | MCP Server | @modelcontextprotocol/sdk (stdio) | $0 |
53
- | Embeddings | all-MiniLM-L6-v2 (local) | $0 |
54
- | Hosting | None needed (local-first) | $0 |
85
+ Agent workflow: `listProjects` `getEntity` `traverse` graph.
55
86
 
56
- ## The Spec Graph
87
+ ## Dogfood
57
88
 
58
- The spec is not a document. It's a knowledge graph.
89
+ dotdog validates its own specs. Every PR:
90
+
91
+ ```
92
+ dotdog validate → find gaps → fix spec → PR → merge → tag → CI publish
93
+ ```
59
94
 
60
- - **Nodes**: entities, tasks, predictions, screens, constraints, user stories
61
- - **Edges**: contains, depends_on, implements, references, calls, precedes
62
- - **Vectors**: every section embedded for semantic search, contradiction detection, staleness checks
63
- - **Predictions**: forecasts with triggers, timeframes, confidence, and actual outcome tracking
95
+ Eat your own dogfood. The tool is the project.
64
96
 
65
- LLMs traverse the graph at query time. They don't read prose and guess — they get exact typed values.
97
+ ## VS Code Extension
66
98
 
67
- ## Score
99
+ Syntax highlighting for `.dog` files. Install:
68
100
 
101
+ ```bash
102
+ cp -r extensions/vscode ~/.vscode/extensions/dotdog
69
103
  ```
70
- spec validate → 43% complete
71
104
 
72
- SPEC.dog
73
- ✓ constitution.dog
74
- data-model.dog
75
- COPY.dog, DESIGN-SYSTEM.dog, plan.dog, INDEX.dog
105
+ ## Format Specifications
106
+
107
+ - [`.dog` format spec](spec/format-spec.dog) : language definition, EBNF grammar, validation rules
108
+ - [`.dag` format spec](spec/format-spec-dag.dog) : graph definition, MCP API, token efficiency
109
+
110
+ ## Links
111
+
112
+ - **GitHub:** [specdog/dotdog](https://github.com/specdog/dotdog)
113
+ - **npm:** [dotdog](https://www.npmjs.com/package/dotdog)
114
+ - **Docs:** [GitHub Pages](https://specdog.github.io/dotdog)
115
+ - **llms.txt:** [llms.txt](llms.txt) : structured for AI agent discovery
116
+ - **AGENTS.md:** [AGENTS.md](AGENTS.md) : instructions for AI coding agents
117
+
118
+ ## Spec-Driven Development
119
+
120
+ dotdog is built for SDD. Write your spec first. Validate it. Compile it. Let AI agents query it. The spec is the source of truth.
121
+
122
+ ```
123
+ spec → validate → compile → serve → AI agent queries
76
124
  ```
125
+
126
+ No more specs that rot in a wiki. No more agents guessing from prose. One source. Zero ambiguity.
127
+
128
+ ## License
129
+
130
+ MIT
package/dist/cli.js CHANGED
@@ -3108,8 +3108,14 @@ function resolvePath(p) {
3108
3108
  }
3109
3109
  return resolved;
3110
3110
  }
3111
+ var isV2 = (n) => Array.isArray(n) && typeof n[0] === "number";
3111
3112
  var N = (dag) => dag.n || dag.nodes || [];
3113
+ function edgeToObj(n, tgtIdx, v2e) {
3114
+ return { t: String(tgtIdx), v: v2e[1] || "", c: v2e[2] || "", r: v2e[3] || 0 };
3115
+ }
3112
3116
  function nodeEdges(n) {
3117
+ if (isV2(n))
3118
+ return (n[6] || []).map((e) => edgeToObj(n, e[0], e));
3113
3119
  return n.es || [];
3114
3120
  }
3115
3121
  function E(dag) {
@@ -3119,21 +3125,31 @@ function E(dag) {
3119
3125
  const seen = new Set;
3120
3126
  for (const node of N(dag)) {
3121
3127
  for (const e of nodeEdges(node)) {
3122
- const key = `${node.i || node.id}→${e.t}:${e.v}`;
3128
+ const src = isV2(node) ? String(node[0]) : node.i || node.id || "";
3129
+ const key = `${src}→${e.t}:${e.v}`;
3123
3130
  if (!seen.has(key)) {
3124
3131
  seen.add(key);
3125
- edges.push({ s: node.i || node.id, t: e.t, v: e.v, d: e.d, c: e.c, r: e.r });
3132
+ edges.push({ s: src, t: e.t, v: e.v, d: e.d, c: e.c, r: e.r });
3126
3133
  }
3127
3134
  }
3128
3135
  }
3129
3136
  return edges;
3130
3137
  }
3131
3138
  var P = (dag) => dag.p || dag.project || "";
3132
- var ni = (n) => n.i || n.id || "";
3133
- var nt = (n) => n.t || n.type || "";
3134
- var np = (n) => n.p || n.properties || {};
3135
- var ns = (n) => n.s || n.states || [];
3136
- var nl = (n) => n.l || n.lifecycle || [];
3139
+ var ni = (n) => isV2(n) ? String(n[0]) : n.i || n.id || "";
3140
+ var nt = (n) => isV2(n) ? n[2] || "" : n.t || n.type || "";
3141
+ function np(n) {
3142
+ if (isV2(n)) {
3143
+ const flat = n[4] || [];
3144
+ const obj = {};
3145
+ for (let i = 0;i < flat.length; i += 2)
3146
+ obj[flat[i]] = flat[i + 1] || "";
3147
+ return obj;
3148
+ }
3149
+ return n.p || n.properties || {};
3150
+ }
3151
+ var ns = (n) => isV2(n) ? n[5] || [] : n.s || n.states || [];
3152
+ var nl = (n) => isV2(n) ? [] : n.l || n.lifecycle || [];
3137
3153
  var es = (e) => e.s || e.source || "";
3138
3154
  var et = (e) => e.t || e.target || "";
3139
3155
  function serve(dir = ".") {
@@ -3203,7 +3219,7 @@ function serve(dir = ".") {
3203
3219
  if (!node)
3204
3220
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
3205
3221
  const edges = E(dag).filter((e) => es(e).toLowerCase() === ni(node).toLowerCase() || et(e).toLowerCase() === ni(node).toLowerCase());
3206
- return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({ ...node, edges }) }] } };
3222
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(isV2(node) ? { id: String(node[0]), name: node[1] || "", type: node[2] || "", description: node[3] || "", properties: np(node), states: ns(node), edges } : { ...node, edges }) }] } };
3207
3223
  }
3208
3224
  if (name === "traverse") {
3209
3225
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
@@ -3221,7 +3237,7 @@ function serve(dir = ".") {
3221
3237
  visitedNodes.add(curr.id);
3222
3238
  const node = N(dag).find((n) => ni(n).toLowerCase() === curr.id.toLowerCase());
3223
3239
  if (node)
3224
- subgraph.nodes.push(node);
3240
+ subgraph.nodes.push(isV2(node) ? { id: String(node[0]), name: node[1] || "", type: node[2] || "", description: node[3] || "", properties: np(node), states: ns(node), edges: nodeEdges(node) } : node);
3225
3241
  const edges = E(dag).filter((e) => es(e).toLowerCase() === curr.id.toLowerCase() || et(e).toLowerCase() === curr.id.toLowerCase());
3226
3242
  for (const e of edges) {
3227
3243
  const edgeKey = `${es(e)}→${et(e)}`;
@@ -3431,6 +3447,12 @@ program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data
3431
3447
  const tmpl = opts.minimal ? minimal : full;
3432
3448
  for (const [f, c] of Object.entries(tmpl)) {
3433
3449
  writeFileSync2(join3(d, f), c);
3450
+ try {
3451
+ parse(c);
3452
+ } catch (_) {
3453
+ console.log(source_default.red(` ✗ Template ${f} is invalid`));
3454
+ process.exit(1);
3455
+ }
3434
3456
  console.log(source_default.green(` ✓ ${f}`));
3435
3457
  }
3436
3458
  console.log(source_default.bold(`
@@ -3555,44 +3577,63 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3555
3577
  }
3556
3578
  }
3557
3579
  }
3558
- const nodeIds = new Map;
3559
- nodes.forEach((n, i) => nodeIds.set(n.i, i));
3560
- const inDegree = new Array(nodes.length).fill(0);
3561
- const adj = new Array(nodes.length).fill(0).map(() => []);
3580
+ const entityNames = new Set(nodes.map((n) => n.i));
3562
3581
  for (const e of edges) {
3563
- const si = nodeIds.get(e.s), ti = nodeIds.get(e.t);
3564
- if (si !== undefined && ti !== undefined) {
3565
- adj[si].push(ti);
3566
- inDegree[ti]++;
3582
+ if (e.s && !entityNames.has(e.s)) {
3583
+ console.log(source_default.red(` ✗ Unknown relationship source "${e.s}" (target: "${e.t}")`));
3584
+ process.exit(1);
3567
3585
  }
3568
- }
3569
- const queue = [];
3570
- for (let j = 0;j < nodes.length; j++)
3571
- if (inDegree[j] === 0)
3572
- queue.push(j);
3573
- const order = [];
3574
- while (queue.length > 0) {
3575
- const u = queue.shift();
3576
- order.push(nodes[u].i);
3577
- for (const v of adj[u]) {
3578
- inDegree[v]--;
3579
- if (inDegree[v] === 0)
3580
- queue.push(v);
3586
+ if (e.t && !entityNames.has(e.t)) {
3587
+ console.log(source_default.red(` ✗ Unknown relationship target "${e.t}" (source: "${e.s}")`));
3588
+ process.exit(1);
3581
3589
  }
3582
3590
  }
3583
- const cycles = order.length !== nodes.length;
3591
+ const nodeIds = new Map;
3592
+ nodes.forEach((n, i) => nodeIds.set(n.i, i));
3593
+ const v2nodes = [];
3584
3594
  for (let j = 0;j < nodes.length; j++) {
3585
- const id = nodes[j].i;
3586
- nodes[j].es = edges.filter((e) => e.s === id || e.t === id).map((e) => ({
3587
- t: e.s === id ? e.t : e.s,
3588
- v: e.v || "",
3589
- d: e.d || "",
3590
- c: e.c,
3591
- r: e.r,
3592
- dir: e.s === id ? "out" : "in"
3593
- }));
3595
+ const nd = nodes[j];
3596
+ const props = [];
3597
+ if (nd.p)
3598
+ for (const [k, v] of Object.entries(nd.p))
3599
+ props.push(k, v);
3600
+ const states = nd.s || [];
3601
+ const outEdges = [];
3602
+ const seen = new Set;
3603
+ for (const e of edges) {
3604
+ if (e.s !== nd.i && e.t !== nd.i)
3605
+ continue;
3606
+ const tid = nodeIds.get(e.s === nd.i ? e.t : e.s);
3607
+ if (tid === undefined)
3608
+ continue;
3609
+ const key = `${j}→${tid}:${e.v}`;
3610
+ if (seen.has(key))
3611
+ continue;
3612
+ seen.add(key);
3613
+ const ee = [tid, e.v || ""];
3614
+ if (e.c)
3615
+ ee.push(e.c);
3616
+ if (e.r)
3617
+ ee.push(1);
3618
+ outEdges.push(ee);
3619
+ }
3620
+ const entry = [j, nd.i || "", nd.t || "", nd.d || "", props, states, outEdges];
3621
+ if (nd.g === "prediction") {
3622
+ const f = [];
3623
+ if (nd.cf)
3624
+ f.push(nd.cf);
3625
+ if (nd.tf)
3626
+ f.push(nd.tf);
3627
+ if (nd.tg)
3628
+ f.push(nd.tg);
3629
+ if (nd.ms)
3630
+ f.push(nd.ms);
3631
+ if (f.length)
3632
+ entry.push(f);
3633
+ }
3634
+ v2nodes.push(entry);
3594
3635
  }
3595
- const dag = { v: "1.5", p, c: `dotdog@${pkg.version}`, n: nodes, o: order, cy: cycles };
3636
+ const dag = { v: 2, p, n: v2nodes };
3596
3637
  const dagJson = JSON.stringify(dag);
3597
3638
  const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
3598
3639
  const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
@@ -3636,13 +3677,17 @@ program2.command("tokens [dir]").action((d = ".") => {
3636
3677
  }
3637
3678
  const dagBytes = Buffer.byteLength(readFileSync3(dagFile, "utf-8"), "utf-8");
3638
3679
  const savings = sourceBytes > 0 ? Math.round((1 - dagBytes / sourceBytes) * 1000) / 10 : 0;
3680
+ const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
3681
+ const dagOnly = JSON.stringify({ v: dag.v, p: dag.p, n: dag.n });
3682
+ const dagOnlyBytes = Buffer.byteLength(dagOnly, "utf-8");
3683
+ const dagOnlyPct = sourceBytes > 0 ? Math.round((1 - dagOnlyBytes / sourceBytes) * 1000) / 10 : 0;
3639
3684
  console.log(source_default.bold(`
3640
3685
  ${p}`));
3641
3686
  console.log(source_default.gray(` ${dogFiles.length} .dog files: ${sourceBytes} bytes`));
3642
- console.log(source_default.gray(` .dag file: ${dagBytes} bytes`));
3643
- console.log(source_default.green(` ${savings}% smaller (${sourceBytes - dagBytes} bytes saved)`));
3687
+ console.log(source_default.gray(` .dag on disk: ${dagBytes} bytes (${savings}% savings, includes metadata)`));
3688
+ console.log(source_default.green(` .dag payload: ${dagOnlyBytes} bytes (${dagOnlyPct}% savings, graph only)`));
3644
3689
  if (contentBytes && contentBytes !== sourceBytes) {
3645
- const cs = Math.round((1 - dagBytes / contentBytes) * 1000) / 10;
3690
+ const cs = Math.round((1 - dagOnlyBytes / contentBytes) * 1000) / 10;
3646
3691
  console.log(source_default.gray(` content-only: ${contentBytes} bytes → ${cs}% savings`));
3647
3692
  }
3648
3693
  }
@@ -3663,22 +3708,32 @@ program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts)
3663
3708
  continue;
3664
3709
  const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
3665
3710
  const nodes = dag.n || dag.nodes || [];
3711
+ const isV22 = (n) => Array.isArray(n) && typeof n[0] === "number";
3712
+ const nodeName = (n) => isV22(n) ? nodes[n[0]] ? nodes[n[0]][1] || String(n[0]) : String(n[0]) : n.i || n.id || "";
3713
+ const slug = (s) => s.replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3666
3714
  let out = "```mermaid\ngraph LR\n";
3667
3715
  for (const n of nodes) {
3668
- const raw = n.i || n.id || "";
3669
- const id = raw.replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3716
+ const raw = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
3670
3717
  if (raw)
3671
- out += ` ${id}[${raw}]
3718
+ out += ` ${slug(raw)}[${raw}]
3672
3719
  `;
3673
3720
  }
3674
3721
  const seen = new Set;
3675
3722
  for (const n of nodes) {
3676
- for (const e of n.es || []) {
3677
- if (e.dir === "in")
3678
- continue;
3679
- const src = (n.i || n.id || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3680
- const tgt = (e.t || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3681
- const verb = e.v || "";
3723
+ const edges = isV22(n) ? n[6] || [] : n.es || [];
3724
+ for (const e of edges) {
3725
+ const srcName = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
3726
+ const src = slug(srcName);
3727
+ let tgtName, verb;
3728
+ if (isV22(n)) {
3729
+ const tgtNode = nodes[e[0]];
3730
+ tgtName = tgtNode ? tgtNode[1] || String(tgtNode[0]) : String(e[0]);
3731
+ verb = e[1] || "";
3732
+ } else {
3733
+ tgtName = e.t || "";
3734
+ verb = e.v || "";
3735
+ }
3736
+ const tgt = slug(tgtName);
3682
3737
  const key = `${src}→${tgt}:${verb}`;
3683
3738
  if (!seen.has(key) && src && tgt) {
3684
3739
  seen.add(key);
@@ -3689,8 +3744,8 @@ program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts)
3689
3744
  }
3690
3745
  const legacyEdges = dag.e || dag.edges || [];
3691
3746
  for (const e of legacyEdges) {
3692
- const src = (e.s || e.source || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3693
- const tgt = (e.t || e.target || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3747
+ const src = slug(e.s || e.source || "");
3748
+ const tgt = slug(e.t || e.target || "");
3694
3749
  const verb = e.v || e.verb || "";
3695
3750
  const key = `${src}→${tgt}:${verb}`;
3696
3751
  if (!seen.has(key) && src && tgt) {
@@ -3913,6 +3968,12 @@ ${e.description || "No description."}
3913
3968
  dm += "```\n\n";
3914
3969
  }
3915
3970
  writeFileSync2(join3(specDir, "data-model.dog"), dm);
3971
+ try {
3972
+ parse(dm);
3973
+ } catch (_) {
3974
+ console.log(source_default.red(" ✗ Generated data-model.dog is invalid"));
3975
+ process.exit(1);
3976
+ }
3916
3977
  console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
3917
3978
  }
3918
3979
  if (!existsSync3(join3(specDir, "COPY.dog")) && uiStrings.length > 0) {
@@ -3925,6 +3986,12 @@ ${e.description || "No description."}
3925
3986
  copy += `| ${s.screen} | ${s.element} | ${s.text} |
3926
3987
  `;
3927
3988
  writeFileSync2(join3(specDir, "COPY.dog"), copy);
3989
+ try {
3990
+ parse(copy);
3991
+ } catch (_) {
3992
+ console.log(source_default.red(" ✗ Generated COPY.dog is invalid"));
3993
+ process.exit(1);
3994
+ }
3928
3995
  console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
3929
3996
  }
3930
3997
  if (!existsSync3(join3(specDir, "INDEX.dog"))) {
@@ -3940,6 +4007,12 @@ ${e.description || "No description."}
3940
4007
  idx += `| Designer | SPEC.dog | COPY.dog |
3941
4008
  `;
3942
4009
  writeFileSync2(join3(specDir, "INDEX.dog"), idx);
4010
+ try {
4011
+ parse(idx);
4012
+ } catch (_) {
4013
+ console.log(source_default.red(" ✗ Generated INDEX.dog is invalid"));
4014
+ process.exit(1);
4015
+ }
3943
4016
  console.log(source_default.green(" ✓ INDEX.dog"));
3944
4017
  }
3945
4018
  console.log(source_default.bold(`
@@ -3974,19 +4047,23 @@ program2.command("simulate <scenario>").description("Walk through a scenario, ch
3974
4047
  let entities = [], relationships = [];
3975
4048
  if (existsSync3(dagFile)) {
3976
4049
  const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
3977
- entities = (dag.n || dag.nodes || []).map((n) => (n.i || n.id || "").toLowerCase());
4050
+ const simNodes = dag.n || dag.nodes || [];
4051
+ const isV22 = (n) => Array.isArray(n) && typeof n[0] === "number";
4052
+ entities = simNodes.map((n) => (isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "").toLowerCase());
3978
4053
  if (dag.e || dag.edges) {
3979
4054
  relationships = dag.e || dag.edges || [];
3980
4055
  } else {
3981
4056
  const seen = new Set;
3982
- for (const n of dag.n || dag.nodes || []) {
3983
- for (const e of n.es || []) {
3984
- if (e.dir === "in")
3985
- continue;
3986
- const key = `${n.i || n.id}→${e.t}:${e.v}`;
4057
+ for (const n of simNodes) {
4058
+ const edges = isV22(n) ? n[6] || [] : n.es || [];
4059
+ for (const e of edges) {
4060
+ const srcName = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
4061
+ const tgtName = isV22(n) ? (simNodes[e[0]] ? simNodes[e[0]][2] : "") || String(e[0]) : e.t || "";
4062
+ const verb = isV22(n) ? e[1] || "" : e.v || "";
4063
+ const key = `${srcName}→${tgtName}:${verb}`;
3987
4064
  if (!seen.has(key)) {
3988
4065
  seen.add(key);
3989
- relationships.push({ s: n.i || n.id, t: e.t, v: e.v, d: e.d, r: e.r });
4066
+ relationships.push({ s: srcName, t: tgtName, v: verb });
3990
4067
  }
3991
4068
  }
3992
4069
  }
@@ -4240,6 +4317,7 @@ program2.command("resolve <name>").description("Mark a prediction as correct, wr
4240
4317
  for (const f of files) {
4241
4318
  const fp = join3(pd, f);
4242
4319
  let content = readFileSync3(fp, "utf-8");
4320
+ const originalContent = readFileSync3(fp, "utf-8");
4243
4321
  const ast = parse(content);
4244
4322
  for (const section of ast.sections) {
4245
4323
  for (const block of section.blocks) {
@@ -4259,11 +4337,18 @@ program2.command("resolve <name>").description("Mark a prediction as correct, wr
4259
4337
  } else {
4260
4338
  newYaml = yamlBlock.replace(/\n\s*(trigger|timeframe|confidence|measurement):/, `
4261
4339
  status: ${status}
4262
- $1`);
4340
+ $&`);
4263
4341
  }
4264
4342
  content = content.replace(yamlBlock, newYaml);
4265
- writeFileSync2(fp, content, "utf-8");
4266
- console.log(source_default.green(` ✓ ${b.statement || b.name}: ${status}`));
4343
+ try {
4344
+ parse(content);
4345
+ writeFileSync2(fp, content, "utf-8");
4346
+ console.log(source_default.green(` ✓ ${b.statement || b.name}: ${status}`));
4347
+ } catch (_) {
4348
+ writeFileSync2(fp, originalContent, "utf-8");
4349
+ console.log(source_default.red(` ✗ resolve produced invalid output for "${name}" — reverted`));
4350
+ process.exit(1);
4351
+ }
4267
4352
  return;
4268
4353
  }
4269
4354
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotdog",
3
- "version": "0.3.6",
3
+ "version": "0.4.1",
4
4
  "description": "CLI tool for structured software specifications. Validate .dog files, compile .dag graphs, query via MCP.",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -16,14 +16,23 @@
16
16
  "kits/"
17
17
  ],
18
18
  "keywords": [
19
- "specification",
19
+ "agent",
20
+ "ai",
21
+ "bun",
20
22
  "cli",
21
- "mcp",
22
- "graph",
23
+ "code-generation",
23
24
  "dag",
24
- "yaml",
25
+ "graph",
26
+ "knowledge-graph",
27
+ "llm",
25
28
  "markdown",
26
- "spec-driven-development"
29
+ "mcp",
30
+ "spec-driven-development",
31
+ "spec-genome",
32
+ "specification",
33
+ "structured",
34
+ "typescript",
35
+ "yaml"
27
36
  ],
28
37
  "license": "MIT",
29
38
  "author": "specdog",
@@ -34,5 +43,9 @@
34
43
  },
35
44
  "publishConfig": {
36
45
  "access": "public"
46
+ },
47
+ "homepage": "https://specdog.github.io/dotdog",
48
+ "bugs": {
49
+ "url": "https://github.com/specdog/dotdog/issues"
37
50
  }
38
51
  }