dotdog 0.3.1 → 0.3.3
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 +244 -73
- 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;
|
|
@@ -2632,6 +2636,15 @@ function parseBlocks(lines, start, end) {
|
|
|
2632
2636
|
continue;
|
|
2633
2637
|
}
|
|
2634
2638
|
}
|
|
2639
|
+
const predMatch = line.match(/^###\s+Prediction:\s*(.+)/);
|
|
2640
|
+
if (predMatch) {
|
|
2641
|
+
const result = parseStructuredBlock(lines, i, end, "prediction", predMatch[1]);
|
|
2642
|
+
if (result) {
|
|
2643
|
+
blocks.push(result.node);
|
|
2644
|
+
i = result.nextLine;
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2635
2648
|
if (/^\|.+\|/.test(line) && i + 1 < end && /^\|[-| ]+\|/.test(lines[i + 1])) {
|
|
2636
2649
|
const table = parseTable(lines, i, end);
|
|
2637
2650
|
if (table) {
|
|
@@ -2711,6 +2724,12 @@ function parseStructuredBlock(lines, start, end, kind, headerRest) {
|
|
|
2711
2724
|
nextLine: i
|
|
2712
2725
|
};
|
|
2713
2726
|
}
|
|
2727
|
+
if (kind === "prediction") {
|
|
2728
|
+
return {
|
|
2729
|
+
node: buildPredictionNode(headerRest, description, yaml, start, i),
|
|
2730
|
+
nextLine: i
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2714
2733
|
return null;
|
|
2715
2734
|
}
|
|
2716
2735
|
function buildEntityNode(name, description, yaml, lineStart, lineEnd) {
|
|
@@ -2731,7 +2750,11 @@ function buildEntityNode(name, description, yaml, lineStart, lineEnd) {
|
|
|
2731
2750
|
}
|
|
2732
2751
|
const states = Array.isArray(yaml.states) ? yaml.states : [];
|
|
2733
2752
|
const lifecycleStr = yaml.lifecycle || "";
|
|
2734
|
-
const
|
|
2753
|
+
const lifecycleParts = lifecycleStr ? lifecycleStr.split(/\s*→\s*/).map((s) => s.trim()) : [];
|
|
2754
|
+
const lifecycle = [];
|
|
2755
|
+
for (let si = 0;si < lifecycleParts.length - 1; si++) {
|
|
2756
|
+
lifecycle.push(`${lifecycleParts[si]} → ${lifecycleParts[si + 1]}`);
|
|
2757
|
+
}
|
|
2735
2758
|
return {
|
|
2736
2759
|
kind: "entity",
|
|
2737
2760
|
name,
|
|
@@ -2754,6 +2777,7 @@ function buildRelationshipNode(headerRest, description, yaml, lineStart, lineEnd
|
|
|
2754
2777
|
source,
|
|
2755
2778
|
target,
|
|
2756
2779
|
verb: yaml.verb || "connects",
|
|
2780
|
+
description: description || yaml.description || "",
|
|
2757
2781
|
cardinality: yaml.cardinality || "N:M",
|
|
2758
2782
|
required: yaml.required === true,
|
|
2759
2783
|
cascade: yaml.cascade || "none",
|
|
@@ -2778,6 +2802,22 @@ function buildEventNode(name, description, yaml, lineStart, lineEnd) {
|
|
|
2778
2802
|
lineEnd
|
|
2779
2803
|
};
|
|
2780
2804
|
}
|
|
2805
|
+
function buildPredictionNode(name, description, yaml, lineStart, lineEnd) {
|
|
2806
|
+
return {
|
|
2807
|
+
kind: "prediction",
|
|
2808
|
+
statement: name,
|
|
2809
|
+
description,
|
|
2810
|
+
trigger: yaml.trigger || "",
|
|
2811
|
+
timeframe: yaml.timeframe || "",
|
|
2812
|
+
confidence: typeof yaml.confidence === "number" ? yaml.confidence : 0,
|
|
2813
|
+
measurement: yaml.measurement || "",
|
|
2814
|
+
preconditions: Array.isArray(yaml.preconditions) ? yaml.preconditions : [],
|
|
2815
|
+
postconditions: Array.isArray(yaml.postconditions) ? yaml.postconditions : [],
|
|
2816
|
+
yaml,
|
|
2817
|
+
lineStart: lineStart + 1,
|
|
2818
|
+
lineEnd
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2781
2821
|
function parseTable(lines, start, end) {
|
|
2782
2822
|
const headerLine = lines[start];
|
|
2783
2823
|
const headers = headerLine.split("|").map((h) => h.trim()).filter(Boolean);
|
|
@@ -2850,15 +2890,25 @@ function parseSimpleYAML(lines) {
|
|
|
2850
2890
|
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
2851
2891
|
currentObj[nestedKey] = value.slice(1, -1).split(",").map((s) => s.trim());
|
|
2852
2892
|
} else {
|
|
2853
|
-
|
|
2854
|
-
if (inlineObj) {
|
|
2855
|
-
currentObj[nestedKey] = inlineObj;
|
|
2856
|
-
} else {
|
|
2857
|
-
currentObj[nestedKey] = value;
|
|
2858
|
-
}
|
|
2893
|
+
currentObj[nestedKey] = value;
|
|
2859
2894
|
}
|
|
2860
2895
|
continue;
|
|
2861
2896
|
}
|
|
2897
|
+
if (nestedMatch && !inNested) {
|
|
2898
|
+
const key = nestedMatch[1];
|
|
2899
|
+
const value = (nestedMatch[2] || "").trim();
|
|
2900
|
+
if (value === "true")
|
|
2901
|
+
result[key] = true;
|
|
2902
|
+
else if (value === "false")
|
|
2903
|
+
result[key] = false;
|
|
2904
|
+
else if (/^-?\d+(\.\d+)?$/.test(value))
|
|
2905
|
+
result[key] = parseFloat(value);
|
|
2906
|
+
else if (value.startsWith("[") && value.endsWith("]"))
|
|
2907
|
+
result[key] = value.slice(1, -1).split(",").map((s) => s.trim());
|
|
2908
|
+
else
|
|
2909
|
+
result[key] = value;
|
|
2910
|
+
continue;
|
|
2911
|
+
}
|
|
2862
2912
|
const deepMatch = line.match(/^\s{4}(\w[\w_]*):\s*(.+)?$/);
|
|
2863
2913
|
if (deepMatch && inNested && nestedKey && typeof currentObj[nestedKey] === "object") {
|
|
2864
2914
|
const deepNested = currentObj[nestedKey];
|
|
@@ -2906,13 +2956,27 @@ function resolvePath(p) {
|
|
|
2906
2956
|
const resolved = p.startsWith("/") ? p : join(process.cwd(), p);
|
|
2907
2957
|
if (!p.startsWith("/") && !p.startsWith("~")) {
|
|
2908
2958
|
const rel = resolve(process.cwd(), p);
|
|
2909
|
-
|
|
2959
|
+
const cwd = process.cwd();
|
|
2960
|
+
const isDescendant = rel.startsWith(cwd + "/");
|
|
2961
|
+
const isSelf = rel === cwd;
|
|
2962
|
+
const isAncestor = cwd.startsWith(rel + "/");
|
|
2963
|
+
if (!isDescendant && !isSelf && !isAncestor) {
|
|
2910
2964
|
throw new Error(`Path traversal blocked: ${p}`);
|
|
2911
2965
|
}
|
|
2912
2966
|
return rel;
|
|
2913
2967
|
}
|
|
2914
2968
|
return resolved;
|
|
2915
2969
|
}
|
|
2970
|
+
var N = (dag) => dag.n || dag.nodes || [];
|
|
2971
|
+
var E = (dag) => dag.e || dag.edges || [];
|
|
2972
|
+
var P = (dag) => dag.p || dag.project || "";
|
|
2973
|
+
var ni = (n) => n.i || n.id || "";
|
|
2974
|
+
var nt = (n) => n.t || n.type || "";
|
|
2975
|
+
var np = (n) => n.p || n.properties || {};
|
|
2976
|
+
var ns = (n) => n.s || n.states || [];
|
|
2977
|
+
var nl = (n) => n.l || n.lifecycle || [];
|
|
2978
|
+
var es = (e) => e.s || e.source || "";
|
|
2979
|
+
var et = (e) => e.t || e.target || "";
|
|
2916
2980
|
function serve(dir = ".") {
|
|
2917
2981
|
const root = resolvePath(dir);
|
|
2918
2982
|
const dagCache = new Map;
|
|
@@ -2954,7 +3018,7 @@ function serve(dir = ".") {
|
|
|
2954
3018
|
{ 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
3019
|
{ 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
3020
|
{ name: "listProjects", description: "List all projects", inputSchema: { type: "object", properties: {} } },
|
|
2957
|
-
{ name: "summary", description: "Get project summary: node count, edge count,
|
|
3021
|
+
{ name: "summary", description: "Get project summary: node count, edge count, token savings", inputSchema: { type: "object", properties: { project: { type: "string" } } } },
|
|
2958
3022
|
{ name: "schema", description: "Get full property schema for an entity", inputSchema: { type: "object", properties: { project: { type: "string" }, entity: { type: "string" } }, required: ["entity"] } }
|
|
2959
3023
|
]
|
|
2960
3024
|
}
|
|
@@ -2970,16 +3034,16 @@ function serve(dir = ".") {
|
|
|
2970
3034
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
2971
3035
|
if (!dag)
|
|
2972
3036
|
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
|
|
3037
|
+
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(N(dag)) }] } };
|
|
2974
3038
|
}
|
|
2975
3039
|
if (name === "getEntity") {
|
|
2976
3040
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
2977
3041
|
if (!dag)
|
|
2978
3042
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
2979
|
-
const node = (dag
|
|
3043
|
+
const node = N(dag).find((n) => ni(n).toLowerCase() === (args.name || "").toLowerCase());
|
|
2980
3044
|
if (!node)
|
|
2981
3045
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
|
|
2982
|
-
const edges = (dag
|
|
3046
|
+
const edges = E(dag).filter((e) => es(e).toLowerCase() === ni(node).toLowerCase() || et(e).toLowerCase() === ni(node).toLowerCase());
|
|
2983
3047
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({ ...node, edges }) }] } };
|
|
2984
3048
|
}
|
|
2985
3049
|
if (name === "traverse") {
|
|
@@ -2987,22 +3051,27 @@ function serve(dir = ".") {
|
|
|
2987
3051
|
if (!dag)
|
|
2988
3052
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
2989
3053
|
const depth = Math.min(Math.max(1, args.depth || 1), 20);
|
|
2990
|
-
const
|
|
3054
|
+
const visitedNodes = new Set;
|
|
3055
|
+
const visitedEdges = new Set;
|
|
2991
3056
|
const subgraph = { nodes: [], edges: [] };
|
|
2992
3057
|
const queue = [{ id: args.from, depth: 0 }];
|
|
2993
3058
|
while (queue.length > 0) {
|
|
2994
3059
|
const curr = queue.shift();
|
|
2995
|
-
if (
|
|
3060
|
+
if (visitedNodes.has(curr.id) || curr.depth > depth)
|
|
2996
3061
|
continue;
|
|
2997
|
-
|
|
2998
|
-
const node = (dag
|
|
3062
|
+
visitedNodes.add(curr.id);
|
|
3063
|
+
const node = N(dag).find((n) => ni(n).toLowerCase() === curr.id.toLowerCase());
|
|
2999
3064
|
if (node)
|
|
3000
3065
|
subgraph.nodes.push(node);
|
|
3001
|
-
const edges = (dag
|
|
3066
|
+
const edges = E(dag).filter((e) => es(e).toLowerCase() === curr.id.toLowerCase() || et(e).toLowerCase() === curr.id.toLowerCase());
|
|
3002
3067
|
for (const e of edges) {
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3068
|
+
const edgeKey = `${es(e)}→${et(e)}`;
|
|
3069
|
+
if (!visitedEdges.has(edgeKey)) {
|
|
3070
|
+
visitedEdges.add(edgeKey);
|
|
3071
|
+
subgraph.edges.push(e);
|
|
3072
|
+
}
|
|
3073
|
+
const next = es(e).toLowerCase() === curr.id.toLowerCase() ? et(e) : es(e);
|
|
3074
|
+
if (!visitedNodes.has(next))
|
|
3006
3075
|
queue.push({ id: next, depth: curr.depth + 1 });
|
|
3007
3076
|
}
|
|
3008
3077
|
}
|
|
@@ -3014,19 +3083,21 @@ function serve(dir = ".") {
|
|
|
3014
3083
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
3015
3084
|
const q = (args.q || "").toLowerCase();
|
|
3016
3085
|
const type = (args.type || "").toLowerCase();
|
|
3017
|
-
const results = (dag
|
|
3086
|
+
const results = N(dag).filter((n) => ni(n).toLowerCase().includes(q) && (!type || nt(n).toLowerCase().includes(type)));
|
|
3018
3087
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(results) }] } };
|
|
3019
3088
|
}
|
|
3020
3089
|
if (name === "summary") {
|
|
3021
3090
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
3022
3091
|
if (!dag)
|
|
3023
3092
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
3093
|
+
const tk = dag.tk || dag.tokens || {};
|
|
3024
3094
|
const s = {
|
|
3025
|
-
project: dag
|
|
3026
|
-
nodes: (dag
|
|
3027
|
-
edges: (dag
|
|
3028
|
-
|
|
3029
|
-
|
|
3095
|
+
project: P(dag),
|
|
3096
|
+
nodes: N(dag).length,
|
|
3097
|
+
edges: E(dag).length,
|
|
3098
|
+
version: dag.v || dag.version || "",
|
|
3099
|
+
savings: tk.sv || tk.savings_pct || 0,
|
|
3100
|
+
method: tk.m || tk.method || ""
|
|
3030
3101
|
};
|
|
3031
3102
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(s) }] } };
|
|
3032
3103
|
}
|
|
@@ -3034,14 +3105,14 @@ function serve(dir = ".") {
|
|
|
3034
3105
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
3035
3106
|
if (!dag)
|
|
3036
3107
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
3037
|
-
const node = (dag
|
|
3108
|
+
const node = N(dag).find((n) => ni(n).toLowerCase() === (args.entity || "").toLowerCase());
|
|
3038
3109
|
if (!node)
|
|
3039
3110
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Entity not found" } };
|
|
3040
3111
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({
|
|
3041
|
-
entity: node
|
|
3042
|
-
properties: node
|
|
3043
|
-
states: node
|
|
3044
|
-
lifecycle: node
|
|
3112
|
+
entity: ni(node),
|
|
3113
|
+
properties: np(node),
|
|
3114
|
+
states: ns(node),
|
|
3115
|
+
lifecycle: nl(node)
|
|
3045
3116
|
}) }] } };
|
|
3046
3117
|
}
|
|
3047
3118
|
return { jsonrpc: "2.0", id, error: { code: 404, message: `Unknown tool: ${name}` } };
|
|
@@ -3071,7 +3142,11 @@ function resolvePath2(p) {
|
|
|
3071
3142
|
const resolved = p.startsWith("/") ? p : join2(process.cwd(), p);
|
|
3072
3143
|
if (!p.startsWith("/") && !p.startsWith("~")) {
|
|
3073
3144
|
const rel = resolve2(process.cwd(), p);
|
|
3074
|
-
|
|
3145
|
+
const cwd = process.cwd();
|
|
3146
|
+
const isDescendant = rel.startsWith(cwd + "/");
|
|
3147
|
+
const isSelf = rel === cwd;
|
|
3148
|
+
const isAncestor = cwd.startsWith(rel + "/");
|
|
3149
|
+
if (!isDescendant && !isSelf && !isAncestor) {
|
|
3075
3150
|
throw new Error(`Path traversal blocked: ${p}`);
|
|
3076
3151
|
}
|
|
3077
3152
|
return rel;
|
|
@@ -3122,7 +3197,9 @@ program2.command("validate [dir]").action((d = ".") => {
|
|
|
3122
3197
|
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3123
3198
|
for (const p of projects) {
|
|
3124
3199
|
found = true;
|
|
3125
|
-
const pd = join2(dd, p
|
|
3200
|
+
const pd = join2(dd, p);
|
|
3201
|
+
if (!existsSync2(join2(pd, "SPEC.dog")))
|
|
3202
|
+
continue;
|
|
3126
3203
|
const files = existsSync2(pd) ? readdirSync2(pd).filter((f) => f.endsWith(".dog")) : [];
|
|
3127
3204
|
const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
|
|
3128
3205
|
const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
|
|
@@ -3139,10 +3216,10 @@ program2.command("validate [dir]").action((d = ".") => {
|
|
|
3139
3216
|
if (!found)
|
|
3140
3217
|
console.log(source_default.yellow("No projects found. Run: spec init <project>"));
|
|
3141
3218
|
});
|
|
3142
|
-
program2.command("init <project>").action((p) => {
|
|
3219
|
+
program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data-model.dog").action((p, opts) => {
|
|
3143
3220
|
const d = join2(process.cwd(), "specs", p);
|
|
3144
3221
|
mkdirSync(d, { recursive: true });
|
|
3145
|
-
const
|
|
3222
|
+
const full = {
|
|
3146
3223
|
"SPEC.dog": `# Project
|
|
3147
3224
|
|
|
3148
3225
|
## Product
|
|
@@ -3174,6 +3251,19 @@ program2.command("init <project>").action((p) => {
|
|
|
3174
3251
|
|---|---|---|
|
|
3175
3252
|
`
|
|
3176
3253
|
};
|
|
3254
|
+
const minimal = {
|
|
3255
|
+
"SPEC.dog": `# Project
|
|
3256
|
+
|
|
3257
|
+
## Product
|
|
3258
|
+
|
|
3259
|
+
`,
|
|
3260
|
+
"data-model.dog": `# Data Model
|
|
3261
|
+
|
|
3262
|
+
## Entities
|
|
3263
|
+
|
|
3264
|
+
`
|
|
3265
|
+
};
|
|
3266
|
+
const tmpl = opts.minimal ? minimal : full;
|
|
3177
3267
|
for (const [f, c] of Object.entries(tmpl)) {
|
|
3178
3268
|
writeFileSync(join2(d, f), c);
|
|
3179
3269
|
console.log(source_default.green(` ✓ ${f}`));
|
|
@@ -3192,7 +3282,7 @@ program2.command("list").action(() => {
|
|
|
3192
3282
|
console.log(source_default.bold(`
|
|
3193
3283
|
${d}/`));
|
|
3194
3284
|
for (const p of projects) {
|
|
3195
|
-
const sp = join2(dd, p
|
|
3285
|
+
const sp = join2(dd, p);
|
|
3196
3286
|
const n = existsSync2(sp) ? readdirSync2(sp).filter((f) => f.endsWith(".dog")).length : 0;
|
|
3197
3287
|
console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
|
|
3198
3288
|
}
|
|
@@ -3215,7 +3305,9 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3215
3305
|
continue;
|
|
3216
3306
|
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3217
3307
|
for (const p of projects) {
|
|
3218
|
-
const pd = join2(dd, p
|
|
3308
|
+
const pd = join2(dd, p);
|
|
3309
|
+
if (!existsSync2(join2(pd, "SPEC.dog")))
|
|
3310
|
+
continue;
|
|
3219
3311
|
if (!existsSync2(pd))
|
|
3220
3312
|
continue;
|
|
3221
3313
|
const files = readdirSync2(pd).filter((f) => f.endsWith(".dog")).sort();
|
|
@@ -3223,64 +3315,139 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3223
3315
|
continue;
|
|
3224
3316
|
found = true;
|
|
3225
3317
|
const sources = {};
|
|
3226
|
-
let sourceBytes = 0;
|
|
3227
|
-
const hash = createHash("sha256");
|
|
3318
|
+
let sourceBytes = 0, contentBytes = 0;
|
|
3228
3319
|
for (const f of files) {
|
|
3229
3320
|
const content = readFileSync2(join2(pd, f), "utf-8");
|
|
3230
3321
|
sources[f] = content;
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3322
|
+
const bytes = Buffer.byteLength(content, "utf-8");
|
|
3323
|
+
sourceBytes += bytes;
|
|
3324
|
+
if (bytes >= 100)
|
|
3325
|
+
contentBytes += bytes;
|
|
3235
3326
|
}
|
|
3236
|
-
const integrity = { sha256: hash.digest("hex"), source_files: files.length, source_bytes: sourceBytes };
|
|
3237
3327
|
const sourceTokens = Math.round(sourceBytes / 4);
|
|
3328
|
+
const contentTokens = Math.round(contentBytes / 4);
|
|
3238
3329
|
const nodes = [], edges = [];
|
|
3239
3330
|
for (const f of files) {
|
|
3240
3331
|
const ast = parse(sources[f]);
|
|
3241
3332
|
for (const section of ast.sections) {
|
|
3242
3333
|
for (const block of section.blocks) {
|
|
3243
|
-
if (block.kind === "entity") {
|
|
3334
|
+
if (block.kind === "entity" || block.kind === "event") {
|
|
3335
|
+
const compactProps = {};
|
|
3336
|
+
for (const [key, val] of Object.entries(block.properties || {})) {
|
|
3337
|
+
let enc = "";
|
|
3338
|
+
const t = val.type || "string";
|
|
3339
|
+
if (t === "string")
|
|
3340
|
+
enc = "s";
|
|
3341
|
+
else if (t === "number")
|
|
3342
|
+
enc = "n";
|
|
3343
|
+
else if (t === "boolean")
|
|
3344
|
+
enc = "b";
|
|
3345
|
+
else if (t === "enum")
|
|
3346
|
+
enc = "e";
|
|
3347
|
+
else if (t === "json")
|
|
3348
|
+
enc = "j";
|
|
3349
|
+
else
|
|
3350
|
+
enc = t[0];
|
|
3351
|
+
if (val.required)
|
|
3352
|
+
enc += "!";
|
|
3353
|
+
compactProps[key] = enc;
|
|
3354
|
+
}
|
|
3355
|
+
nodes.push({
|
|
3356
|
+
i: block.name || "",
|
|
3357
|
+
t: block.type || "",
|
|
3358
|
+
g: block.kind,
|
|
3359
|
+
d: block.description || "",
|
|
3360
|
+
p: compactProps,
|
|
3361
|
+
s: block.states || [],
|
|
3362
|
+
l: block.lifecycle || []
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
if (block.kind === "prediction") {
|
|
3244
3366
|
nodes.push({
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3367
|
+
i: block.statement || block.name || "",
|
|
3368
|
+
t: "prediction",
|
|
3369
|
+
g: "prediction",
|
|
3370
|
+
d: block.description || "",
|
|
3371
|
+
p: {},
|
|
3372
|
+
s: [],
|
|
3373
|
+
l: [],
|
|
3374
|
+
cf: block.confidence || 0,
|
|
3375
|
+
tf: block.timeframe || "",
|
|
3376
|
+
tg: block.trigger || "",
|
|
3377
|
+
ms: block.measurement || ""
|
|
3252
3378
|
});
|
|
3253
3379
|
}
|
|
3254
3380
|
if (block.kind === "relationship") {
|
|
3255
3381
|
edges.push({
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3382
|
+
s: block.source,
|
|
3383
|
+
t: block.target,
|
|
3384
|
+
v: block.verb,
|
|
3385
|
+
d: block.description || "",
|
|
3386
|
+
c: block.cardinality,
|
|
3387
|
+
r: block.required
|
|
3262
3388
|
});
|
|
3263
3389
|
}
|
|
3264
3390
|
}
|
|
3265
3391
|
}
|
|
3266
3392
|
}
|
|
3267
|
-
const dag = {
|
|
3393
|
+
const dag = { v: "1.4", p, c: `dotdog@${pkg.version}`, n: nodes, e: edges };
|
|
3268
3394
|
const dagJson = JSON.stringify(dag);
|
|
3269
3395
|
const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
|
|
3270
|
-
const
|
|
3396
|
+
const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
|
|
3397
|
+
const contentSavingsPct = contentTokens > 0 ? Math.round((1 - dagTokens / contentTokens) * 1000) / 10 : 0;
|
|
3271
3398
|
const savingsTokens = sourceTokens - dagTokens;
|
|
3272
|
-
const outPath = opts.output || join2(pd,
|
|
3273
|
-
const
|
|
3399
|
+
const outPath = opts.output || join2(pd, `${p}.dag`);
|
|
3400
|
+
const tokens = { m: "chars/4", st: sourceTokens, ct: contentTokens, dt: dagTokens, sv: allSavingsPct, cs: contentSavingsPct, saved: savingsTokens };
|
|
3401
|
+
const report = { ...dag, tk: tokens };
|
|
3274
3402
|
writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
3275
3403
|
console.log(source_default.green(` ✓ ${outPath}`));
|
|
3276
3404
|
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)}...`));
|
|
3405
|
+
console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${allSavingsPct}% savings, ${contentSavingsPct}% content-only, ${savingsTokens} saved)`));
|
|
3279
3406
|
}
|
|
3280
3407
|
}
|
|
3281
3408
|
if (!found)
|
|
3282
3409
|
console.log(source_default.yellow("No projects found."));
|
|
3283
3410
|
});
|
|
3411
|
+
program2.command("tokens [dir]").action((d = ".") => {
|
|
3412
|
+
const dir = resolvePath2(d);
|
|
3413
|
+
const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
|
|
3414
|
+
let found = false;
|
|
3415
|
+
for (const dd of dirs) {
|
|
3416
|
+
if (!existsSync2(dd))
|
|
3417
|
+
continue;
|
|
3418
|
+
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3419
|
+
for (const p of projects) {
|
|
3420
|
+
const pd = join2(dd, p);
|
|
3421
|
+
if (!existsSync2(join2(pd, "SPEC.dog")))
|
|
3422
|
+
continue;
|
|
3423
|
+
const dagFile = join2(pd, `${p}.dag`);
|
|
3424
|
+
if (!existsSync2(dagFile))
|
|
3425
|
+
continue;
|
|
3426
|
+
found = true;
|
|
3427
|
+
const dogFiles = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
|
|
3428
|
+
let sourceBytes = 0, contentBytes = 0;
|
|
3429
|
+
for (const f of dogFiles) {
|
|
3430
|
+
const bytes = Buffer.byteLength(readFileSync2(join2(pd, f), "utf-8"), "utf-8");
|
|
3431
|
+
sourceBytes += bytes;
|
|
3432
|
+
if (bytes >= 100)
|
|
3433
|
+
contentBytes += bytes;
|
|
3434
|
+
}
|
|
3435
|
+
const dagBytes = Buffer.byteLength(readFileSync2(dagFile, "utf-8"), "utf-8");
|
|
3436
|
+
const savings = sourceBytes > 0 ? Math.round((1 - dagBytes / sourceBytes) * 1000) / 10 : 0;
|
|
3437
|
+
console.log(source_default.bold(`
|
|
3438
|
+
${p}`));
|
|
3439
|
+
console.log(source_default.gray(` ${dogFiles.length} .dog files: ${sourceBytes} bytes`));
|
|
3440
|
+
console.log(source_default.gray(` .dag file: ${dagBytes} bytes`));
|
|
3441
|
+
console.log(source_default.green(` ${savings}% smaller (${sourceBytes - dagBytes} bytes saved)`));
|
|
3442
|
+
if (contentBytes && contentBytes !== sourceBytes) {
|
|
3443
|
+
const cs = Math.round((1 - dagBytes / contentBytes) * 1000) / 10;
|
|
3444
|
+
console.log(source_default.gray(` content-only: ${contentBytes} bytes → ${cs}% savings`));
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
if (!found)
|
|
3449
|
+
console.log(source_default.yellow("No .dag files found. Run compile first."));
|
|
3450
|
+
});
|
|
3284
3451
|
program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts) => {
|
|
3285
3452
|
const dir = resolvePath2(d);
|
|
3286
3453
|
const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
|
|
@@ -3327,7 +3494,9 @@ Spec Analysis
|
|
|
3327
3494
|
for (const p of projects) {
|
|
3328
3495
|
if (opts.project && p !== opts.project)
|
|
3329
3496
|
continue;
|
|
3330
|
-
const pd = join2(dd, p
|
|
3497
|
+
const pd = join2(dd, p);
|
|
3498
|
+
if (!existsSync2(join2(pd, "SPEC.dog")))
|
|
3499
|
+
continue;
|
|
3331
3500
|
if (!existsSync2(pd))
|
|
3332
3501
|
continue;
|
|
3333
3502
|
const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
|
|
@@ -3407,7 +3576,7 @@ program2.command("generate [dir]").description("Generate missing spec files from
|
|
|
3407
3576
|
for (const p of projects) {
|
|
3408
3577
|
if (opts.project && p !== opts.project)
|
|
3409
3578
|
continue;
|
|
3410
|
-
const pd = join2(dd, p
|
|
3579
|
+
const pd = join2(dd, p);
|
|
3411
3580
|
const sp = join2(pd, "SPEC.dog");
|
|
3412
3581
|
if (existsSync2(sp)) {
|
|
3413
3582
|
specContent = readFileSync2(sp, "utf-8");
|
|
@@ -3525,7 +3694,9 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
3525
3694
|
continue;
|
|
3526
3695
|
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3527
3696
|
for (const p of projects) {
|
|
3528
|
-
const pd = join2(dd, p
|
|
3697
|
+
const pd = join2(dd, p);
|
|
3698
|
+
if (!existsSync2(join2(pd, "SPEC.dog")))
|
|
3699
|
+
continue;
|
|
3529
3700
|
if (!existsSync2(pd))
|
|
3530
3701
|
continue;
|
|
3531
3702
|
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.3",
|
|
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.
|