dotdog 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +172 -78
- package/package.json +4 -6
package/dist/cli.js
CHANGED
|
@@ -2535,9 +2535,8 @@ 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";
|
|
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;
|
|
@@ -2897,14 +2901,36 @@ function parseInlineObject(value) {
|
|
|
2897
2901
|
}
|
|
2898
2902
|
// src/serve.ts
|
|
2899
2903
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
2900
|
-
import { join } from "path";
|
|
2904
|
+
import { join, resolve } from "path";
|
|
2901
2905
|
import { homedir } from "os";
|
|
2902
2906
|
import * as readline from "readline";
|
|
2903
2907
|
function resolvePath(p) {
|
|
2904
2908
|
if (p.startsWith("~"))
|
|
2905
2909
|
p = join(homedir(), p.slice(1));
|
|
2906
|
-
|
|
2910
|
+
const resolved = p.startsWith("/") ? p : join(process.cwd(), p);
|
|
2911
|
+
if (!p.startsWith("/") && !p.startsWith("~")) {
|
|
2912
|
+
const rel = resolve(process.cwd(), p);
|
|
2913
|
+
const cwd = process.cwd();
|
|
2914
|
+
const isDescendant = rel.startsWith(cwd + "/");
|
|
2915
|
+
const isSelf = rel === cwd;
|
|
2916
|
+
const isAncestor = cwd.startsWith(rel + "/");
|
|
2917
|
+
if (!isDescendant && !isSelf && !isAncestor) {
|
|
2918
|
+
throw new Error(`Path traversal blocked: ${p}`);
|
|
2919
|
+
}
|
|
2920
|
+
return rel;
|
|
2921
|
+
}
|
|
2922
|
+
return resolved;
|
|
2907
2923
|
}
|
|
2924
|
+
var N = (dag) => dag.n || dag.nodes || [];
|
|
2925
|
+
var E = (dag) => dag.e || dag.edges || [];
|
|
2926
|
+
var P = (dag) => dag.p || dag.project || "";
|
|
2927
|
+
var ni = (n) => n.i || n.id || "";
|
|
2928
|
+
var nt = (n) => n.t || n.type || "";
|
|
2929
|
+
var np = (n) => n.p || n.properties || {};
|
|
2930
|
+
var ns = (n) => n.s || n.states || [];
|
|
2931
|
+
var nl = (n) => n.l || n.lifecycle || [];
|
|
2932
|
+
var es = (e) => e.s || e.source || "";
|
|
2933
|
+
var et = (e) => e.t || e.target || "";
|
|
2908
2934
|
function serve(dir = ".") {
|
|
2909
2935
|
const root = resolvePath(dir);
|
|
2910
2936
|
const dagCache = new Map;
|
|
@@ -2946,7 +2972,7 @@ function serve(dir = ".") {
|
|
|
2946
2972
|
{ name: "traverse", description: "Traverse graph: from node, follow edges, return subgraph", inputSchema: { type: "object", properties: { project: { type: "string" }, from: { type: "string" }, depth: { type: "number", default: 1 } }, required: ["from"] } },
|
|
2947
2973
|
{ name: "search", description: "Find entities by name or type", inputSchema: { type: "object", properties: { project: { type: "string" }, q: { type: "string" }, type: { type: "string" } }, required: ["q"] } },
|
|
2948
2974
|
{ name: "listProjects", description: "List all projects", inputSchema: { type: "object", properties: {} } },
|
|
2949
|
-
{ name: "summary", description: "Get project summary: node count, edge count,
|
|
2975
|
+
{ name: "summary", description: "Get project summary: node count, edge count, token savings", inputSchema: { type: "object", properties: { project: { type: "string" } } } },
|
|
2950
2976
|
{ name: "schema", description: "Get full property schema for an entity", inputSchema: { type: "object", properties: { project: { type: "string" }, entity: { type: "string" } }, required: ["entity"] } }
|
|
2951
2977
|
]
|
|
2952
2978
|
}
|
|
@@ -2962,39 +2988,44 @@ function serve(dir = ".") {
|
|
|
2962
2988
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
2963
2989
|
if (!dag)
|
|
2964
2990
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
2965
|
-
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(dag
|
|
2991
|
+
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(N(dag)) }] } };
|
|
2966
2992
|
}
|
|
2967
2993
|
if (name === "getEntity") {
|
|
2968
2994
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
2969
2995
|
if (!dag)
|
|
2970
2996
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
2971
|
-
const node = (dag
|
|
2997
|
+
const node = N(dag).find((n) => ni(n).toLowerCase() === (args.name || "").toLowerCase());
|
|
2972
2998
|
if (!node)
|
|
2973
2999
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
|
|
2974
|
-
const edges = (dag
|
|
3000
|
+
const edges = E(dag).filter((e) => es(e).toLowerCase() === ni(node).toLowerCase() || et(e).toLowerCase() === ni(node).toLowerCase());
|
|
2975
3001
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({ ...node, edges }) }] } };
|
|
2976
3002
|
}
|
|
2977
3003
|
if (name === "traverse") {
|
|
2978
3004
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
2979
3005
|
if (!dag)
|
|
2980
3006
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
2981
|
-
const depth = args.depth || 1;
|
|
2982
|
-
const
|
|
3007
|
+
const depth = Math.min(Math.max(1, args.depth || 1), 20);
|
|
3008
|
+
const visitedNodes = new Set;
|
|
3009
|
+
const visitedEdges = new Set;
|
|
2983
3010
|
const subgraph = { nodes: [], edges: [] };
|
|
2984
3011
|
const queue = [{ id: args.from, depth: 0 }];
|
|
2985
3012
|
while (queue.length > 0) {
|
|
2986
3013
|
const curr = queue.shift();
|
|
2987
|
-
if (
|
|
3014
|
+
if (visitedNodes.has(curr.id) || curr.depth > depth)
|
|
2988
3015
|
continue;
|
|
2989
|
-
|
|
2990
|
-
const node = (dag
|
|
3016
|
+
visitedNodes.add(curr.id);
|
|
3017
|
+
const node = N(dag).find((n) => ni(n).toLowerCase() === curr.id.toLowerCase());
|
|
2991
3018
|
if (node)
|
|
2992
3019
|
subgraph.nodes.push(node);
|
|
2993
|
-
const edges = (dag
|
|
3020
|
+
const edges = E(dag).filter((e) => es(e).toLowerCase() === curr.id.toLowerCase() || et(e).toLowerCase() === curr.id.toLowerCase());
|
|
2994
3021
|
for (const e of edges) {
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3022
|
+
const edgeKey = `${es(e)}→${et(e)}`;
|
|
3023
|
+
if (!visitedEdges.has(edgeKey)) {
|
|
3024
|
+
visitedEdges.add(edgeKey);
|
|
3025
|
+
subgraph.edges.push(e);
|
|
3026
|
+
}
|
|
3027
|
+
const next = es(e).toLowerCase() === curr.id.toLowerCase() ? et(e) : es(e);
|
|
3028
|
+
if (!visitedNodes.has(next))
|
|
2998
3029
|
queue.push({ id: next, depth: curr.depth + 1 });
|
|
2999
3030
|
}
|
|
3000
3031
|
}
|
|
@@ -3006,19 +3037,21 @@ function serve(dir = ".") {
|
|
|
3006
3037
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
3007
3038
|
const q = (args.q || "").toLowerCase();
|
|
3008
3039
|
const type = (args.type || "").toLowerCase();
|
|
3009
|
-
const results = (dag
|
|
3040
|
+
const results = N(dag).filter((n) => ni(n).toLowerCase().includes(q) && (!type || nt(n).toLowerCase().includes(type)));
|
|
3010
3041
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(results) }] } };
|
|
3011
3042
|
}
|
|
3012
3043
|
if (name === "summary") {
|
|
3013
3044
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
3014
3045
|
if (!dag)
|
|
3015
3046
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
3047
|
+
const tk = dag.tk || dag.tokens || {};
|
|
3016
3048
|
const s = {
|
|
3017
|
-
project: dag
|
|
3018
|
-
nodes: (dag
|
|
3019
|
-
edges: (dag
|
|
3020
|
-
|
|
3021
|
-
|
|
3049
|
+
project: P(dag),
|
|
3050
|
+
nodes: N(dag).length,
|
|
3051
|
+
edges: E(dag).length,
|
|
3052
|
+
version: dag.v || dag.version || "",
|
|
3053
|
+
savings: tk.sv || tk.savings_pct || 0,
|
|
3054
|
+
method: tk.m || tk.method || ""
|
|
3022
3055
|
};
|
|
3023
3056
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(s) }] } };
|
|
3024
3057
|
}
|
|
@@ -3026,14 +3059,14 @@ function serve(dir = ".") {
|
|
|
3026
3059
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
3027
3060
|
if (!dag)
|
|
3028
3061
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
3029
|
-
const node = (dag
|
|
3062
|
+
const node = N(dag).find((n) => ni(n).toLowerCase() === (args.entity || "").toLowerCase());
|
|
3030
3063
|
if (!node)
|
|
3031
3064
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Entity not found" } };
|
|
3032
3065
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({
|
|
3033
|
-
entity: node
|
|
3034
|
-
properties: node
|
|
3035
|
-
states: node
|
|
3036
|
-
lifecycle: node
|
|
3066
|
+
entity: ni(node),
|
|
3067
|
+
properties: np(node),
|
|
3068
|
+
states: ns(node),
|
|
3069
|
+
lifecycle: nl(node)
|
|
3037
3070
|
}) }] } };
|
|
3038
3071
|
}
|
|
3039
3072
|
return { jsonrpc: "2.0", id, error: { code: 404, message: `Unknown tool: ${name}` } };
|
|
@@ -3060,7 +3093,19 @@ function serve(dir = ".") {
|
|
|
3060
3093
|
function resolvePath2(p) {
|
|
3061
3094
|
if (p.startsWith("~"))
|
|
3062
3095
|
p = join2(homedir2(), p.slice(1));
|
|
3063
|
-
|
|
3096
|
+
const resolved = p.startsWith("/") ? p : join2(process.cwd(), p);
|
|
3097
|
+
if (!p.startsWith("/") && !p.startsWith("~")) {
|
|
3098
|
+
const rel = resolve2(process.cwd(), p);
|
|
3099
|
+
const cwd = process.cwd();
|
|
3100
|
+
const isDescendant = rel.startsWith(cwd + "/");
|
|
3101
|
+
const isSelf = rel === cwd;
|
|
3102
|
+
const isAncestor = cwd.startsWith(rel + "/");
|
|
3103
|
+
if (!isDescendant && !isSelf && !isAncestor) {
|
|
3104
|
+
throw new Error(`Path traversal blocked: ${p}`);
|
|
3105
|
+
}
|
|
3106
|
+
return rel;
|
|
3107
|
+
}
|
|
3108
|
+
return resolved;
|
|
3064
3109
|
}
|
|
3065
3110
|
function parseSections2(markdown) {
|
|
3066
3111
|
const lines = markdown.split(`
|
|
@@ -3095,9 +3140,10 @@ function parseSections2(markdown) {
|
|
|
3095
3140
|
}
|
|
3096
3141
|
var program2 = new Command;
|
|
3097
3142
|
var pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
|
|
3098
|
-
program2.name("spec").alias("dotdog").description("
|
|
3143
|
+
program2.name("spec").alias("dotdog").description("CLI for structured software specs : validate .dog, compile .dag, query via MCP").version(pkg.version);
|
|
3099
3144
|
program2.command("validate [dir]").action((d = ".") => {
|
|
3100
|
-
const
|
|
3145
|
+
const dir = resolvePath2(d);
|
|
3146
|
+
const dirs = [join2(dir, "projects"), join2(dir, "specs")];
|
|
3101
3147
|
let found = false;
|
|
3102
3148
|
for (const dd of dirs) {
|
|
3103
3149
|
if (!existsSync2(dd))
|
|
@@ -3105,12 +3151,14 @@ program2.command("validate [dir]").action((d = ".") => {
|
|
|
3105
3151
|
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3106
3152
|
for (const p of projects) {
|
|
3107
3153
|
found = true;
|
|
3108
|
-
const pd = join2(dd, p
|
|
3154
|
+
const pd = join2(dd, p);
|
|
3155
|
+
if (!existsSync2(join2(pd, "SPEC.dog")))
|
|
3156
|
+
continue;
|
|
3109
3157
|
const files = existsSync2(pd) ? readdirSync2(pd).filter((f) => f.endsWith(".dog")) : [];
|
|
3110
3158
|
const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
|
|
3111
3159
|
const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
|
|
3112
3160
|
console.log(source_default.bold(`
|
|
3113
|
-
${p}
|
|
3161
|
+
${p} : ${files.length} .dog files, ${100 - Math.round((missing.length * 3 + optional.length) / 20 * 100)}% complete`));
|
|
3114
3162
|
for (const f of files)
|
|
3115
3163
|
console.log(source_default.gray(` ${f}`));
|
|
3116
3164
|
if (missing.length)
|
|
@@ -3122,10 +3170,10 @@ program2.command("validate [dir]").action((d = ".") => {
|
|
|
3122
3170
|
if (!found)
|
|
3123
3171
|
console.log(source_default.yellow("No projects found. Run: spec init <project>"));
|
|
3124
3172
|
});
|
|
3125
|
-
program2.command("init <project>").action((p) => {
|
|
3173
|
+
program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data-model.dog").action((p, opts) => {
|
|
3126
3174
|
const d = join2(process.cwd(), "specs", p);
|
|
3127
3175
|
mkdirSync(d, { recursive: true });
|
|
3128
|
-
const
|
|
3176
|
+
const full = {
|
|
3129
3177
|
"SPEC.dog": `# Project
|
|
3130
3178
|
|
|
3131
3179
|
## Product
|
|
@@ -3157,6 +3205,19 @@ program2.command("init <project>").action((p) => {
|
|
|
3157
3205
|
|---|---|---|
|
|
3158
3206
|
`
|
|
3159
3207
|
};
|
|
3208
|
+
const minimal = {
|
|
3209
|
+
"SPEC.dog": `# Project
|
|
3210
|
+
|
|
3211
|
+
## Product
|
|
3212
|
+
|
|
3213
|
+
`,
|
|
3214
|
+
"data-model.dog": `# Data Model
|
|
3215
|
+
|
|
3216
|
+
## Entities
|
|
3217
|
+
|
|
3218
|
+
`
|
|
3219
|
+
};
|
|
3220
|
+
const tmpl = opts.minimal ? minimal : full;
|
|
3160
3221
|
for (const [f, c] of Object.entries(tmpl)) {
|
|
3161
3222
|
writeFileSync(join2(d, f), c);
|
|
3162
3223
|
console.log(source_default.green(` ✓ ${f}`));
|
|
@@ -3175,9 +3236,9 @@ program2.command("list").action(() => {
|
|
|
3175
3236
|
console.log(source_default.bold(`
|
|
3176
3237
|
${d}/`));
|
|
3177
3238
|
for (const p of projects) {
|
|
3178
|
-
const sp = join2(dd, p
|
|
3239
|
+
const sp = join2(dd, p);
|
|
3179
3240
|
const n = existsSync2(sp) ? readdirSync2(sp).filter((f) => f.endsWith(".dog")).length : 0;
|
|
3180
|
-
console.log(` ${source_default.cyan(p)}
|
|
3241
|
+
console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
|
|
3181
3242
|
}
|
|
3182
3243
|
}
|
|
3183
3244
|
});
|
|
@@ -3198,7 +3259,9 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3198
3259
|
continue;
|
|
3199
3260
|
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3200
3261
|
for (const p of projects) {
|
|
3201
|
-
const pd = join2(dd, p
|
|
3262
|
+
const pd = join2(dd, p);
|
|
3263
|
+
if (!existsSync2(join2(pd, "SPEC.dog")))
|
|
3264
|
+
continue;
|
|
3202
3265
|
if (!existsSync2(pd))
|
|
3203
3266
|
continue;
|
|
3204
3267
|
const files = readdirSync2(pd).filter((f) => f.endsWith(".dog")).sort();
|
|
@@ -3206,59 +3269,79 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3206
3269
|
continue;
|
|
3207
3270
|
found = true;
|
|
3208
3271
|
const sources = {};
|
|
3209
|
-
let sourceBytes = 0;
|
|
3210
|
-
const hash = createHash("sha256");
|
|
3272
|
+
let sourceBytes = 0, contentBytes = 0;
|
|
3211
3273
|
for (const f of files) {
|
|
3212
3274
|
const content = readFileSync2(join2(pd, f), "utf-8");
|
|
3213
3275
|
sources[f] = content;
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3276
|
+
const bytes = Buffer.byteLength(content, "utf-8");
|
|
3277
|
+
sourceBytes += bytes;
|
|
3278
|
+
if (bytes >= 100)
|
|
3279
|
+
contentBytes += bytes;
|
|
3218
3280
|
}
|
|
3219
|
-
const integrity = { sha256: hash.digest("hex"), source_files: files.length, source_bytes: sourceBytes };
|
|
3220
3281
|
const sourceTokens = Math.round(sourceBytes / 4);
|
|
3282
|
+
const contentTokens = Math.round(contentBytes / 4);
|
|
3221
3283
|
const nodes = [], edges = [];
|
|
3222
3284
|
for (const f of files) {
|
|
3223
3285
|
const ast = parse(sources[f]);
|
|
3224
3286
|
for (const section of ast.sections) {
|
|
3225
3287
|
for (const block of section.blocks) {
|
|
3226
|
-
if (block.kind === "entity") {
|
|
3288
|
+
if (block.kind === "entity" || block.kind === "event" || block.kind === "prediction") {
|
|
3289
|
+
const compactProps = {};
|
|
3290
|
+
for (const [key, val] of Object.entries(block.properties)) {
|
|
3291
|
+
let enc = "";
|
|
3292
|
+
const t = val.type || "string";
|
|
3293
|
+
if (t === "string")
|
|
3294
|
+
enc = "s";
|
|
3295
|
+
else if (t === "number")
|
|
3296
|
+
enc = "n";
|
|
3297
|
+
else if (t === "boolean")
|
|
3298
|
+
enc = "b";
|
|
3299
|
+
else if (t === "enum")
|
|
3300
|
+
enc = "e";
|
|
3301
|
+
else if (t === "json")
|
|
3302
|
+
enc = "j";
|
|
3303
|
+
else
|
|
3304
|
+
enc = t[0];
|
|
3305
|
+
if (val.required)
|
|
3306
|
+
enc += "!";
|
|
3307
|
+
compactProps[key] = enc;
|
|
3308
|
+
}
|
|
3227
3309
|
nodes.push({
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3310
|
+
i: block.name,
|
|
3311
|
+
t: block.type,
|
|
3312
|
+
g: block.kind,
|
|
3313
|
+
d: block.description || "",
|
|
3314
|
+
p: compactProps,
|
|
3315
|
+
s: block.states || [],
|
|
3316
|
+
l: block.lifecycle || []
|
|
3235
3317
|
});
|
|
3236
3318
|
}
|
|
3237
3319
|
if (block.kind === "relationship") {
|
|
3238
3320
|
edges.push({
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3321
|
+
s: block.source,
|
|
3322
|
+
t: block.target,
|
|
3323
|
+
v: block.verb,
|
|
3324
|
+
d: block.description || "",
|
|
3325
|
+
c: block.cardinality,
|
|
3326
|
+
r: block.required
|
|
3245
3327
|
});
|
|
3246
3328
|
}
|
|
3247
3329
|
}
|
|
3248
3330
|
}
|
|
3249
3331
|
}
|
|
3250
|
-
const dag = {
|
|
3332
|
+
const dag = { v: "1.4", p, c: `dotdog@${pkg.version}`, n: nodes, e: edges };
|
|
3251
3333
|
const dagJson = JSON.stringify(dag);
|
|
3252
3334
|
const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
|
|
3253
|
-
const
|
|
3335
|
+
const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
|
|
3336
|
+
const contentSavingsPct = contentTokens > 0 ? Math.round((1 - dagTokens / contentTokens) * 1000) / 10 : 0;
|
|
3254
3337
|
const savingsTokens = sourceTokens - dagTokens;
|
|
3255
|
-
const outPath = opts.output || join2(pd,
|
|
3256
|
-
const
|
|
3338
|
+
const outPath = opts.output || join2(pd, `${p}.dag`);
|
|
3339
|
+
const tokens = { m: "chars/4", st: sourceTokens, ct: contentTokens, dt: dagTokens, sv: allSavingsPct, cs: contentSavingsPct, saved: savingsTokens };
|
|
3340
|
+
const report = { ...dag, tk: tokens };
|
|
3257
3341
|
writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
3258
3342
|
console.log(source_default.green(` ✓ ${outPath}`));
|
|
3259
3343
|
console.log(source_default.gray(` ${nodes.length} nodes, ${edges.length} edges, ${files.length} files`));
|
|
3260
|
-
console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${
|
|
3261
|
-
console.log(source_default.gray(` sha256: ${integrity.sha256.substring(0, 12)}...`));
|
|
3344
|
+
console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${allSavingsPct}% savings, ${contentSavingsPct}% content-only, ${savingsTokens} saved)`));
|
|
3262
3345
|
}
|
|
3263
3346
|
}
|
|
3264
3347
|
if (!found)
|
|
@@ -3286,7 +3369,7 @@ program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts)
|
|
|
3286
3369
|
out += "```\n";
|
|
3287
3370
|
if (opts.save) {
|
|
3288
3371
|
const outFile = join2(dd, p, "..", `${p}.md`);
|
|
3289
|
-
writeFileSync(outFile, `# ${p}
|
|
3372
|
+
writeFileSync(outFile, `# ${p} : Spec Graph
|
|
3290
3373
|
|
|
3291
3374
|
${out}`);
|
|
3292
3375
|
console.log(source_default.green(` ✓ ${outFile}`));
|
|
@@ -3295,8 +3378,8 @@ ${out}`);
|
|
|
3295
3378
|
}
|
|
3296
3379
|
}
|
|
3297
3380
|
});
|
|
3298
|
-
program2.command("serve [dir]").description("MCP server
|
|
3299
|
-
program2.command("analyze [dir]").description("Analyze a spec project
|
|
3381
|
+
program2.command("serve [dir]").description("MCP server : expose .dag graph to AI agents over stdio").action((d = ".") => serve(resolvePath2(d)));
|
|
3382
|
+
program2.command("analyze [dir]").description("Analyze a spec project : score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
|
|
3300
3383
|
const dir = resolvePath2(d);
|
|
3301
3384
|
const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
|
|
3302
3385
|
console.log(source_default.bold(`
|
|
@@ -3310,7 +3393,9 @@ Spec Analysis
|
|
|
3310
3393
|
for (const p of projects) {
|
|
3311
3394
|
if (opts.project && p !== opts.project)
|
|
3312
3395
|
continue;
|
|
3313
|
-
const pd = join2(dd, p
|
|
3396
|
+
const pd = join2(dd, p);
|
|
3397
|
+
if (!existsSync2(join2(pd, "SPEC.dog")))
|
|
3398
|
+
continue;
|
|
3314
3399
|
if (!existsSync2(pd))
|
|
3315
3400
|
continue;
|
|
3316
3401
|
const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
|
|
@@ -3344,7 +3429,7 @@ Spec Analysis
|
|
|
3344
3429
|
console.log(` ${files.length} files | ${score}% complete`);
|
|
3345
3430
|
for (const a of analyses) {
|
|
3346
3431
|
const detail = a.entities > 0 ? ` (${a.entities} entities, ${a.rels} rels)` : "";
|
|
3347
|
-
console.log(source_default.gray(` ${a.file}
|
|
3432
|
+
console.log(source_default.gray(` ${a.file} : ${a.sections} sections, ${(a.size / 1024).toFixed(1)}KB${detail}`));
|
|
3348
3433
|
}
|
|
3349
3434
|
const gaps = [];
|
|
3350
3435
|
for (const f of missingReq)
|
|
@@ -3390,7 +3475,7 @@ program2.command("generate [dir]").description("Generate missing spec files from
|
|
|
3390
3475
|
for (const p of projects) {
|
|
3391
3476
|
if (opts.project && p !== opts.project)
|
|
3392
3477
|
continue;
|
|
3393
|
-
const pd = join2(dd, p
|
|
3478
|
+
const pd = join2(dd, p);
|
|
3394
3479
|
const sp = join2(pd, "SPEC.dog");
|
|
3395
3480
|
if (existsSync2(sp)) {
|
|
3396
3481
|
specContent = readFileSync2(sp, "utf-8");
|
|
@@ -3495,7 +3580,7 @@ program2.command("simulate <scenario>").description("Run a simulation scenario (
|
|
|
3495
3580
|
console.log(source_default.bold(`
|
|
3496
3581
|
Simulation: ${scenario} (project: ${opts.project})
|
|
3497
3582
|
`));
|
|
3498
|
-
console.log(source_default.gray("Simulation engine
|
|
3583
|
+
console.log(source_default.gray("Simulation engine : reads SPEC.dog scenarios, walks through steps, checks pre/postconditions."));
|
|
3499
3584
|
console.log(source_default.gray("Full engine coming in a future release."));
|
|
3500
3585
|
});
|
|
3501
3586
|
program2.command("staleness [dir]").action((d = ".") => {
|
|
@@ -3508,7 +3593,9 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
3508
3593
|
continue;
|
|
3509
3594
|
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3510
3595
|
for (const p of projects) {
|
|
3511
|
-
const pd = join2(dd, p
|
|
3596
|
+
const pd = join2(dd, p);
|
|
3597
|
+
if (!existsSync2(join2(pd, "SPEC.dog")))
|
|
3598
|
+
continue;
|
|
3512
3599
|
if (!existsSync2(pd))
|
|
3513
3600
|
continue;
|
|
3514
3601
|
const planFile = join2(pd, "plan.dog");
|
|
@@ -3556,6 +3643,13 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
3556
3643
|
}
|
|
3557
3644
|
}
|
|
3558
3645
|
});
|
|
3646
|
+
program2.command("woof").action(() => {
|
|
3647
|
+
console.log(" / \\__");
|
|
3648
|
+
console.log(" ( @\\___");
|
|
3649
|
+
console.log(" / O");
|
|
3650
|
+
console.log(" / (_____/");
|
|
3651
|
+
console.log("/_____/ U");
|
|
3652
|
+
});
|
|
3559
3653
|
program2.parse();
|
|
3560
3654
|
export {
|
|
3561
3655
|
parseToJSON,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotdog",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.2",
|
|
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": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"mcp",
|
|
23
23
|
"documentation",
|
|
24
24
|
"cli",
|
|
25
|
+
"dogfood",
|
|
25
26
|
"spec-driven-development",
|
|
26
27
|
"graph",
|
|
27
28
|
"markdown",
|
|
@@ -32,10 +33,7 @@
|
|
|
32
33
|
],
|
|
33
34
|
"license": "MIT",
|
|
34
35
|
"author": "specdog",
|
|
35
|
-
"repository":
|
|
36
|
-
"type": "git",
|
|
37
|
-
"url": "git+https://github.com/specdog/dotdog.git"
|
|
38
|
-
},
|
|
36
|
+
"repository": "github:specdog/dotdog",
|
|
39
37
|
"dependencies": {
|
|
40
38
|
"commander": "^15.0.0",
|
|
41
39
|
"chalk": "^5.6.0"
|