dotdog 0.3.1 → 0.3.2

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 (5) hide show
  1. package/README.md +52 -106
  2. package/dist/cli.js +136 -66
  3. package/package.json +13 -17
  4. package/CHANGELOG.md +0 -35
  5. package/LICENSE +0 -21
package/README.md CHANGED
@@ -1,130 +1,76 @@
1
- # dotdog
1
+ # spec-platform
2
2
 
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)
3
+ Monorepo for the Spec Platform — a knowledge graph system where specs ARE the database and LLMs ARE the query engine.
7
4
 
8
- > **Feed the dog. Ship with specs.** Write .dog specs. Dog checks them. AI agents fetch them.
5
+ ## The Flywheel
9
6
 
10
- ## Install
11
-
12
- ```bash
13
- npm install -g dotdog
14
- ```
15
-
16
- Requires Node.js >= 20.
17
-
18
- ## Quick Start
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
24
7
  ```
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
- ` ``
8
+ spec → validate → app → data → better spec → better app → ...
66
9
  ```
67
10
 
68
- ### `.dag` : Machine-Compiled Graph
69
-
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.
71
-
72
- ## MCP Server : AI Agent Integration
73
-
74
- `dotdog serve` exposes specs to any MCP-compatible AI agent over stdio. Six tools:
11
+ The spec describes the platform. The platform validates the spec. The validation report improves the spec. Each cycle adds granularity.
75
12
 
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 |
84
-
85
- Agent workflow: `listProjects` → `getEntity` → `traverse` graph.
86
-
87
- ## Dogfood
88
-
89
- dotdog validates its own specs. Every PR:
13
+ ## Structure
90
14
 
91
15
  ```
92
- dotdog validate → find gaps → fix spec → PR → merge → tag → CI publish
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
93
29
  ```
94
30
 
95
- Eat your own dogfood. The tool is the project.
31
+ ## Quick Start
96
32
 
97
- ## VS Code Extension
33
+ ```bash
34
+ bun install
35
+ cd projects/spec-platform/specs
98
36
 
99
- Syntax highlighting for `.dog` files. Install:
37
+ # Validate our own spec (dogfood)
38
+ bun ../../../packages/spec-cli/src/index.ts validate ../..
100
39
 
101
- ```bash
102
- cp -r extensions/vscode ~/.vscode/extensions/dotdog
40
+ # List projects
41
+ bun ../../../packages/spec-cli/src/index.ts list
103
42
  ```
104
43
 
105
- ## Format Specifications
44
+ ## $0 Stack
106
45
 
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
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 |
109
55
 
110
- ## Links
56
+ ## The Spec Graph
111
57
 
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
58
+ The spec is not a document. It's a knowledge graph.
117
59
 
118
- ## Spec-Driven Development
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
119
64
 
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.
65
+ LLMs traverse the graph at query time. They don't read prose and guess they get exact typed values.
121
66
 
122
- ```
123
- spec → validate → compile → serve → AI agent queries
124
- ```
125
-
126
- No more specs that rot in a wiki. No more agents guessing from prose. One source. Zero ambiguity.
67
+ ## Score
127
68
 
128
- ## License
69
+ ```
70
+ spec validate → 43% complete
129
71
 
130
- MIT
72
+ ✓ SPEC.dog
73
+ ✓ constitution.dog
74
+ ✓ data-model.dog
75
+ ⚠ COPY.dog, DESIGN-SYSTEM.dog, plan.dog, INDEX.dog
76
+ ```
package/dist/cli.js CHANGED
@@ -2537,7 +2537,6 @@ var source_default = chalk;
2537
2537
  import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, mkdirSync, writeFileSync } from "fs";
2538
2538
  import { join as join2, resolve as resolve2 } from "path";
2539
2539
  import { homedir as homedir2 } from "os";
2540
- import { createHash } from "crypto";
2541
2540
 
2542
2541
  // src/parser.ts
2543
2542
  function parse(source) {
@@ -2553,7 +2552,14 @@ function parseToJSON(source) {
2553
2552
  function parseSections(lines) {
2554
2553
  const sections = [];
2555
2554
  let i = 0;
2556
- const rootBlocks = parseBlocks(lines, 0, lines.length);
2555
+ let firstHeading = lines.length;
2556
+ for (let j = 0;j < lines.length; j++) {
2557
+ if (/^##\s/.test(lines[j])) {
2558
+ firstHeading = j;
2559
+ break;
2560
+ }
2561
+ }
2562
+ const rootBlocks = parseBlocks(lines, 0, firstHeading);
2557
2563
  if (rootBlocks.length > 0) {
2558
2564
  sections.push({
2559
2565
  kind: "section",
@@ -2574,7 +2580,7 @@ function parseSections(lines) {
2574
2580
  const sectionStart = i;
2575
2581
  i++;
2576
2582
  const end = findSectionEnd(lines, i, level);
2577
- const blocks = parseBlocks(lines, i, end);
2583
+ const blocks = parseBlocks(lines, sectionStart, end);
2578
2584
  sections.push({
2579
2585
  kind: "section",
2580
2586
  level,
@@ -2593,9 +2599,7 @@ function parseSections(lines) {
2593
2599
  function findSectionEnd(lines, start, currentLevel) {
2594
2600
  for (let i = start;i < lines.length; i++) {
2595
2601
  const line = lines[i];
2596
- if (/^##\s/.test(line))
2597
- return i;
2598
- if (currentLevel === 2 && /^###\s/.test(line))
2602
+ if (/^##\s/.test(line) || /^###\s/.test(line))
2599
2603
  return i;
2600
2604
  }
2601
2605
  return lines.length;
@@ -2906,13 +2910,27 @@ function resolvePath(p) {
2906
2910
  const resolved = p.startsWith("/") ? p : join(process.cwd(), p);
2907
2911
  if (!p.startsWith("/") && !p.startsWith("~")) {
2908
2912
  const rel = resolve(process.cwd(), p);
2909
- if (!rel.startsWith(process.cwd() + "/") && rel !== process.cwd()) {
2913
+ const cwd = process.cwd();
2914
+ const isDescendant = rel.startsWith(cwd + "/");
2915
+ const isSelf = rel === cwd;
2916
+ const isAncestor = cwd.startsWith(rel + "/");
2917
+ if (!isDescendant && !isSelf && !isAncestor) {
2910
2918
  throw new Error(`Path traversal blocked: ${p}`);
2911
2919
  }
2912
2920
  return rel;
2913
2921
  }
2914
2922
  return resolved;
2915
2923
  }
2924
+ var N = (dag) => dag.n || dag.nodes || [];
2925
+ var E = (dag) => dag.e || dag.edges || [];
2926
+ var P = (dag) => dag.p || dag.project || "";
2927
+ var ni = (n) => n.i || n.id || "";
2928
+ var nt = (n) => n.t || n.type || "";
2929
+ var np = (n) => n.p || n.properties || {};
2930
+ var ns = (n) => n.s || n.states || [];
2931
+ var nl = (n) => n.l || n.lifecycle || [];
2932
+ var es = (e) => e.s || e.source || "";
2933
+ var et = (e) => e.t || e.target || "";
2916
2934
  function serve(dir = ".") {
2917
2935
  const root = resolvePath(dir);
2918
2936
  const dagCache = new Map;
@@ -2954,7 +2972,7 @@ function serve(dir = ".") {
2954
2972
  { name: "traverse", description: "Traverse graph: from node, follow edges, return subgraph", inputSchema: { type: "object", properties: { project: { type: "string" }, from: { type: "string" }, depth: { type: "number", default: 1 } }, required: ["from"] } },
2955
2973
  { name: "search", description: "Find entities by name or type", inputSchema: { type: "object", properties: { project: { type: "string" }, q: { type: "string" }, type: { type: "string" } }, required: ["q"] } },
2956
2974
  { name: "listProjects", description: "List all projects", inputSchema: { type: "object", properties: {} } },
2957
- { name: "summary", description: "Get project summary: node count, edge count, completeness", inputSchema: { type: "object", properties: { project: { type: "string" } } } },
2975
+ { name: "summary", description: "Get project summary: node count, edge count, token savings", inputSchema: { type: "object", properties: { project: { type: "string" } } } },
2958
2976
  { name: "schema", description: "Get full property schema for an entity", inputSchema: { type: "object", properties: { project: { type: "string" }, entity: { type: "string" } }, required: ["entity"] } }
2959
2977
  ]
2960
2978
  }
@@ -2970,16 +2988,16 @@ function serve(dir = ".") {
2970
2988
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
2971
2989
  if (!dag)
2972
2990
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
2973
- return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(dag.nodes || []) }] } };
2991
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(N(dag)) }] } };
2974
2992
  }
2975
2993
  if (name === "getEntity") {
2976
2994
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
2977
2995
  if (!dag)
2978
2996
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
2979
- const node = (dag.nodes || []).find((n) => n.id.toLowerCase() === (args.name || "").toLowerCase());
2997
+ const node = N(dag).find((n) => ni(n).toLowerCase() === (args.name || "").toLowerCase());
2980
2998
  if (!node)
2981
2999
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
2982
- const edges = (dag.edges || []).filter((e) => e.source.toLowerCase() === node.id.toLowerCase() || e.target.toLowerCase() === node.id.toLowerCase());
3000
+ const edges = E(dag).filter((e) => es(e).toLowerCase() === ni(node).toLowerCase() || et(e).toLowerCase() === ni(node).toLowerCase());
2983
3001
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({ ...node, edges }) }] } };
2984
3002
  }
2985
3003
  if (name === "traverse") {
@@ -2987,22 +3005,27 @@ function serve(dir = ".") {
2987
3005
  if (!dag)
2988
3006
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
2989
3007
  const depth = Math.min(Math.max(1, args.depth || 1), 20);
2990
- const visited = new Set;
3008
+ const visitedNodes = new Set;
3009
+ const visitedEdges = new Set;
2991
3010
  const subgraph = { nodes: [], edges: [] };
2992
3011
  const queue = [{ id: args.from, depth: 0 }];
2993
3012
  while (queue.length > 0) {
2994
3013
  const curr = queue.shift();
2995
- if (visited.has(curr.id) || curr.depth > depth)
3014
+ if (visitedNodes.has(curr.id) || curr.depth > depth)
2996
3015
  continue;
2997
- visited.add(curr.id);
2998
- const node = (dag.nodes || []).find((n) => n.id.toLowerCase() === curr.id.toLowerCase());
3016
+ visitedNodes.add(curr.id);
3017
+ const node = N(dag).find((n) => ni(n).toLowerCase() === curr.id.toLowerCase());
2999
3018
  if (node)
3000
3019
  subgraph.nodes.push(node);
3001
- const edges = (dag.edges || []).filter((e) => e.source.toLowerCase() === curr.id.toLowerCase() || e.target.toLowerCase() === curr.id.toLowerCase());
3020
+ const edges = E(dag).filter((e) => es(e).toLowerCase() === curr.id.toLowerCase() || et(e).toLowerCase() === curr.id.toLowerCase());
3002
3021
  for (const e of edges) {
3003
- subgraph.edges.push(e);
3004
- const next = e.source.toLowerCase() === curr.id.toLowerCase() ? e.target : e.source;
3005
- if (!visited.has(next))
3022
+ const edgeKey = `${es(e)}→${et(e)}`;
3023
+ if (!visitedEdges.has(edgeKey)) {
3024
+ visitedEdges.add(edgeKey);
3025
+ subgraph.edges.push(e);
3026
+ }
3027
+ const next = es(e).toLowerCase() === curr.id.toLowerCase() ? et(e) : es(e);
3028
+ if (!visitedNodes.has(next))
3006
3029
  queue.push({ id: next, depth: curr.depth + 1 });
3007
3030
  }
3008
3031
  }
@@ -3014,19 +3037,21 @@ function serve(dir = ".") {
3014
3037
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
3015
3038
  const q = (args.q || "").toLowerCase();
3016
3039
  const type = (args.type || "").toLowerCase();
3017
- const results = (dag.nodes || []).filter((n) => n.id.toLowerCase().includes(q) && (!type || (n.type || "").toLowerCase().includes(type)));
3040
+ const results = N(dag).filter((n) => ni(n).toLowerCase().includes(q) && (!type || nt(n).toLowerCase().includes(type)));
3018
3041
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(results) }] } };
3019
3042
  }
3020
3043
  if (name === "summary") {
3021
3044
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
3022
3045
  if (!dag)
3023
3046
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
3047
+ const tk = dag.tk || dag.tokens || {};
3024
3048
  const s = {
3025
- project: dag.project,
3026
- nodes: (dag.nodes || []).length,
3027
- edges: (dag.edges || []).length,
3028
- files: dag.files || dag.count || 0,
3029
- compiled: dag.compiled_at
3049
+ project: P(dag),
3050
+ nodes: N(dag).length,
3051
+ edges: E(dag).length,
3052
+ version: dag.v || dag.version || "",
3053
+ savings: tk.sv || tk.savings_pct || 0,
3054
+ method: tk.m || tk.method || ""
3030
3055
  };
3031
3056
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(s) }] } };
3032
3057
  }
@@ -3034,14 +3059,14 @@ function serve(dir = ".") {
3034
3059
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
3035
3060
  if (!dag)
3036
3061
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
3037
- const node = (dag.nodes || []).find((n) => n.id.toLowerCase() === (args.entity || "").toLowerCase());
3062
+ const node = N(dag).find((n) => ni(n).toLowerCase() === (args.entity || "").toLowerCase());
3038
3063
  if (!node)
3039
3064
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Entity not found" } };
3040
3065
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({
3041
- entity: node.id,
3042
- properties: node.properties || {},
3043
- states: node.states || [],
3044
- lifecycle: node.lifecycle || []
3066
+ entity: ni(node),
3067
+ properties: np(node),
3068
+ states: ns(node),
3069
+ lifecycle: nl(node)
3045
3070
  }) }] } };
3046
3071
  }
3047
3072
  return { jsonrpc: "2.0", id, error: { code: 404, message: `Unknown tool: ${name}` } };
@@ -3071,7 +3096,11 @@ function resolvePath2(p) {
3071
3096
  const resolved = p.startsWith("/") ? p : join2(process.cwd(), p);
3072
3097
  if (!p.startsWith("/") && !p.startsWith("~")) {
3073
3098
  const rel = resolve2(process.cwd(), p);
3074
- if (!rel.startsWith(process.cwd() + "/") && rel !== process.cwd()) {
3099
+ const cwd = process.cwd();
3100
+ const isDescendant = rel.startsWith(cwd + "/");
3101
+ const isSelf = rel === cwd;
3102
+ const isAncestor = cwd.startsWith(rel + "/");
3103
+ if (!isDescendant && !isSelf && !isAncestor) {
3075
3104
  throw new Error(`Path traversal blocked: ${p}`);
3076
3105
  }
3077
3106
  return rel;
@@ -3122,7 +3151,9 @@ program2.command("validate [dir]").action((d = ".") => {
3122
3151
  const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3123
3152
  for (const p of projects) {
3124
3153
  found = true;
3125
- const pd = join2(dd, p, "specs");
3154
+ const pd = join2(dd, p);
3155
+ if (!existsSync2(join2(pd, "SPEC.dog")))
3156
+ continue;
3126
3157
  const files = existsSync2(pd) ? readdirSync2(pd).filter((f) => f.endsWith(".dog")) : [];
3127
3158
  const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
3128
3159
  const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
@@ -3139,10 +3170,10 @@ program2.command("validate [dir]").action((d = ".") => {
3139
3170
  if (!found)
3140
3171
  console.log(source_default.yellow("No projects found. Run: spec init <project>"));
3141
3172
  });
3142
- program2.command("init <project>").action((p) => {
3173
+ program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data-model.dog").action((p, opts) => {
3143
3174
  const d = join2(process.cwd(), "specs", p);
3144
3175
  mkdirSync(d, { recursive: true });
3145
- const tmpl = {
3176
+ const full = {
3146
3177
  "SPEC.dog": `# Project
3147
3178
 
3148
3179
  ## Product
@@ -3174,6 +3205,19 @@ program2.command("init <project>").action((p) => {
3174
3205
  |---|---|---|
3175
3206
  `
3176
3207
  };
3208
+ const minimal = {
3209
+ "SPEC.dog": `# Project
3210
+
3211
+ ## Product
3212
+
3213
+ `,
3214
+ "data-model.dog": `# Data Model
3215
+
3216
+ ## Entities
3217
+
3218
+ `
3219
+ };
3220
+ const tmpl = opts.minimal ? minimal : full;
3177
3221
  for (const [f, c] of Object.entries(tmpl)) {
3178
3222
  writeFileSync(join2(d, f), c);
3179
3223
  console.log(source_default.green(` ✓ ${f}`));
@@ -3192,7 +3236,7 @@ program2.command("list").action(() => {
3192
3236
  console.log(source_default.bold(`
3193
3237
  ${d}/`));
3194
3238
  for (const p of projects) {
3195
- const sp = join2(dd, p, "specs");
3239
+ const sp = join2(dd, p);
3196
3240
  const n = existsSync2(sp) ? readdirSync2(sp).filter((f) => f.endsWith(".dog")).length : 0;
3197
3241
  console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
3198
3242
  }
@@ -3215,7 +3259,9 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3215
3259
  continue;
3216
3260
  const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3217
3261
  for (const p of projects) {
3218
- const pd = join2(dd, p, "specs");
3262
+ const pd = join2(dd, p);
3263
+ if (!existsSync2(join2(pd, "SPEC.dog")))
3264
+ continue;
3219
3265
  if (!existsSync2(pd))
3220
3266
  continue;
3221
3267
  const files = readdirSync2(pd).filter((f) => f.endsWith(".dog")).sort();
@@ -3223,59 +3269,79 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3223
3269
  continue;
3224
3270
  found = true;
3225
3271
  const sources = {};
3226
- let sourceBytes = 0;
3227
- const hash = createHash("sha256");
3272
+ let sourceBytes = 0, contentBytes = 0;
3228
3273
  for (const f of files) {
3229
3274
  const content = readFileSync2(join2(pd, f), "utf-8");
3230
3275
  sources[f] = content;
3231
- sourceBytes += Buffer.byteLength(content, "utf-8");
3232
- hash.update(content);
3233
- hash.update(`
3234
- `);
3276
+ const bytes = Buffer.byteLength(content, "utf-8");
3277
+ sourceBytes += bytes;
3278
+ if (bytes >= 100)
3279
+ contentBytes += bytes;
3235
3280
  }
3236
- const integrity = { sha256: hash.digest("hex"), source_files: files.length, source_bytes: sourceBytes };
3237
3281
  const sourceTokens = Math.round(sourceBytes / 4);
3282
+ const contentTokens = Math.round(contentBytes / 4);
3238
3283
  const nodes = [], edges = [];
3239
3284
  for (const f of files) {
3240
3285
  const ast = parse(sources[f]);
3241
3286
  for (const section of ast.sections) {
3242
3287
  for (const block of section.blocks) {
3243
- if (block.kind === "entity") {
3288
+ if (block.kind === "entity" || block.kind === "event" || block.kind === "prediction") {
3289
+ const compactProps = {};
3290
+ for (const [key, val] of Object.entries(block.properties)) {
3291
+ let enc = "";
3292
+ const t = val.type || "string";
3293
+ if (t === "string")
3294
+ enc = "s";
3295
+ else if (t === "number")
3296
+ enc = "n";
3297
+ else if (t === "boolean")
3298
+ enc = "b";
3299
+ else if (t === "enum")
3300
+ enc = "e";
3301
+ else if (t === "json")
3302
+ enc = "j";
3303
+ else
3304
+ enc = t[0];
3305
+ if (val.required)
3306
+ enc += "!";
3307
+ compactProps[key] = enc;
3308
+ }
3244
3309
  nodes.push({
3245
- id: block.name,
3246
- type: block.type,
3247
- description: block.description || "",
3248
- file: f,
3249
- properties: Object.keys(block.properties).length,
3250
- states: block.states || [],
3251
- chars: section.content?.length || 0
3310
+ i: block.name,
3311
+ t: block.type,
3312
+ g: block.kind,
3313
+ d: block.description || "",
3314
+ p: compactProps,
3315
+ s: block.states || [],
3316
+ l: block.lifecycle || []
3252
3317
  });
3253
3318
  }
3254
3319
  if (block.kind === "relationship") {
3255
3320
  edges.push({
3256
- source: block.source,
3257
- target: block.target,
3258
- verb: block.verb,
3259
- cardinality: block.cardinality,
3260
- required: block.required,
3261
- file: f
3321
+ s: block.source,
3322
+ t: block.target,
3323
+ v: block.verb,
3324
+ d: block.description || "",
3325
+ c: block.cardinality,
3326
+ r: block.required
3262
3327
  });
3263
3328
  }
3264
3329
  }
3265
3330
  }
3266
3331
  }
3267
- const dag = { version: "1.2", project: p, compiled_at: new Date().toISOString(), compiler: `dotdog@${pkg.version}`, integrity, nodes, edges };
3332
+ const dag = { v: "1.4", p, c: `dotdog@${pkg.version}`, n: nodes, e: edges };
3268
3333
  const dagJson = JSON.stringify(dag);
3269
3334
  const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
3270
- const savingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
3335
+ const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
3336
+ const contentSavingsPct = contentTokens > 0 ? Math.round((1 - dagTokens / contentTokens) * 1000) / 10 : 0;
3271
3337
  const savingsTokens = sourceTokens - dagTokens;
3272
- const outPath = opts.output || join2(pd, "..", `${p}.dag`);
3273
- const report = { ...dag, tokens: { source_total: sourceTokens, dag_total: dagTokens, savings_pct: savingsPct, savings_tokens: savingsTokens } };
3338
+ const outPath = opts.output || join2(pd, `${p}.dag`);
3339
+ const tokens = { m: "chars/4", st: sourceTokens, ct: contentTokens, dt: dagTokens, sv: allSavingsPct, cs: contentSavingsPct, saved: savingsTokens };
3340
+ const report = { ...dag, tk: tokens };
3274
3341
  writeFileSync(outPath, JSON.stringify(report, null, 2));
3275
3342
  console.log(source_default.green(` ✓ ${outPath}`));
3276
3343
  console.log(source_default.gray(` ${nodes.length} nodes, ${edges.length} edges, ${files.length} files`));
3277
- console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${savingsPct}% savings, ${savingsTokens} tokens saved)`));
3278
- console.log(source_default.gray(` sha256: ${integrity.sha256.substring(0, 12)}...`));
3344
+ console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${allSavingsPct}% savings, ${contentSavingsPct}% content-only, ${savingsTokens} saved)`));
3279
3345
  }
3280
3346
  }
3281
3347
  if (!found)
@@ -3327,7 +3393,9 @@ Spec Analysis
3327
3393
  for (const p of projects) {
3328
3394
  if (opts.project && p !== opts.project)
3329
3395
  continue;
3330
- const pd = join2(dd, p, "specs");
3396
+ const pd = join2(dd, p);
3397
+ if (!existsSync2(join2(pd, "SPEC.dog")))
3398
+ continue;
3331
3399
  if (!existsSync2(pd))
3332
3400
  continue;
3333
3401
  const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
@@ -3407,7 +3475,7 @@ program2.command("generate [dir]").description("Generate missing spec files from
3407
3475
  for (const p of projects) {
3408
3476
  if (opts.project && p !== opts.project)
3409
3477
  continue;
3410
- const pd = join2(dd, p, "specs");
3478
+ const pd = join2(dd, p);
3411
3479
  const sp = join2(pd, "SPEC.dog");
3412
3480
  if (existsSync2(sp)) {
3413
3481
  specContent = readFileSync2(sp, "utf-8");
@@ -3525,7 +3593,9 @@ program2.command("staleness [dir]").action((d = ".") => {
3525
3593
  continue;
3526
3594
  const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3527
3595
  for (const p of projects) {
3528
- const pd = join2(dd, p, "specs");
3596
+ const pd = join2(dd, p);
3597
+ if (!existsSync2(join2(pd, "SPEC.dog")))
3598
+ continue;
3529
3599
  if (!existsSync2(pd))
3530
3600
  continue;
3531
3601
  const planFile = join2(pd, "plan.dog");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotdog",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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",
@@ -15,29 +15,25 @@
15
15
  "LICENSE"
16
16
  ],
17
17
  "keywords": [
18
+ "spec",
18
19
  "specification",
19
- "cli",
20
+ "ai",
21
+ "agent",
20
22
  "mcp",
23
+ "documentation",
24
+ "cli",
25
+ "dogfood",
26
+ "spec-driven-development",
21
27
  "graph",
22
- "dag",
23
- "yaml",
24
28
  "markdown",
25
- "spec-driven-development"
29
+ "yaml",
30
+ "dotdog",
31
+ "dag",
32
+ "dog"
26
33
  ],
27
34
  "license": "MIT",
28
35
  "author": "specdog",
29
- "homepage": "https://github.com/specdog/dotdog#readme",
30
- "bugs": {
31
- "url": "https://github.com/specdog/dotdog/issues"
32
- },
33
- "repository": {
34
- "type": "git",
35
- "url": "https://github.com/specdog/dotdog.git",
36
- "directory": "packages/dotdog"
37
- },
38
- "engines": {
39
- "node": ">=20"
40
- },
36
+ "repository": "github:specdog/dotdog",
41
37
  "dependencies": {
42
38
  "commander": "^15.0.0",
43
39
  "chalk": "^5.6.0"
package/CHANGELOG.md DELETED
@@ -1,35 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.3.1
4
-
5
- - Fix npm packaging: homepage, bugs, engines, repository fields
6
- - Trim keywords to 8 specific terms
7
- - Ship correct README, LICENSE, and CHANGELOG in package
8
- - CLI help text uses colons, no em dashes
9
-
10
- ## 0.3.0
11
-
12
- - `dotdog analyze` — deep project analysis: score, gaps, entity audit
13
- - `dotdog generate` — generate missing spec files from SPEC.dog
14
- - `dotdog simulate` — run simulation scenarios
15
- - `.dag` v1.2: provable token savings, typed nodes and edges
16
- - VS Code extension: syntax highlighting for .dog files
17
- - GitHub Pages and llms.txt for AI agent discoverability
18
- - `dotdog serve` MCP server: getEntity, traverse, search, schema, summary
19
- - Path traversal guard, traverse depth cap, serve hardening
20
-
21
- ## 0.2.0
22
-
23
- - `dotdog compile` — compile .dog files to .dag graph
24
- - `dotdog visualize` — Mermaid graph output with --save flag
25
- - MCP server: expose .dag graph to AI agents over stdio
26
- - `dotdog staleness` — detect drift between spec and reality
27
- - `dotdog init` — scaffold new spec genome projects
28
- - `dotdog list` — list all projects and .dog file counts
29
-
30
- ## 0.1.0
31
-
32
- - `.dog` file format v1.0
33
- - `.dag` file format v1.0
34
- - `dotdog validate` — validate spec completeness
35
- - `dotdog parse` — parse .dog files
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 specdog
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.