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 +35 -0
- package/LICENSE +21 -0
- package/README.md +106 -52
- package/dist/cli.js +272 -26
- package/package.json +21 -9
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
|
-
#
|
|
1
|
+
# dotdog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/dotdog)
|
|
4
|
+
[](https://www.npmjs.com/package/dotdog)
|
|
5
|
+
[](https://github.com/specdog/dotdog/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/specdog/dotdog/actions)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
> **Feed the dog. Ship with specs.** Write .dog specs. Dog checks them. AI agents fetch them.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g dotdog
|
|
9
14
|
```
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
Requires Node.js >= 20.
|
|
12
17
|
|
|
13
|
-
##
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
68
|
+
### `.dag` : Machine-Compiled Graph
|
|
32
69
|
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
bun ../../../packages/spec-cli/src/index.ts validate ../..
|
|
72
|
+
## MCP Server : AI Agent Integration
|
|
39
73
|
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
87
|
+
## Dogfood
|
|
57
88
|
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
+
## VS Code Extension
|
|
66
98
|
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
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}
|
|
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)}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3230
|
-
file: f,
|
|
3231
|
-
section: section.heading
|
|
3261
|
+
file: f
|
|
3232
3262
|
});
|
|
3233
3263
|
}
|
|
3234
3264
|
}
|
|
3235
3265
|
}
|
|
3236
3266
|
}
|
|
3237
|
-
|
|
3238
|
-
const
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
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}
|
|
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
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
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
|
-
"
|
|
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"
|