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.
Files changed (2) hide show
  1. package/dist/cli.js +172 -78
  2. 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
- const rootBlocks = parseBlocks(lines, 0, lines.length);
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, i, end);
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
- return p.startsWith("/") ? p : join(process.cwd(), p);
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, completeness", inputSchema: { type: "object", properties: { project: { type: "string" } } } },
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.nodes || []) }] } };
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.nodes || []).find((n) => n.id.toLowerCase() === (args.name || "").toLowerCase());
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.edges || []).filter((e) => e.source.toLowerCase() === node.id.toLowerCase() || e.target.toLowerCase() === node.id.toLowerCase());
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 visited = new Set;
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 (visited.has(curr.id) || curr.depth > depth)
3014
+ if (visitedNodes.has(curr.id) || curr.depth > depth)
2988
3015
  continue;
2989
- visited.add(curr.id);
2990
- const node = (dag.nodes || []).find((n) => n.id.toLowerCase() === curr.id.toLowerCase());
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.edges || []).filter((e) => e.source.toLowerCase() === curr.id.toLowerCase() || e.target.toLowerCase() === curr.id.toLowerCase());
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
- subgraph.edges.push(e);
2996
- const next = e.source.toLowerCase() === curr.id.toLowerCase() ? e.target : e.source;
2997
- if (!visited.has(next))
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.nodes || []).filter((n) => n.id.toLowerCase().includes(q) && (!type || (n.type || "").toLowerCase().includes(type)));
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.project,
3018
- nodes: (dag.nodes || []).length,
3019
- edges: (dag.edges || []).length,
3020
- files: dag.files || dag.count || 0,
3021
- compiled: dag.compiled_at
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.nodes || []).find((n) => n.id.toLowerCase() === (args.entity || "").toLowerCase());
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.id,
3034
- properties: node.properties || {},
3035
- states: node.states || [],
3036
- lifecycle: node.lifecycle || []
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
- return p.startsWith("/") ? p : join2(process.cwd(), p);
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("The spec dog validate, analyze, generate, simulate .dog files").version(pkg.version);
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 dirs = [join2(d, "projects"), join2(d, "specs")];
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, "specs");
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} ${files.length} .dog files, ${100 - Math.round((missing.length * 3 + optional.length) / 20 * 100)}% complete`));
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 tmpl = {
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, "specs");
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)} ${n} .dog files`);
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, "specs");
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
- sourceBytes += Buffer.byteLength(content, "utf-8");
3215
- hash.update(content);
3216
- hash.update(`
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
- id: block.name,
3229
- type: block.type,
3230
- description: block.description || "",
3231
- file: f,
3232
- properties: Object.keys(block.properties).length,
3233
- states: block.states || [],
3234
- chars: section.content?.length || 0
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
- source: block.source,
3240
- target: block.target,
3241
- verb: block.verb,
3242
- cardinality: block.cardinality,
3243
- required: block.required,
3244
- file: f
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 = { version: "1.2", project: p, compiled_at: new Date().toISOString(), compiler: `dotdog@${pkg.version}`, integrity, nodes, edges };
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 savingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
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, "..", `${p}.dag`);
3256
- const report = { ...dag, tokens: { source_total: sourceTokens, dag_total: dagTokens, savings_pct: savingsPct, savings_tokens: savingsTokens } };
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 (${savingsPct}% savings, ${savingsTokens} tokens saved)`));
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} Spec Graph
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 expose .dag graph to AI agents over stdio").action((d = ".") => serve(d));
3299
- program2.command("analyze [dir]").description("Analyze a spec project score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
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, "specs");
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} ${a.sections} sections, ${(a.size / 1024).toFixed(1)}KB${detail}`));
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, "specs");
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 reads SPEC.dog scenarios, walks through steps, checks pre/postconditions."));
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, "specs");
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.0",
4
- "description": "The spec dog structured, AI-queryable software specifications. Write .dog specs, compile to .dag graphs, query via MCP. Built for humans and AI agents.",
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"