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.
- package/README.md +52 -106
- package/dist/cli.js +136 -66
- package/package.json +13 -17
- package/CHANGELOG.md +0 -35
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,130 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# spec-platform
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://www.npmjs.com/package/dotdog)
|
|
5
|
-
[](https://github.com/specdog/dotdog/blob/main/LICENSE)
|
|
6
|
-
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
## Quick Start
|
|
96
32
|
|
|
97
|
-
|
|
33
|
+
```bash
|
|
34
|
+
bun install
|
|
35
|
+
cd projects/spec-platform/specs
|
|
98
36
|
|
|
99
|
-
|
|
37
|
+
# Validate our own spec (dogfood)
|
|
38
|
+
bun ../../../packages/spec-cli/src/index.ts validate ../..
|
|
100
39
|
|
|
101
|
-
|
|
102
|
-
|
|
40
|
+
# List projects
|
|
41
|
+
bun ../../../packages/spec-cli/src/index.ts list
|
|
103
42
|
```
|
|
104
43
|
|
|
105
|
-
##
|
|
44
|
+
## $0 Stack
|
|
106
45
|
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
##
|
|
56
|
+
## The Spec Graph
|
|
111
57
|
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
```
|
|
70
|
+
spec validate → 43% complete
|
|
129
71
|
|
|
130
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
3014
|
+
if (visitedNodes.has(curr.id) || curr.depth > depth)
|
|
2996
3015
|
continue;
|
|
2997
|
-
|
|
2998
|
-
const node = (dag
|
|
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
|
|
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
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
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
|
|
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
|
|
3026
|
-
nodes: (dag
|
|
3027
|
-
edges: (dag
|
|
3028
|
-
|
|
3029
|
-
|
|
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
|
|
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
|
|
3042
|
-
properties: node
|
|
3043
|
-
states: node
|
|
3044
|
-
lifecycle: node
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
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
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
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
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
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 = {
|
|
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
|
|
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,
|
|
3273
|
-
const
|
|
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 (${
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
29
|
+
"yaml",
|
|
30
|
+
"dotdog",
|
|
31
|
+
"dag",
|
|
32
|
+
"dog"
|
|
26
33
|
],
|
|
27
34
|
"license": "MIT",
|
|
28
35
|
"author": "specdog",
|
|
29
|
-
"
|
|
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.
|