dotdog 0.3.6 → 0.4.0
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 +106 -52
- package/dist/cli.js +150 -65
- package/package.json +1 -1
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
|
@@ -3108,8 +3108,14 @@ function resolvePath(p) {
|
|
|
3108
3108
|
}
|
|
3109
3109
|
return resolved;
|
|
3110
3110
|
}
|
|
3111
|
+
var isV2 = (n) => Array.isArray(n) && typeof n[0] === "number";
|
|
3111
3112
|
var N = (dag) => dag.n || dag.nodes || [];
|
|
3113
|
+
function edgeToObj(n, tgtIdx, v2e) {
|
|
3114
|
+
return { t: String(tgtIdx), v: v2e[1] || "", c: v2e[2] || "", r: v2e[3] || 0 };
|
|
3115
|
+
}
|
|
3112
3116
|
function nodeEdges(n) {
|
|
3117
|
+
if (isV2(n))
|
|
3118
|
+
return (n[6] || []).map((e) => edgeToObj(n, e[0], e));
|
|
3113
3119
|
return n.es || [];
|
|
3114
3120
|
}
|
|
3115
3121
|
function E(dag) {
|
|
@@ -3119,21 +3125,31 @@ function E(dag) {
|
|
|
3119
3125
|
const seen = new Set;
|
|
3120
3126
|
for (const node of N(dag)) {
|
|
3121
3127
|
for (const e of nodeEdges(node)) {
|
|
3122
|
-
const
|
|
3128
|
+
const src = isV2(node) ? String(node[0]) : node.i || node.id || "";
|
|
3129
|
+
const key = `${src}→${e.t}:${e.v}`;
|
|
3123
3130
|
if (!seen.has(key)) {
|
|
3124
3131
|
seen.add(key);
|
|
3125
|
-
edges.push({ s:
|
|
3132
|
+
edges.push({ s: src, t: e.t, v: e.v, d: e.d, c: e.c, r: e.r });
|
|
3126
3133
|
}
|
|
3127
3134
|
}
|
|
3128
3135
|
}
|
|
3129
3136
|
return edges;
|
|
3130
3137
|
}
|
|
3131
3138
|
var P = (dag) => dag.p || dag.project || "";
|
|
3132
|
-
var ni = (n) => n.i || n.id || "";
|
|
3133
|
-
var nt = (n) => n.t || n.type || "";
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3139
|
+
var ni = (n) => isV2(n) ? String(n[0]) : n.i || n.id || "";
|
|
3140
|
+
var nt = (n) => isV2(n) ? n[2] || "" : n.t || n.type || "";
|
|
3141
|
+
function np(n) {
|
|
3142
|
+
if (isV2(n)) {
|
|
3143
|
+
const flat = n[4] || [];
|
|
3144
|
+
const obj = {};
|
|
3145
|
+
for (let i = 0;i < flat.length; i += 2)
|
|
3146
|
+
obj[flat[i]] = flat[i + 1] || "";
|
|
3147
|
+
return obj;
|
|
3148
|
+
}
|
|
3149
|
+
return n.p || n.properties || {};
|
|
3150
|
+
}
|
|
3151
|
+
var ns = (n) => isV2(n) ? n[5] || [] : n.s || n.states || [];
|
|
3152
|
+
var nl = (n) => isV2(n) ? [] : n.l || n.lifecycle || [];
|
|
3137
3153
|
var es = (e) => e.s || e.source || "";
|
|
3138
3154
|
var et = (e) => e.t || e.target || "";
|
|
3139
3155
|
function serve(dir = ".") {
|
|
@@ -3203,7 +3219,7 @@ function serve(dir = ".") {
|
|
|
3203
3219
|
if (!node)
|
|
3204
3220
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
|
|
3205
3221
|
const edges = E(dag).filter((e) => es(e).toLowerCase() === ni(node).toLowerCase() || et(e).toLowerCase() === ni(node).toLowerCase());
|
|
3206
|
-
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({ ...node, edges }) }] } };
|
|
3222
|
+
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(isV2(node) ? { id: String(node[0]), name: node[1] || "", type: node[2] || "", description: node[3] || "", properties: np(node), states: ns(node), edges } : { ...node, edges }) }] } };
|
|
3207
3223
|
}
|
|
3208
3224
|
if (name === "traverse") {
|
|
3209
3225
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
@@ -3221,7 +3237,7 @@ function serve(dir = ".") {
|
|
|
3221
3237
|
visitedNodes.add(curr.id);
|
|
3222
3238
|
const node = N(dag).find((n) => ni(n).toLowerCase() === curr.id.toLowerCase());
|
|
3223
3239
|
if (node)
|
|
3224
|
-
subgraph.nodes.push(node);
|
|
3240
|
+
subgraph.nodes.push(isV2(node) ? { id: String(node[0]), name: node[1] || "", type: node[2] || "", description: node[3] || "", properties: np(node), states: ns(node), edges: nodeEdges(node) } : node);
|
|
3225
3241
|
const edges = E(dag).filter((e) => es(e).toLowerCase() === curr.id.toLowerCase() || et(e).toLowerCase() === curr.id.toLowerCase());
|
|
3226
3242
|
for (const e of edges) {
|
|
3227
3243
|
const edgeKey = `${es(e)}→${et(e)}`;
|
|
@@ -3431,6 +3447,12 @@ program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data
|
|
|
3431
3447
|
const tmpl = opts.minimal ? minimal : full;
|
|
3432
3448
|
for (const [f, c] of Object.entries(tmpl)) {
|
|
3433
3449
|
writeFileSync2(join3(d, f), c);
|
|
3450
|
+
try {
|
|
3451
|
+
parse(c);
|
|
3452
|
+
} catch (_) {
|
|
3453
|
+
console.log(source_default.red(` ✗ Template ${f} is invalid`));
|
|
3454
|
+
process.exit(1);
|
|
3455
|
+
}
|
|
3434
3456
|
console.log(source_default.green(` ✓ ${f}`));
|
|
3435
3457
|
}
|
|
3436
3458
|
console.log(source_default.bold(`
|
|
@@ -3555,44 +3577,63 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3555
3577
|
}
|
|
3556
3578
|
}
|
|
3557
3579
|
}
|
|
3558
|
-
const
|
|
3559
|
-
nodes.forEach((n, i) => nodeIds.set(n.i, i));
|
|
3560
|
-
const inDegree = new Array(nodes.length).fill(0);
|
|
3561
|
-
const adj = new Array(nodes.length).fill(0).map(() => []);
|
|
3580
|
+
const entityNames = new Set(nodes.map((n) => n.i));
|
|
3562
3581
|
for (const e of edges) {
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
inDegree[ti]++;
|
|
3582
|
+
if (e.s && !entityNames.has(e.s)) {
|
|
3583
|
+
console.log(source_default.red(` ✗ Unknown relationship source "${e.s}" (target: "${e.t}")`));
|
|
3584
|
+
process.exit(1);
|
|
3567
3585
|
}
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
if (inDegree[j] === 0)
|
|
3572
|
-
queue.push(j);
|
|
3573
|
-
const order = [];
|
|
3574
|
-
while (queue.length > 0) {
|
|
3575
|
-
const u = queue.shift();
|
|
3576
|
-
order.push(nodes[u].i);
|
|
3577
|
-
for (const v of adj[u]) {
|
|
3578
|
-
inDegree[v]--;
|
|
3579
|
-
if (inDegree[v] === 0)
|
|
3580
|
-
queue.push(v);
|
|
3586
|
+
if (e.t && !entityNames.has(e.t)) {
|
|
3587
|
+
console.log(source_default.red(` ✗ Unknown relationship target "${e.t}" (source: "${e.s}")`));
|
|
3588
|
+
process.exit(1);
|
|
3581
3589
|
}
|
|
3582
3590
|
}
|
|
3583
|
-
const
|
|
3591
|
+
const nodeIds = new Map;
|
|
3592
|
+
nodes.forEach((n, i) => nodeIds.set(n.i, i));
|
|
3593
|
+
const v2nodes = [];
|
|
3584
3594
|
for (let j = 0;j < nodes.length; j++) {
|
|
3585
|
-
const
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3595
|
+
const nd = nodes[j];
|
|
3596
|
+
const props = [];
|
|
3597
|
+
if (nd.p)
|
|
3598
|
+
for (const [k, v] of Object.entries(nd.p))
|
|
3599
|
+
props.push(k, v);
|
|
3600
|
+
const states = nd.s || [];
|
|
3601
|
+
const outEdges = [];
|
|
3602
|
+
const seen = new Set;
|
|
3603
|
+
for (const e of edges) {
|
|
3604
|
+
if (e.s !== nd.i && e.t !== nd.i)
|
|
3605
|
+
continue;
|
|
3606
|
+
const tid = nodeIds.get(e.s === nd.i ? e.t : e.s);
|
|
3607
|
+
if (tid === undefined)
|
|
3608
|
+
continue;
|
|
3609
|
+
const key = `${j}→${tid}:${e.v}`;
|
|
3610
|
+
if (seen.has(key))
|
|
3611
|
+
continue;
|
|
3612
|
+
seen.add(key);
|
|
3613
|
+
const ee = [tid, e.v || ""];
|
|
3614
|
+
if (e.c)
|
|
3615
|
+
ee.push(e.c);
|
|
3616
|
+
if (e.r)
|
|
3617
|
+
ee.push(1);
|
|
3618
|
+
outEdges.push(ee);
|
|
3619
|
+
}
|
|
3620
|
+
const entry = [j, nd.i || "", nd.t || "", nd.d || "", props, states, outEdges];
|
|
3621
|
+
if (nd.g === "prediction") {
|
|
3622
|
+
const f = [];
|
|
3623
|
+
if (nd.cf)
|
|
3624
|
+
f.push(nd.cf);
|
|
3625
|
+
if (nd.tf)
|
|
3626
|
+
f.push(nd.tf);
|
|
3627
|
+
if (nd.tg)
|
|
3628
|
+
f.push(nd.tg);
|
|
3629
|
+
if (nd.ms)
|
|
3630
|
+
f.push(nd.ms);
|
|
3631
|
+
if (f.length)
|
|
3632
|
+
entry.push(f);
|
|
3633
|
+
}
|
|
3634
|
+
v2nodes.push(entry);
|
|
3594
3635
|
}
|
|
3595
|
-
const dag = { v:
|
|
3636
|
+
const dag = { v: 2, p, n: v2nodes };
|
|
3596
3637
|
const dagJson = JSON.stringify(dag);
|
|
3597
3638
|
const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
|
|
3598
3639
|
const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
|
|
@@ -3636,13 +3677,17 @@ program2.command("tokens [dir]").action((d = ".") => {
|
|
|
3636
3677
|
}
|
|
3637
3678
|
const dagBytes = Buffer.byteLength(readFileSync3(dagFile, "utf-8"), "utf-8");
|
|
3638
3679
|
const savings = sourceBytes > 0 ? Math.round((1 - dagBytes / sourceBytes) * 1000) / 10 : 0;
|
|
3680
|
+
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
3681
|
+
const dagOnly = JSON.stringify({ v: dag.v, p: dag.p, n: dag.n });
|
|
3682
|
+
const dagOnlyBytes = Buffer.byteLength(dagOnly, "utf-8");
|
|
3683
|
+
const dagOnlyPct = sourceBytes > 0 ? Math.round((1 - dagOnlyBytes / sourceBytes) * 1000) / 10 : 0;
|
|
3639
3684
|
console.log(source_default.bold(`
|
|
3640
3685
|
${p}`));
|
|
3641
3686
|
console.log(source_default.gray(` ${dogFiles.length} .dog files: ${sourceBytes} bytes`));
|
|
3642
|
-
console.log(source_default.gray(` .dag
|
|
3643
|
-
console.log(source_default.green(` ${
|
|
3687
|
+
console.log(source_default.gray(` .dag on disk: ${dagBytes} bytes (${savings}% savings, includes metadata)`));
|
|
3688
|
+
console.log(source_default.green(` .dag payload: ${dagOnlyBytes} bytes (${dagOnlyPct}% savings, graph only)`));
|
|
3644
3689
|
if (contentBytes && contentBytes !== sourceBytes) {
|
|
3645
|
-
const cs = Math.round((1 -
|
|
3690
|
+
const cs = Math.round((1 - dagOnlyBytes / contentBytes) * 1000) / 10;
|
|
3646
3691
|
console.log(source_default.gray(` content-only: ${contentBytes} bytes → ${cs}% savings`));
|
|
3647
3692
|
}
|
|
3648
3693
|
}
|
|
@@ -3663,22 +3708,32 @@ program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts)
|
|
|
3663
3708
|
continue;
|
|
3664
3709
|
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
3665
3710
|
const nodes = dag.n || dag.nodes || [];
|
|
3711
|
+
const isV22 = (n) => Array.isArray(n) && typeof n[0] === "number";
|
|
3712
|
+
const nodeName = (n) => isV22(n) ? nodes[n[0]] ? nodes[n[0]][1] || String(n[0]) : String(n[0]) : n.i || n.id || "";
|
|
3713
|
+
const slug = (s) => s.replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
|
|
3666
3714
|
let out = "```mermaid\ngraph LR\n";
|
|
3667
3715
|
for (const n of nodes) {
|
|
3668
|
-
const raw = n.i || n.id || "";
|
|
3669
|
-
const id = raw.replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
|
|
3716
|
+
const raw = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
|
|
3670
3717
|
if (raw)
|
|
3671
|
-
out += ` ${
|
|
3718
|
+
out += ` ${slug(raw)}[${raw}]
|
|
3672
3719
|
`;
|
|
3673
3720
|
}
|
|
3674
3721
|
const seen = new Set;
|
|
3675
3722
|
for (const n of nodes) {
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
const src = (
|
|
3680
|
-
|
|
3681
|
-
|
|
3723
|
+
const edges = isV22(n) ? n[6] || [] : n.es || [];
|
|
3724
|
+
for (const e of edges) {
|
|
3725
|
+
const srcName = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
|
|
3726
|
+
const src = slug(srcName);
|
|
3727
|
+
let tgtName, verb;
|
|
3728
|
+
if (isV22(n)) {
|
|
3729
|
+
const tgtNode = nodes[e[0]];
|
|
3730
|
+
tgtName = tgtNode ? tgtNode[1] || String(tgtNode[0]) : String(e[0]);
|
|
3731
|
+
verb = e[1] || "";
|
|
3732
|
+
} else {
|
|
3733
|
+
tgtName = e.t || "";
|
|
3734
|
+
verb = e.v || "";
|
|
3735
|
+
}
|
|
3736
|
+
const tgt = slug(tgtName);
|
|
3682
3737
|
const key = `${src}→${tgt}:${verb}`;
|
|
3683
3738
|
if (!seen.has(key) && src && tgt) {
|
|
3684
3739
|
seen.add(key);
|
|
@@ -3689,8 +3744,8 @@ program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts)
|
|
|
3689
3744
|
}
|
|
3690
3745
|
const legacyEdges = dag.e || dag.edges || [];
|
|
3691
3746
|
for (const e of legacyEdges) {
|
|
3692
|
-
const src = (e.s || e.source || "")
|
|
3693
|
-
const tgt = (e.t || e.target || "")
|
|
3747
|
+
const src = slug(e.s || e.source || "");
|
|
3748
|
+
const tgt = slug(e.t || e.target || "");
|
|
3694
3749
|
const verb = e.v || e.verb || "";
|
|
3695
3750
|
const key = `${src}→${tgt}:${verb}`;
|
|
3696
3751
|
if (!seen.has(key) && src && tgt) {
|
|
@@ -3913,6 +3968,12 @@ ${e.description || "No description."}
|
|
|
3913
3968
|
dm += "```\n\n";
|
|
3914
3969
|
}
|
|
3915
3970
|
writeFileSync2(join3(specDir, "data-model.dog"), dm);
|
|
3971
|
+
try {
|
|
3972
|
+
parse(dm);
|
|
3973
|
+
} catch (_) {
|
|
3974
|
+
console.log(source_default.red(" ✗ Generated data-model.dog is invalid"));
|
|
3975
|
+
process.exit(1);
|
|
3976
|
+
}
|
|
3916
3977
|
console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
|
|
3917
3978
|
}
|
|
3918
3979
|
if (!existsSync3(join3(specDir, "COPY.dog")) && uiStrings.length > 0) {
|
|
@@ -3925,6 +3986,12 @@ ${e.description || "No description."}
|
|
|
3925
3986
|
copy += `| ${s.screen} | ${s.element} | ${s.text} |
|
|
3926
3987
|
`;
|
|
3927
3988
|
writeFileSync2(join3(specDir, "COPY.dog"), copy);
|
|
3989
|
+
try {
|
|
3990
|
+
parse(copy);
|
|
3991
|
+
} catch (_) {
|
|
3992
|
+
console.log(source_default.red(" ✗ Generated COPY.dog is invalid"));
|
|
3993
|
+
process.exit(1);
|
|
3994
|
+
}
|
|
3928
3995
|
console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
|
|
3929
3996
|
}
|
|
3930
3997
|
if (!existsSync3(join3(specDir, "INDEX.dog"))) {
|
|
@@ -3940,6 +4007,12 @@ ${e.description || "No description."}
|
|
|
3940
4007
|
idx += `| Designer | SPEC.dog | COPY.dog |
|
|
3941
4008
|
`;
|
|
3942
4009
|
writeFileSync2(join3(specDir, "INDEX.dog"), idx);
|
|
4010
|
+
try {
|
|
4011
|
+
parse(idx);
|
|
4012
|
+
} catch (_) {
|
|
4013
|
+
console.log(source_default.red(" ✗ Generated INDEX.dog is invalid"));
|
|
4014
|
+
process.exit(1);
|
|
4015
|
+
}
|
|
3943
4016
|
console.log(source_default.green(" ✓ INDEX.dog"));
|
|
3944
4017
|
}
|
|
3945
4018
|
console.log(source_default.bold(`
|
|
@@ -3974,19 +4047,23 @@ program2.command("simulate <scenario>").description("Walk through a scenario, ch
|
|
|
3974
4047
|
let entities = [], relationships = [];
|
|
3975
4048
|
if (existsSync3(dagFile)) {
|
|
3976
4049
|
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
3977
|
-
|
|
4050
|
+
const simNodes = dag.n || dag.nodes || [];
|
|
4051
|
+
const isV22 = (n) => Array.isArray(n) && typeof n[0] === "number";
|
|
4052
|
+
entities = simNodes.map((n) => (isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "").toLowerCase());
|
|
3978
4053
|
if (dag.e || dag.edges) {
|
|
3979
4054
|
relationships = dag.e || dag.edges || [];
|
|
3980
4055
|
} else {
|
|
3981
4056
|
const seen = new Set;
|
|
3982
|
-
for (const n of
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
const
|
|
4057
|
+
for (const n of simNodes) {
|
|
4058
|
+
const edges = isV22(n) ? n[6] || [] : n.es || [];
|
|
4059
|
+
for (const e of edges) {
|
|
4060
|
+
const srcName = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
|
|
4061
|
+
const tgtName = isV22(n) ? (simNodes[e[0]] ? simNodes[e[0]][2] : "") || String(e[0]) : e.t || "";
|
|
4062
|
+
const verb = isV22(n) ? e[1] || "" : e.v || "";
|
|
4063
|
+
const key = `${srcName}→${tgtName}:${verb}`;
|
|
3987
4064
|
if (!seen.has(key)) {
|
|
3988
4065
|
seen.add(key);
|
|
3989
|
-
relationships.push({ s:
|
|
4066
|
+
relationships.push({ s: srcName, t: tgtName, v: verb });
|
|
3990
4067
|
}
|
|
3991
4068
|
}
|
|
3992
4069
|
}
|
|
@@ -4240,6 +4317,7 @@ program2.command("resolve <name>").description("Mark a prediction as correct, wr
|
|
|
4240
4317
|
for (const f of files) {
|
|
4241
4318
|
const fp = join3(pd, f);
|
|
4242
4319
|
let content = readFileSync3(fp, "utf-8");
|
|
4320
|
+
const originalContent = readFileSync3(fp, "utf-8");
|
|
4243
4321
|
const ast = parse(content);
|
|
4244
4322
|
for (const section of ast.sections) {
|
|
4245
4323
|
for (const block of section.blocks) {
|
|
@@ -4259,11 +4337,18 @@ program2.command("resolve <name>").description("Mark a prediction as correct, wr
|
|
|
4259
4337
|
} else {
|
|
4260
4338
|
newYaml = yamlBlock.replace(/\n\s*(trigger|timeframe|confidence|measurement):/, `
|
|
4261
4339
|
status: ${status}
|
|
4262
|
-
|
|
4340
|
+
$&`);
|
|
4263
4341
|
}
|
|
4264
4342
|
content = content.replace(yamlBlock, newYaml);
|
|
4265
|
-
|
|
4266
|
-
|
|
4343
|
+
try {
|
|
4344
|
+
parse(content);
|
|
4345
|
+
writeFileSync2(fp, content, "utf-8");
|
|
4346
|
+
console.log(source_default.green(` ✓ ${b.statement || b.name}: ${status}`));
|
|
4347
|
+
} catch (_) {
|
|
4348
|
+
writeFileSync2(fp, originalContent, "utf-8");
|
|
4349
|
+
console.log(source_default.red(` ✗ resolve produced invalid output for "${name}" — reverted`));
|
|
4350
|
+
process.exit(1);
|
|
4351
|
+
}
|
|
4267
4352
|
return;
|
|
4268
4353
|
}
|
|
4269
4354
|
}
|