dotdog 0.2.5 → 0.3.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
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 ADDED
@@ -0,0 +1,21 @@
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.
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
@@ -2535,8 +2535,9 @@ var source_default = chalk;
2535
2535
 
2536
2536
  // src/cli.ts
2537
2537
  import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, mkdirSync, writeFileSync } from "fs";
2538
- import { join as join2 } from "path";
2538
+ import { join as join2, resolve as resolve2 } from "path";
2539
2539
  import { homedir as homedir2 } from "os";
2540
+ import { createHash } from "crypto";
2540
2541
 
2541
2542
  // src/parser.ts
2542
2543
  function parse(source) {
@@ -2896,13 +2897,21 @@ function parseInlineObject(value) {
2896
2897
  }
2897
2898
  // src/serve.ts
2898
2899
  import { existsSync, readdirSync, readFileSync } from "fs";
2899
- import { join } from "path";
2900
+ import { join, resolve } from "path";
2900
2901
  import { homedir } from "os";
2901
2902
  import * as readline from "readline";
2902
2903
  function resolvePath(p) {
2903
2904
  if (p.startsWith("~"))
2904
2905
  p = join(homedir(), p.slice(1));
2905
- return p.startsWith("/") ? p : join(process.cwd(), p);
2906
+ const resolved = p.startsWith("/") ? p : join(process.cwd(), p);
2907
+ if (!p.startsWith("/") && !p.startsWith("~")) {
2908
+ const rel = resolve(process.cwd(), p);
2909
+ if (!rel.startsWith(process.cwd() + "/") && rel !== process.cwd()) {
2910
+ throw new Error(`Path traversal blocked: ${p}`);
2911
+ }
2912
+ return rel;
2913
+ }
2914
+ return resolved;
2906
2915
  }
2907
2916
  function serve(dir = ".") {
2908
2917
  const root = resolvePath(dir);
@@ -2977,7 +2986,7 @@ function serve(dir = ".") {
2977
2986
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
2978
2987
  if (!dag)
2979
2988
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
2980
- const depth = args.depth || 1;
2989
+ const depth = Math.min(Math.max(1, args.depth || 1), 20);
2981
2990
  const visited = new Set;
2982
2991
  const subgraph = { nodes: [], edges: [] };
2983
2992
  const queue = [{ id: args.from, depth: 0 }];
@@ -3059,7 +3068,15 @@ function serve(dir = ".") {
3059
3068
  function resolvePath2(p) {
3060
3069
  if (p.startsWith("~"))
3061
3070
  p = join2(homedir2(), p.slice(1));
3062
- return p.startsWith("/") ? p : join2(process.cwd(), p);
3071
+ const resolved = p.startsWith("/") ? p : join2(process.cwd(), p);
3072
+ if (!p.startsWith("/") && !p.startsWith("~")) {
3073
+ const rel = resolve2(process.cwd(), p);
3074
+ if (!rel.startsWith(process.cwd() + "/") && rel !== process.cwd()) {
3075
+ throw new Error(`Path traversal blocked: ${p}`);
3076
+ }
3077
+ return rel;
3078
+ }
3079
+ return resolved;
3063
3080
  }
3064
3081
  function parseSections2(markdown) {
3065
3082
  const lines = markdown.split(`
@@ -3094,9 +3111,10 @@ function parseSections2(markdown) {
3094
3111
  }
3095
3112
  var program2 = new Command;
3096
3113
  var pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
3097
- program2.name("spec").alias("dotdog").description("The spec dog validate, analyze, generate .dog files").version(pkg.version);
3114
+ program2.name("spec").alias("dotdog").description("CLI for structured software specs : validate .dog, compile .dag, query via MCP").version(pkg.version);
3098
3115
  program2.command("validate [dir]").action((d = ".") => {
3099
- const dirs = [join2(d, "projects"), join2(d, "specs")];
3116
+ const dir = resolvePath2(d);
3117
+ const dirs = [join2(dir, "projects"), join2(dir, "specs")];
3100
3118
  let found = false;
3101
3119
  for (const dd of dirs) {
3102
3120
  if (!existsSync2(dd))
@@ -3109,7 +3127,7 @@ program2.command("validate [dir]").action((d = ".") => {
3109
3127
  const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
3110
3128
  const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
3111
3129
  console.log(source_default.bold(`
3112
- ${p} ${files.length} .dog files, ${100 - Math.round((missing.length * 3 + optional.length) / 20 * 100)}% complete`));
3130
+ ${p} : ${files.length} .dog files, ${100 - Math.round((missing.length * 3 + optional.length) / 20 * 100)}% complete`));
3113
3131
  for (const f of files)
3114
3132
  console.log(source_default.gray(` ${f}`));
3115
3133
  if (missing.length)
@@ -3176,7 +3194,7 @@ ${d}/`));
3176
3194
  for (const p of projects) {
3177
3195
  const sp = join2(dd, p, "specs");
3178
3196
  const n = existsSync2(sp) ? readdirSync2(sp).filter((f) => f.endsWith(".dog")).length : 0;
3179
- console.log(` ${source_default.cyan(p)} ${n} .dog files`);
3197
+ console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
3180
3198
  }
3181
3199
  }
3182
3200
  });
@@ -3200,45 +3218,64 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3200
3218
  const pd = join2(dd, p, "specs");
3201
3219
  if (!existsSync2(pd))
3202
3220
  continue;
3203
- const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
3204
- const dag = { version: "1.1", project: p, compiled_at: new Date().toISOString(), nodes: [], edges: [], files: files.length };
3221
+ const files = readdirSync2(pd).filter((f) => f.endsWith(".dog")).sort();
3222
+ if (!files.length)
3223
+ continue;
3224
+ found = true;
3225
+ const sources = {};
3226
+ let sourceBytes = 0;
3227
+ const hash = createHash("sha256");
3205
3228
  for (const f of files) {
3206
3229
  const content = readFileSync2(join2(pd, f), "utf-8");
3207
- const ast = parse(content);
3230
+ sources[f] = content;
3231
+ sourceBytes += Buffer.byteLength(content, "utf-8");
3232
+ hash.update(content);
3233
+ hash.update(`
3234
+ `);
3235
+ }
3236
+ const integrity = { sha256: hash.digest("hex"), source_files: files.length, source_bytes: sourceBytes };
3237
+ const sourceTokens = Math.round(sourceBytes / 4);
3238
+ const nodes = [], edges = [];
3239
+ for (const f of files) {
3240
+ const ast = parse(sources[f]);
3208
3241
  for (const section of ast.sections) {
3209
3242
  for (const block of section.blocks) {
3210
3243
  if (block.kind === "entity") {
3211
- dag.nodes.push({
3244
+ nodes.push({
3212
3245
  id: block.name,
3213
3246
  type: block.type,
3214
3247
  description: block.description || "",
3215
3248
  file: f,
3216
- properties: block.properties,
3249
+ properties: Object.keys(block.properties).length,
3217
3250
  states: block.states || [],
3218
- lifecycle: block.lifecycle || [],
3219
3251
  chars: section.content?.length || 0
3220
3252
  });
3221
3253
  }
3222
3254
  if (block.kind === "relationship") {
3223
- dag.edges.push({
3255
+ edges.push({
3224
3256
  source: block.source,
3225
3257
  target: block.target,
3226
3258
  verb: block.verb,
3227
3259
  cardinality: block.cardinality,
3228
3260
  required: block.required,
3229
- cascade: block.cascade,
3230
- file: f,
3231
- section: section.heading
3261
+ file: f
3232
3262
  });
3233
3263
  }
3234
3264
  }
3235
3265
  }
3236
3266
  }
3237
- found = true;
3238
- const out = opts.output || join2(pd, "..", `${p}.dag`);
3239
- writeFileSync(out, JSON.stringify(dag, null, 2));
3240
- console.log(source_default.green(` ✓ ${out}`));
3241
- console.log(source_default.gray(` ${dag.nodes.length} nodes, ${dag.edges.length} edges, ${dag.files} files`));
3267
+ const dag = { version: "1.2", project: p, compiled_at: new Date().toISOString(), compiler: `dotdog@${pkg.version}`, integrity, nodes, edges };
3268
+ const dagJson = JSON.stringify(dag);
3269
+ const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
3270
+ const savingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
3271
+ 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 } };
3274
+ writeFileSync(outPath, JSON.stringify(report, null, 2));
3275
+ console.log(source_default.green(` ✓ ${outPath}`));
3276
+ 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)}...`));
3242
3279
  }
3243
3280
  }
3244
3281
  if (!found)
@@ -3266,7 +3303,7 @@ program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts)
3266
3303
  out += "```\n";
3267
3304
  if (opts.save) {
3268
3305
  const outFile = join2(dd, p, "..", `${p}.md`);
3269
- writeFileSync(outFile, `# ${p} Spec Graph
3306
+ writeFileSync(outFile, `# ${p} : Spec Graph
3270
3307
 
3271
3308
  ${out}`);
3272
3309
  console.log(source_default.green(` ✓ ${outFile}`));
@@ -3275,7 +3312,209 @@ ${out}`);
3275
3312
  }
3276
3313
  }
3277
3314
  });
3278
- program2.command("serve [dir]").description("MCP server expose .dag graph to AI agents over stdio").action((d = ".") => serve(d));
3315
+ program2.command("serve [dir]").description("MCP server : expose .dag graph to AI agents over stdio").action((d = ".") => serve(resolvePath2(d)));
3316
+ program2.command("analyze [dir]").description("Analyze a spec project : score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
3317
+ const dir = resolvePath2(d);
3318
+ const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3319
+ console.log(source_default.bold(`
3320
+ Spec Analysis
3321
+ `));
3322
+ let found = false;
3323
+ for (const dd of dirs) {
3324
+ if (!existsSync2(dd))
3325
+ continue;
3326
+ const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3327
+ for (const p of projects) {
3328
+ if (opts.project && p !== opts.project)
3329
+ continue;
3330
+ const pd = join2(dd, p, "specs");
3331
+ if (!existsSync2(pd))
3332
+ continue;
3333
+ const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
3334
+ if (!files.length)
3335
+ continue;
3336
+ found = true;
3337
+ console.log(source_default.bold(`
3338
+ ${p}`));
3339
+ console.log(" " + "─".repeat(50));
3340
+ const allEntities = [];
3341
+ const allRelationships = [];
3342
+ const analyses = [];
3343
+ for (const f of files) {
3344
+ const content = readFileSync2(join2(pd, f), "utf-8");
3345
+ const ast = parse(content);
3346
+ const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
3347
+ const rels = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "relationship"));
3348
+ allEntities.push(...entities);
3349
+ allRelationships.push(...rels);
3350
+ analyses.push({ file: f, sections: ast.sections.length, size: content.length, entities: entities.length, rels: rels.length });
3351
+ }
3352
+ const missingReq = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
3353
+ const missingOpt = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
3354
+ let score = 100 - missingReq.length * 15 - missingOpt.length * 5;
3355
+ const noDesc = allEntities.filter((e) => !e.description || e.description.length < 10).length;
3356
+ score = Math.max(0, score - noDesc * 3);
3357
+ const noProps = allEntities.filter((e) => Object.keys(e.properties).length === 0).length;
3358
+ score = Math.max(0, score - noProps * 5);
3359
+ const noStates = allEntities.filter((e) => e.states.length === 0).length;
3360
+ score = Math.max(0, score - noStates * 3);
3361
+ console.log(` ${files.length} files | ${score}% complete`);
3362
+ for (const a of analyses) {
3363
+ const detail = a.entities > 0 ? ` (${a.entities} entities, ${a.rels} rels)` : "";
3364
+ console.log(source_default.gray(` ${a.file} : ${a.sections} sections, ${(a.size / 1024).toFixed(1)}KB${detail}`));
3365
+ }
3366
+ const gaps = [];
3367
+ for (const f of missingReq)
3368
+ gaps.push(`\uD83D\uDD34 ${f}: Missing required file`);
3369
+ for (const f of missingOpt)
3370
+ gaps.push(`\uD83D\uDFE1 ${f}: Missing optional file`);
3371
+ const entityNames = new Set(allEntities.map((e) => e.name));
3372
+ for (const e of allEntities) {
3373
+ if (!e.description || e.description.length < 10)
3374
+ gaps.push(`\uD83D\uDFE1 ${e.name}: No description`);
3375
+ if (Object.keys(e.properties).length === 0)
3376
+ gaps.push(`\uD83D\uDFE1 ${e.name}: No properties defined`);
3377
+ if (e.states.length === 0)
3378
+ gaps.push(`\uD83D\uDD35 ${e.name}: No states defined`);
3379
+ }
3380
+ for (const r of allRelationships) {
3381
+ if (r.source && !entityNames.has(r.source))
3382
+ gaps.push(`\uD83D\uDFE1 Relationship: unknown source "${r.source}"`);
3383
+ if (r.target && !entityNames.has(r.target))
3384
+ gaps.push(`\uD83D\uDFE1 Relationship: unknown target "${r.target}"`);
3385
+ }
3386
+ if (gaps.length > 0) {
3387
+ console.log(source_default.bold(`
3388
+ Gaps (${gaps.length})`));
3389
+ for (const g of gaps)
3390
+ console.log(` ${g}`);
3391
+ } else
3392
+ console.log(source_default.green(`
3393
+ No gaps found.`));
3394
+ }
3395
+ }
3396
+ if (!found)
3397
+ console.log(source_default.yellow("No spec projects found. Run: dotdog init <project>"));
3398
+ });
3399
+ program2.command("generate [dir]").description("Generate missing spec files from SPEC.dog").option("-p, --project <name>").action((d = ".", opts) => {
3400
+ const dir = resolvePath2(d);
3401
+ const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3402
+ let specContent = "", specDir = "";
3403
+ for (const dd of dirs) {
3404
+ if (!existsSync2(dd))
3405
+ continue;
3406
+ const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3407
+ for (const p of projects) {
3408
+ if (opts.project && p !== opts.project)
3409
+ continue;
3410
+ const pd = join2(dd, p, "specs");
3411
+ const sp = join2(pd, "SPEC.dog");
3412
+ if (existsSync2(sp)) {
3413
+ specContent = readFileSync2(sp, "utf-8");
3414
+ specDir = pd;
3415
+ break;
3416
+ }
3417
+ }
3418
+ if (specContent)
3419
+ break;
3420
+ }
3421
+ if (!specContent) {
3422
+ console.log(source_default.red("No SPEC.dog found. Create one first."));
3423
+ return;
3424
+ }
3425
+ console.log(source_default.bold(`
3426
+ Spec Generator
3427
+ `));
3428
+ console.log(source_default.gray(` Source: ${specDir}/SPEC.dog
3429
+ `));
3430
+ const ast = parse(specContent);
3431
+ const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
3432
+ const uiStrings = [];
3433
+ for (const section of ast.sections) {
3434
+ const h = section.heading.toLowerCase();
3435
+ if (h.includes("what the user sees") || h.includes("screen")) {
3436
+ const text = section.blocks.filter((b) => b.kind === "prose").map((b) => b.content).join(`
3437
+ `);
3438
+ for (const m of text.match(/\[([^\]]+)\]/g) || [])
3439
+ uiStrings.push({ screen: section.heading, element: "button", text: m });
3440
+ for (const m of text.match(/"([^"]+)"/g) || [])
3441
+ uiStrings.push({ screen: section.heading, element: "label", text: m });
3442
+ }
3443
+ }
3444
+ if (!existsSync2(join2(specDir, "data-model.dog")) && entities.length > 0) {
3445
+ let dm = `# Data Model
3446
+
3447
+ ## Core Entities
3448
+
3449
+ `;
3450
+ for (const e of entities) {
3451
+ dm += `### Entity: ${e.name}
3452
+
3453
+ ${e.description || "No description."}
3454
+
3455
+ `;
3456
+ dm += "```yaml\n";
3457
+ dm += `entity: ${e.name}
3458
+ `;
3459
+ dm += `type: entity
3460
+ `;
3461
+ dm += `properties:
3462
+ `;
3463
+ for (const [k, v] of Object.entries(e.properties)) {
3464
+ dm += ` ${k}:
3465
+ `;
3466
+ dm += ` type: ${v.type}
3467
+ `;
3468
+ if (v.required)
3469
+ dm += ` required: true
3470
+ `;
3471
+ }
3472
+ if (e.states.length > 0)
3473
+ dm += `states: [${e.states.join(", ")}]
3474
+ `;
3475
+ dm += "```\n\n";
3476
+ }
3477
+ writeFileSync(join2(specDir, "data-model.dog"), dm);
3478
+ console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
3479
+ }
3480
+ if (!existsSync2(join2(specDir, "COPY.dog")) && uiStrings.length > 0) {
3481
+ let copy = `# App Copy
3482
+
3483
+ | Screen | Element | Copy |
3484
+ |--------|---------|------|
3485
+ `;
3486
+ for (const s of uiStrings)
3487
+ copy += `| ${s.screen} | ${s.element} | ${s.text} |
3488
+ `;
3489
+ writeFileSync(join2(specDir, "COPY.dog"), copy);
3490
+ console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
3491
+ }
3492
+ if (!existsSync2(join2(specDir, "INDEX.dog"))) {
3493
+ let idx = `# INDEX
3494
+
3495
+ | You are... | Start here | Then... |
3496
+ |------------|-----------|---------|
3497
+ `;
3498
+ idx += `| Developer | SPEC.dog | data-model.dog → plan.dog |
3499
+ `;
3500
+ idx += `| AI agent | data-model.dog | COPY.dog → SPEC.dog |
3501
+ `;
3502
+ idx += `| Designer | SPEC.dog | COPY.dog |
3503
+ `;
3504
+ writeFileSync(join2(specDir, "INDEX.dog"), idx);
3505
+ console.log(source_default.green(" ✓ INDEX.dog"));
3506
+ }
3507
+ console.log(source_default.bold(`
3508
+ Run dotdog validate to verify.
3509
+ `));
3510
+ });
3511
+ program2.command("simulate <scenario>").description("Run a simulation scenario (phase 1 stub)").option("-p, --project <name>", "Project name", "default").action((scenario, opts) => {
3512
+ console.log(source_default.bold(`
3513
+ Simulation: ${scenario} (project: ${opts.project})
3514
+ `));
3515
+ console.log(source_default.gray("Simulation engine : reads SPEC.dog scenarios, walks through steps, checks pre/postconditions."));
3516
+ console.log(source_default.gray("Full engine coming in a future release."));
3517
+ });
3279
3518
  program2.command("staleness [dir]").action((d = ".") => {
3280
3519
  const dir = resolvePath2(d);
3281
3520
  const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
@@ -3334,6 +3573,13 @@ program2.command("staleness [dir]").action((d = ".") => {
3334
3573
  }
3335
3574
  }
3336
3575
  });
3576
+ program2.command("woof").action(() => {
3577
+ console.log(" / \\__");
3578
+ console.log(" ( @\\___");
3579
+ console.log(" / O");
3580
+ console.log(" / (_____/");
3581
+ console.log("/_____/ U");
3582
+ });
3337
3583
  program2.parse();
3338
3584
  export {
3339
3585
  parseToJSON,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dotdog",
3
- "version": "0.2.5",
4
- "description": "The spec dog \u2014 validate, analyze, parse, and generate .dog spec genome files",
3
+ "version": "0.3.1",
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",
7
7
  "bin": {
@@ -15,17 +15,29 @@
15
15
  "LICENSE"
16
16
  ],
17
17
  "keywords": [
18
- "spec",
19
- "dogfood",
20
18
  "specification",
21
- "ai",
22
- "documentation",
23
- "dotdog",
24
- "dog"
19
+ "cli",
20
+ "mcp",
21
+ "graph",
22
+ "dag",
23
+ "yaml",
24
+ "markdown",
25
+ "spec-driven-development"
25
26
  ],
26
27
  "license": "MIT",
27
28
  "author": "specdog",
28
- "repository": "github:specdog/dotdog",
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
+ },
29
41
  "dependencies": {
30
42
  "commander": "^15.0.0",
31
43
  "chalk": "^5.6.0"