dotdog 0.3.5 → 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/dist/cli.js CHANGED
@@ -2534,8 +2534,150 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
2534
2534
  var source_default = chalk;
2535
2535
 
2536
2536
  // src/cli.ts
2537
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, mkdirSync, writeFileSync } from "fs";
2538
- import { join as join2, resolve as resolve2 } from "path";
2537
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
2538
+ import { join as join3, resolve as resolve2 } from "path";
2539
+
2540
+ // src/index.ts
2541
+ import { readFileSync, readdirSync } from "fs";
2542
+ import { join } from "path";
2543
+ function tokenize(text) {
2544
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2 && !stopWords.has(t));
2545
+ }
2546
+ var stopWords = new Set([
2547
+ "the",
2548
+ "and",
2549
+ "for",
2550
+ "are",
2551
+ "but",
2552
+ "not",
2553
+ "you",
2554
+ "all",
2555
+ "can",
2556
+ "had",
2557
+ "her",
2558
+ "was",
2559
+ "one",
2560
+ "our",
2561
+ "out",
2562
+ "has",
2563
+ "have",
2564
+ "been",
2565
+ "some",
2566
+ "than",
2567
+ "that",
2568
+ "this",
2569
+ "with",
2570
+ "from",
2571
+ "they",
2572
+ "will",
2573
+ "would",
2574
+ "which",
2575
+ "their",
2576
+ "there",
2577
+ "about",
2578
+ "into",
2579
+ "each",
2580
+ "said",
2581
+ "does",
2582
+ "also",
2583
+ "after",
2584
+ "before",
2585
+ "other",
2586
+ "more",
2587
+ "only",
2588
+ "over",
2589
+ "such",
2590
+ "when",
2591
+ "where",
2592
+ "what",
2593
+ "who",
2594
+ "how",
2595
+ "then",
2596
+ "just",
2597
+ "very",
2598
+ "much",
2599
+ "well",
2600
+ "should",
2601
+ "could",
2602
+ "through",
2603
+ "between"
2604
+ ]);
2605
+ function buildIndex(projectDir, projectName) {
2606
+ const files = readdirSync(projectDir).filter((f) => f.endsWith(".dog"));
2607
+ const entries = [];
2608
+ const docFreq = new Map;
2609
+ for (const file of files) {
2610
+ const content = readFileSync(join(projectDir, file), "utf-8");
2611
+ const sections = content.split(/\n(?=##\s)/);
2612
+ for (const section of sections) {
2613
+ const lines = section.split(`
2614
+ `);
2615
+ const heading = lines[0]?.replace(/^#+\s*/, "") || "(root)";
2616
+ const body = lines.slice(1).join(`
2617
+ `);
2618
+ if (body.trim().length < 20)
2619
+ continue;
2620
+ const terms = tokenize(body);
2621
+ if (terms.length < 5)
2622
+ continue;
2623
+ const tf = new Map;
2624
+ for (const t of terms) {
2625
+ tf.set(t, (tf.get(t) || 0) + 1);
2626
+ docFreq.set(t, (docFreq.get(t) || 0) + 1);
2627
+ }
2628
+ entries.push({
2629
+ section: heading,
2630
+ heading,
2631
+ file,
2632
+ content: body.slice(0, 500),
2633
+ vector: []
2634
+ });
2635
+ }
2636
+ }
2637
+ const vocabulary = [...docFreq.entries()].filter(([_, df2]) => df2 >= 2).sort((a, b) => b[1] - a[1]).slice(0, 1000).map(([term]) => term);
2638
+ const df = vocabulary.map((t) => docFreq.get(t) || 0);
2639
+ const N = entries.length;
2640
+ for (const entry of entries) {
2641
+ const terms = tokenize(entry.content);
2642
+ const tf = new Map;
2643
+ for (const t of terms)
2644
+ tf.set(t, (tf.get(t) || 0) + 1);
2645
+ entry.vector = vocabulary.map((term) => {
2646
+ const termFreq = tf.get(term) || 0;
2647
+ if (termFreq === 0)
2648
+ return 0;
2649
+ const docFreqTerm = docFreq.get(term) || 1;
2650
+ return termFreq / terms.length * Math.log(N / docFreqTerm);
2651
+ });
2652
+ }
2653
+ return {
2654
+ version: "1.0",
2655
+ project: projectName,
2656
+ built: new Date().toISOString(),
2657
+ entries,
2658
+ vocabulary,
2659
+ df
2660
+ };
2661
+ }
2662
+ function searchIndex(index, query, limit = 10) {
2663
+ const queryTerms = tokenize(query);
2664
+ const queryVec = index.vocabulary.map((term) => queryTerms.includes(term) ? 1 : 0);
2665
+ const results = index.entries.map((entry) => {
2666
+ let dot = 0;
2667
+ let entryMag = 0;
2668
+ let queryMag = 0;
2669
+ for (let j = 0;j < queryVec.length; j++) {
2670
+ dot += queryVec[j] * entry.vector[j];
2671
+ queryMag += queryVec[j] * queryVec[j];
2672
+ entryMag += entry.vector[j] * entry.vector[j];
2673
+ }
2674
+ const score = queryMag > 0 && entryMag > 0 ? dot / (Math.sqrt(queryMag) * Math.sqrt(entryMag)) : 0;
2675
+ return { entry, score };
2676
+ });
2677
+ return results.filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
2678
+ }
2679
+
2680
+ // src/cli.ts
2539
2681
  import { homedir as homedir2 } from "os";
2540
2682
 
2541
2683
  // src/parser.ts
@@ -2809,12 +2951,11 @@ function buildPredictionNode(name, description, yaml, lineStart, lineEnd) {
2809
2951
  description,
2810
2952
  trigger: yaml.trigger || "",
2811
2953
  timeframe: yaml.timeframe || "",
2812
- confidence: typeof yaml.confidence === "number" ? yaml.confidence : 0,
2954
+ confidence: yaml.confidence || 0,
2813
2955
  measurement: yaml.measurement || "",
2814
- preconditions: Array.isArray(yaml.preconditions) ? yaml.preconditions : [],
2815
- postconditions: Array.isArray(yaml.postconditions) ? yaml.postconditions : [],
2956
+ status: yaml.status || "pending",
2816
2957
  yaml,
2817
- lineStart: lineStart + 1,
2958
+ lineStart,
2818
2959
  lineEnd
2819
2960
  };
2820
2961
  }
@@ -2946,14 +3087,14 @@ function parseInlineObject(value) {
2946
3087
  return obj;
2947
3088
  }
2948
3089
  // src/serve.ts
2949
- import { existsSync, readdirSync, readFileSync } from "fs";
2950
- import { join, resolve } from "path";
3090
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
3091
+ import { join as join2, resolve } from "path";
2951
3092
  import { homedir } from "os";
2952
3093
  import * as readline from "readline";
2953
3094
  function resolvePath(p) {
2954
3095
  if (p.startsWith("~"))
2955
- p = join(homedir(), p.slice(1));
2956
- const resolved = p.startsWith("/") ? p : join(process.cwd(), p);
3096
+ p = join2(homedir(), p.slice(1));
3097
+ const resolved = p.startsWith("/") ? p : join2(process.cwd(), p);
2957
3098
  if (!p.startsWith("/") && !p.startsWith("~")) {
2958
3099
  const rel = resolve(process.cwd(), p);
2959
3100
  const cwd = process.cwd();
@@ -2967,29 +3108,63 @@ function resolvePath(p) {
2967
3108
  }
2968
3109
  return resolved;
2969
3110
  }
3111
+ var isV2 = (n) => Array.isArray(n) && typeof n[0] === "number";
2970
3112
  var N = (dag) => dag.n || dag.nodes || [];
2971
- var E = (dag) => dag.e || dag.edges || [];
3113
+ function edgeToObj(n, tgtIdx, v2e) {
3114
+ return { t: String(tgtIdx), v: v2e[1] || "", c: v2e[2] || "", r: v2e[3] || 0 };
3115
+ }
3116
+ function nodeEdges(n) {
3117
+ if (isV2(n))
3118
+ return (n[6] || []).map((e) => edgeToObj(n, e[0], e));
3119
+ return n.es || [];
3120
+ }
3121
+ function E(dag) {
3122
+ if (dag.e)
3123
+ return dag.e;
3124
+ const edges = [];
3125
+ const seen = new Set;
3126
+ for (const node of N(dag)) {
3127
+ for (const e of nodeEdges(node)) {
3128
+ const src = isV2(node) ? String(node[0]) : node.i || node.id || "";
3129
+ const key = `${src}→${e.t}:${e.v}`;
3130
+ if (!seen.has(key)) {
3131
+ seen.add(key);
3132
+ edges.push({ s: src, t: e.t, v: e.v, d: e.d, c: e.c, r: e.r });
3133
+ }
3134
+ }
3135
+ }
3136
+ return edges;
3137
+ }
2972
3138
  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 || [];
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 || [];
2978
3153
  var es = (e) => e.s || e.source || "";
2979
3154
  var et = (e) => e.t || e.target || "";
2980
3155
  function serve(dir = ".") {
2981
3156
  const root = resolvePath(dir);
2982
3157
  const dagCache = new Map;
2983
3158
  function loadDags() {
2984
- const dirs = [join(root, "projects"), join(root, "specs"), root];
3159
+ const dirs = [join2(root, "projects"), join2(root, "specs"), root];
2985
3160
  for (const dd of dirs) {
2986
- if (!existsSync(dd))
3161
+ if (!existsSync2(dd))
2987
3162
  continue;
2988
- const projects = readdirSync(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3163
+ const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
2989
3164
  for (const p of projects) {
2990
- const dagFile = join(dd, p, `${p}.dag`);
2991
- if (existsSync(dagFile)) {
2992
- dagCache.set(p, JSON.parse(readFileSync(dagFile, "utf-8")));
3165
+ const dagFile = join2(dd, p, `${p}.dag`);
3166
+ if (existsSync2(dagFile)) {
3167
+ dagCache.set(p, JSON.parse(readFileSync2(dagFile, "utf-8")));
2993
3168
  }
2994
3169
  }
2995
3170
  }
@@ -3044,7 +3219,7 @@ function serve(dir = ".") {
3044
3219
  if (!node)
3045
3220
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
3046
3221
  const edges = E(dag).filter((e) => es(e).toLowerCase() === ni(node).toLowerCase() || et(e).toLowerCase() === ni(node).toLowerCase());
3047
- 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 }) }] } };
3048
3223
  }
3049
3224
  if (name === "traverse") {
3050
3225
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
@@ -3062,7 +3237,7 @@ function serve(dir = ".") {
3062
3237
  visitedNodes.add(curr.id);
3063
3238
  const node = N(dag).find((n) => ni(n).toLowerCase() === curr.id.toLowerCase());
3064
3239
  if (node)
3065
- 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);
3066
3241
  const edges = E(dag).filter((e) => es(e).toLowerCase() === curr.id.toLowerCase() || et(e).toLowerCase() === curr.id.toLowerCase());
3067
3242
  for (const e of edges) {
3068
3243
  const edgeKey = `${es(e)}→${et(e)}`;
@@ -3096,6 +3271,8 @@ function serve(dir = ".") {
3096
3271
  nodes: N(dag).length,
3097
3272
  edges: E(dag).length,
3098
3273
  version: dag.v || dag.version || "",
3274
+ order: dag.o || [],
3275
+ cycles: dag.cy !== undefined ? dag.cy : null,
3099
3276
  savings: tk.sv || tk.savings_pct || 0,
3100
3277
  method: tk.m || tk.method || ""
3101
3278
  };
@@ -3138,8 +3315,8 @@ function serve(dir = ".") {
3138
3315
  // src/cli.ts
3139
3316
  function resolvePath2(p) {
3140
3317
  if (p.startsWith("~"))
3141
- p = join2(homedir2(), p.slice(1));
3142
- const resolved = p.startsWith("/") ? p : join2(process.cwd(), p);
3318
+ p = join3(homedir2(), p.slice(1));
3319
+ const resolved = p.startsWith("/") ? p : join3(process.cwd(), p);
3143
3320
  if (!p.startsWith("/") && !p.startsWith("~")) {
3144
3321
  const rel = resolve2(process.cwd(), p);
3145
3322
  const cwd = process.cwd();
@@ -3185,22 +3362,22 @@ function parseSections2(markdown) {
3185
3362
  return sections;
3186
3363
  }
3187
3364
  var program2 = new Command;
3188
- var pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
3365
+ var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf-8"));
3189
3366
  program2.name("spec").alias("dotdog").description("CLI for structured software specs : validate .dog, compile .dag, query via MCP").version(pkg.version);
3190
3367
  program2.command("validate [dir]").action((d = ".") => {
3191
3368
  const dir = resolvePath2(d);
3192
- const dirs = [join2(dir, "projects"), join2(dir, "specs")];
3369
+ const dirs = [join3(dir, "projects"), join3(dir, "specs")];
3193
3370
  let found = false, hasErrors = false;
3194
3371
  for (const dd of dirs) {
3195
- if (!existsSync2(dd))
3372
+ if (!existsSync3(dd))
3196
3373
  continue;
3197
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3374
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3198
3375
  for (const p of projects) {
3199
3376
  found = true;
3200
- const pd = join2(dd, p);
3201
- if (!existsSync2(join2(pd, "SPEC.dog")))
3377
+ const pd = join3(dd, p);
3378
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3202
3379
  continue;
3203
- const files = existsSync2(pd) ? readdirSync2(pd).filter((f) => f.endsWith(".dog")) : [];
3380
+ const files = existsSync3(pd) ? readdirSync3(pd).filter((f) => f.endsWith(".dog")) : [];
3204
3381
  const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
3205
3382
  const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
3206
3383
  console.log(source_default.bold(`
@@ -3221,7 +3398,7 @@ program2.command("validate [dir]").action((d = ".") => {
3221
3398
  process.exit(1);
3222
3399
  });
3223
3400
  program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data-model.dog").action((p, opts) => {
3224
- const d = join2(process.cwd(), "specs", p);
3401
+ const d = join3(process.cwd(), "specs", p);
3225
3402
  mkdirSync(d, { recursive: true });
3226
3403
  const full = {
3227
3404
  "SPEC.dog": `# Project
@@ -3269,7 +3446,13 @@ program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data
3269
3446
  };
3270
3447
  const tmpl = opts.minimal ? minimal : full;
3271
3448
  for (const [f, c] of Object.entries(tmpl)) {
3272
- writeFileSync(join2(d, f), c);
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
+ }
3273
3456
  console.log(source_default.green(` ✓ ${f}`));
3274
3457
  }
3275
3458
  console.log(source_default.bold(`
@@ -3277,23 +3460,23 @@ Project "${p}" initialized. Fill in SPEC.dog then run spec validate.`));
3277
3460
  });
3278
3461
  program2.command("list").action(() => {
3279
3462
  for (const d of ["projects", "specs"]) {
3280
- const dd = join2(process.cwd(), d);
3281
- if (!existsSync2(dd))
3463
+ const dd = join3(process.cwd(), d);
3464
+ if (!existsSync3(dd))
3282
3465
  continue;
3283
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3466
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3284
3467
  if (!projects.length)
3285
3468
  continue;
3286
3469
  console.log(source_default.bold(`
3287
3470
  ${d}/`));
3288
3471
  for (const p of projects) {
3289
- const sp = join2(dd, p);
3290
- const n = existsSync2(sp) ? readdirSync2(sp).filter((f) => f.endsWith(".dog")).length : 0;
3472
+ const sp = join3(dd, p);
3473
+ const n = existsSync3(sp) ? readdirSync3(sp).filter((f) => f.endsWith(".dog")).length : 0;
3291
3474
  console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
3292
3475
  }
3293
3476
  }
3294
3477
  });
3295
3478
  program2.command("parse <file>").action((f) => {
3296
- const c = readFileSync2(f, "utf-8");
3479
+ const c = readFileSync3(f, "utf-8");
3297
3480
  const s = parseSections2(c);
3298
3481
  console.log(source_default.bold(`
3299
3482
  ${s.length} sections`));
@@ -3302,26 +3485,26 @@ ${s.length} sections`));
3302
3485
  });
3303
3486
  program2.command("compile [dir]").option("-o, --output <file>").action((d = ".", opts) => {
3304
3487
  const dir = resolvePath2(d);
3305
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3488
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3306
3489
  let found = false;
3307
3490
  for (const dd of dirs) {
3308
- if (!existsSync2(dd))
3491
+ if (!existsSync3(dd))
3309
3492
  continue;
3310
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3493
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3311
3494
  for (const p of projects) {
3312
- const pd = join2(dd, p);
3313
- if (!existsSync2(join2(pd, "SPEC.dog")))
3495
+ const pd = join3(dd, p);
3496
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3314
3497
  continue;
3315
- if (!existsSync2(pd))
3498
+ if (!existsSync3(pd))
3316
3499
  continue;
3317
- const files = readdirSync2(pd).filter((f) => f.endsWith(".dog")).sort();
3500
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog")).sort();
3318
3501
  if (!files.length)
3319
3502
  continue;
3320
3503
  found = true;
3321
3504
  const sources = {};
3322
3505
  let sourceBytes = 0, contentBytes = 0;
3323
3506
  for (const f of files) {
3324
- const content = readFileSync2(join2(pd, f), "utf-8");
3507
+ const content = readFileSync3(join3(pd, f), "utf-8");
3325
3508
  sources[f] = content;
3326
3509
  const bytes = Buffer.byteLength(content, "utf-8");
3327
3510
  sourceBytes += bytes;
@@ -3394,16 +3577,72 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3394
3577
  }
3395
3578
  }
3396
3579
  }
3397
- const dag = { v: "1.4", p, c: `dotdog@${pkg.version}`, n: nodes, e: edges };
3580
+ const entityNames = new Set(nodes.map((n) => n.i));
3581
+ for (const e of edges) {
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);
3585
+ }
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);
3589
+ }
3590
+ }
3591
+ const nodeIds = new Map;
3592
+ nodes.forEach((n, i) => nodeIds.set(n.i, i));
3593
+ const v2nodes = [];
3594
+ for (let j = 0;j < nodes.length; j++) {
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);
3635
+ }
3636
+ const dag = { v: 2, p, n: v2nodes };
3398
3637
  const dagJson = JSON.stringify(dag);
3399
3638
  const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
3400
3639
  const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
3401
3640
  const contentSavingsPct = contentTokens > 0 ? Math.round((1 - dagTokens / contentTokens) * 1000) / 10 : 0;
3402
3641
  const savingsTokens = sourceTokens - dagTokens;
3403
- const outPath = opts.output || join2(pd, `${p}.dag`);
3642
+ const outPath = opts.output || join3(pd, `${p}.dag`);
3404
3643
  const tokens = { m: "chars/4", st: sourceTokens, ct: contentTokens, dt: dagTokens, sv: allSavingsPct, cs: contentSavingsPct, saved: savingsTokens };
3405
3644
  const report = { ...dag, tk: tokens };
3406
- writeFileSync(outPath, JSON.stringify(report, null, 2));
3645
+ writeFileSync2(outPath, JSON.stringify(report, null, 2));
3407
3646
  console.log(source_default.green(` ✓ ${outPath}`));
3408
3647
  console.log(source_default.gray(` ${nodes.length} nodes, ${edges.length} edges, ${files.length} files`));
3409
3648
  console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${allSavingsPct}% savings, ${contentSavingsPct}% content-only, ${savingsTokens} saved)`));
@@ -3414,37 +3653,41 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3414
3653
  });
3415
3654
  program2.command("tokens [dir]").action((d = ".") => {
3416
3655
  const dir = resolvePath2(d);
3417
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3656
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3418
3657
  let found = false;
3419
3658
  for (const dd of dirs) {
3420
- if (!existsSync2(dd))
3659
+ if (!existsSync3(dd))
3421
3660
  continue;
3422
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3661
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3423
3662
  for (const p of projects) {
3424
- const pd = join2(dd, p);
3425
- if (!existsSync2(join2(pd, "SPEC.dog")))
3663
+ const pd = join3(dd, p);
3664
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3426
3665
  continue;
3427
- const dagFile = join2(pd, `${p}.dag`);
3428
- if (!existsSync2(dagFile))
3666
+ const dagFile = join3(pd, `${p}.dag`);
3667
+ if (!existsSync3(dagFile))
3429
3668
  continue;
3430
3669
  found = true;
3431
- const dogFiles = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
3670
+ const dogFiles = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
3432
3671
  let sourceBytes = 0, contentBytes = 0;
3433
3672
  for (const f of dogFiles) {
3434
- const bytes = Buffer.byteLength(readFileSync2(join2(pd, f), "utf-8"), "utf-8");
3673
+ const bytes = Buffer.byteLength(readFileSync3(join3(pd, f), "utf-8"), "utf-8");
3435
3674
  sourceBytes += bytes;
3436
3675
  if (bytes >= 100)
3437
3676
  contentBytes += bytes;
3438
3677
  }
3439
- const dagBytes = Buffer.byteLength(readFileSync2(dagFile, "utf-8"), "utf-8");
3678
+ const dagBytes = Buffer.byteLength(readFileSync3(dagFile, "utf-8"), "utf-8");
3440
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;
3441
3684
  console.log(source_default.bold(`
3442
3685
  ${p}`));
3443
3686
  console.log(source_default.gray(` ${dogFiles.length} .dog files: ${sourceBytes} bytes`));
3444
- console.log(source_default.gray(` .dag file: ${dagBytes} bytes`));
3445
- console.log(source_default.green(` ${savings}% smaller (${sourceBytes - dagBytes} bytes saved)`));
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)`));
3446
3689
  if (contentBytes && contentBytes !== sourceBytes) {
3447
- const cs = Math.round((1 - dagBytes / contentBytes) * 1000) / 10;
3690
+ const cs = Math.round((1 - dagOnlyBytes / contentBytes) * 1000) / 10;
3448
3691
  console.log(source_default.gray(` content-only: ${contentBytes} bytes → ${cs}% savings`));
3449
3692
  }
3450
3693
  }
@@ -3454,27 +3697,67 @@ program2.command("tokens [dir]").action((d = ".") => {
3454
3697
  });
3455
3698
  program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts) => {
3456
3699
  const dir = resolvePath2(d);
3457
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3700
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3458
3701
  for (const dd of dirs) {
3459
- if (!existsSync2(dd))
3702
+ if (!existsSync3(dd))
3460
3703
  continue;
3461
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3704
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3462
3705
  for (const p of projects) {
3463
- const dagFile = join2(dd, p, `${p}.dag`);
3464
- if (!existsSync2(dagFile))
3706
+ const dagFile = join3(dd, p, `${p}.dag`);
3707
+ if (!existsSync3(dagFile))
3465
3708
  continue;
3466
- const dag = JSON.parse(readFileSync2(dagFile, "utf-8"));
3709
+ const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
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_");
3467
3714
  let out = "```mermaid\ngraph LR\n";
3468
- for (const n of dag.nodes || [])
3469
- out += ` ${n.id.replace(/\s+/g, "_")}[${n.id}]
3715
+ for (const n of nodes) {
3716
+ const raw = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
3717
+ if (raw)
3718
+ out += ` ${slug(raw)}[${raw}]
3470
3719
  `;
3471
- for (const e of dag.edges || [])
3472
- out += ` ${e.source.replace(/\s+/g, "_")} -->|${e.verb || ""}| ${e.target.replace(/\s+/g, "_")}
3720
+ }
3721
+ const seen = new Set;
3722
+ for (const n of nodes) {
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);
3737
+ const key = `${src}→${tgt}:${verb}`;
3738
+ if (!seen.has(key) && src && tgt) {
3739
+ seen.add(key);
3740
+ out += ` ${src} -->|${verb}| ${tgt}
3473
3741
  `;
3742
+ }
3743
+ }
3744
+ }
3745
+ const legacyEdges = dag.e || dag.edges || [];
3746
+ for (const e of legacyEdges) {
3747
+ const src = slug(e.s || e.source || "");
3748
+ const tgt = slug(e.t || e.target || "");
3749
+ const verb = e.v || e.verb || "";
3750
+ const key = `${src}→${tgt}:${verb}`;
3751
+ if (!seen.has(key) && src && tgt) {
3752
+ seen.add(key);
3753
+ out += ` ${src} -->|${verb}| ${tgt}
3754
+ `;
3755
+ }
3756
+ }
3474
3757
  out += "```\n";
3475
3758
  if (opts.save) {
3476
- const outFile = join2(dd, p, "..", `${p}.md`);
3477
- writeFileSync(outFile, `# ${p} : Spec Graph
3759
+ const outFile = join3(dd, p, `${p}.md`);
3760
+ writeFileSync2(outFile, `# ${p} : Spec Graph
3478
3761
 
3479
3762
  ${out}`);
3480
3763
  console.log(source_default.green(` ✓ ${outFile}`));
@@ -3486,24 +3769,24 @@ ${out}`);
3486
3769
  program2.command("serve [dir]").description("MCP server : expose .dag graph to AI agents over stdio").action((d = ".") => serve(resolvePath2(d)));
3487
3770
  program2.command("analyze [dir]").description("Analyze a spec project : score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
3488
3771
  const dir = resolvePath2(d);
3489
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3772
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3490
3773
  console.log(source_default.bold(`
3491
3774
  Spec Analysis
3492
3775
  `));
3493
3776
  let found = false, hasGaps = false;
3494
3777
  for (const dd of dirs) {
3495
- if (!existsSync2(dd))
3778
+ if (!existsSync3(dd))
3496
3779
  continue;
3497
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3780
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3498
3781
  for (const p of projects) {
3499
3782
  if (opts.project && p !== opts.project)
3500
3783
  continue;
3501
- const pd = join2(dd, p);
3502
- if (!existsSync2(join2(pd, "SPEC.dog")))
3784
+ const pd = join3(dd, p);
3785
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3503
3786
  continue;
3504
- if (!existsSync2(pd))
3787
+ if (!existsSync3(pd))
3505
3788
  continue;
3506
- const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
3789
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
3507
3790
  if (!files.length)
3508
3791
  continue;
3509
3792
  found = true;
@@ -3514,7 +3797,7 @@ Spec Analysis
3514
3797
  const allRelationships = [];
3515
3798
  const analyses = [];
3516
3799
  for (const f of files) {
3517
- const content = readFileSync2(join2(pd, f), "utf-8");
3800
+ const content = readFileSync3(join3(pd, f), "utf-8");
3518
3801
  const ast = parse(content);
3519
3802
  const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
3520
3803
  const rels = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "relationship"));
@@ -3556,6 +3839,39 @@ Spec Analysis
3556
3839
  if (r.target && !entityNames.has(r.target))
3557
3840
  gaps.push(`\uD83D\uDFE1 Relationship: unknown target "${r.target}"`);
3558
3841
  }
3842
+ const contradictions = [];
3843
+ const relMap = new Map;
3844
+ for (const r of allRelationships) {
3845
+ const key = `${r.source}→${r.target}`;
3846
+ if (!relMap.has(key))
3847
+ relMap.set(key, []);
3848
+ relMap.get(key).push(r);
3849
+ }
3850
+ for (const [key, rels] of relMap) {
3851
+ if (rels.length > 1) {
3852
+ const verbs = [...new Set(rels.map((r) => r.verb))];
3853
+ if (verbs.length > 1)
3854
+ contradictions.push(`\uD83D\uDD34 Contradiction: "${key}" has conflicting verbs: ${verbs.join(", ")}`);
3855
+ else
3856
+ contradictions.push(`\uD83D\uDFE1 Duplicate: "${key}" appears ${rels.length} times with same verb "${verbs[0]}"`);
3857
+ }
3858
+ }
3859
+ for (const r of allRelationships) {
3860
+ const reverse = allRelationships.find((r2) => r2.source === r.target && r2.target === r.source);
3861
+ if (reverse && r.verb !== reverse.verb && r.source < r.target) {
3862
+ contradictions.push(`\uD83D\uDFE1 Bidirectional: "${r.source}→${r.target}" verb "${r.verb}" vs "${reverse.source}→${reverse.target}" verb "${reverse.verb}"`);
3863
+ }
3864
+ }
3865
+ for (const c of contradictions) {
3866
+ if (c.startsWith("\uD83D\uDD34"))
3867
+ hasGaps = true;
3868
+ }
3869
+ if (contradictions.length > 0) {
3870
+ console.log(source_default.bold(`
3871
+ Contradictions (${contradictions.length})`));
3872
+ for (const c of contradictions)
3873
+ console.log(` ${c}`);
3874
+ }
3559
3875
  if (gaps.length > 0) {
3560
3876
  console.log(source_default.bold(`
3561
3877
  Gaps (${gaps.length})`));
@@ -3575,19 +3891,19 @@ Spec Analysis
3575
3891
  });
3576
3892
  program2.command("generate [dir]").description("Generate missing spec files from SPEC.dog").option("-p, --project <name>").action((d = ".", opts) => {
3577
3893
  const dir = resolvePath2(d);
3578
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3894
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3579
3895
  let specContent = "", specDir = "";
3580
3896
  for (const dd of dirs) {
3581
- if (!existsSync2(dd))
3897
+ if (!existsSync3(dd))
3582
3898
  continue;
3583
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3899
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3584
3900
  for (const p of projects) {
3585
3901
  if (opts.project && p !== opts.project)
3586
3902
  continue;
3587
- const pd = join2(dd, p);
3588
- const sp = join2(pd, "SPEC.dog");
3589
- if (existsSync2(sp)) {
3590
- specContent = readFileSync2(sp, "utf-8");
3903
+ const pd = join3(dd, p);
3904
+ const sp = join3(pd, "SPEC.dog");
3905
+ if (existsSync3(sp)) {
3906
+ specContent = readFileSync3(sp, "utf-8");
3591
3907
  specDir = pd;
3592
3908
  break;
3593
3909
  }
@@ -3618,7 +3934,7 @@ Spec Generator
3618
3934
  uiStrings.push({ screen: section.heading, element: "label", text: m });
3619
3935
  }
3620
3936
  }
3621
- if (!existsSync2(join2(specDir, "data-model.dog")) && entities.length > 0) {
3937
+ if (!existsSync3(join3(specDir, "data-model.dog")) && entities.length > 0) {
3622
3938
  let dm = `# Data Model
3623
3939
 
3624
3940
  ## Core Entities
@@ -3651,10 +3967,16 @@ ${e.description || "No description."}
3651
3967
  `;
3652
3968
  dm += "```\n\n";
3653
3969
  }
3654
- writeFileSync(join2(specDir, "data-model.dog"), dm);
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
+ }
3655
3977
  console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
3656
3978
  }
3657
- if (!existsSync2(join2(specDir, "COPY.dog")) && uiStrings.length > 0) {
3979
+ if (!existsSync3(join3(specDir, "COPY.dog")) && uiStrings.length > 0) {
3658
3980
  let copy = `# App Copy
3659
3981
 
3660
3982
  | Screen | Element | Copy |
@@ -3663,10 +3985,16 @@ ${e.description || "No description."}
3663
3985
  for (const s of uiStrings)
3664
3986
  copy += `| ${s.screen} | ${s.element} | ${s.text} |
3665
3987
  `;
3666
- writeFileSync(join2(specDir, "COPY.dog"), copy);
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
+ }
3667
3995
  console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
3668
3996
  }
3669
- if (!existsSync2(join2(specDir, "INDEX.dog"))) {
3997
+ if (!existsSync3(join3(specDir, "INDEX.dog"))) {
3670
3998
  let idx = `# INDEX
3671
3999
 
3672
4000
  | You are... | Start here | Then... |
@@ -3678,41 +4006,149 @@ ${e.description || "No description."}
3678
4006
  `;
3679
4007
  idx += `| Designer | SPEC.dog | COPY.dog |
3680
4008
  `;
3681
- writeFileSync(join2(specDir, "INDEX.dog"), idx);
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
+ }
3682
4016
  console.log(source_default.green(" ✓ INDEX.dog"));
3683
4017
  }
3684
4018
  console.log(source_default.bold(`
3685
4019
  Run dotdog validate to verify.
3686
4020
  `));
3687
4021
  });
3688
- program2.command("simulate <scenario>").description("Run a simulation scenario (phase 1 stub)").option("-p, --project <name>", "Project name", "default").action((scenario, opts) => {
4022
+ program2.command("simulate <scenario>").description("Walk through a scenario, check pre/postconditions").option("-p, --project <name>").action((scenario, opts) => {
4023
+ const dir = resolvePath2(".");
4024
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4025
+ let projectDir = "";
4026
+ for (const dd of dirs) {
4027
+ if (!existsSync3(dd))
4028
+ continue;
4029
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4030
+ for (const p of projects) {
4031
+ if (opts.project && p !== opts.project)
4032
+ continue;
4033
+ const pd = join3(dd, p);
4034
+ if (existsSync3(join3(pd, "SPEC.dog"))) {
4035
+ projectDir = pd;
4036
+ break;
4037
+ }
4038
+ }
4039
+ if (projectDir)
4040
+ break;
4041
+ }
4042
+ if (!projectDir) {
4043
+ console.log(source_default.red("Project not found."));
4044
+ return;
4045
+ }
4046
+ const dagFile = join3(projectDir, `${opts.project || projectDir.split("/").pop()}.dag`);
4047
+ let entities = [], relationships = [];
4048
+ if (existsSync3(dagFile)) {
4049
+ const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
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());
4053
+ if (dag.e || dag.edges) {
4054
+ relationships = dag.e || dag.edges || [];
4055
+ } else {
4056
+ const seen = new Set;
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}`;
4064
+ if (!seen.has(key)) {
4065
+ seen.add(key);
4066
+ relationships.push({ s: srcName, t: tgtName, v: verb });
4067
+ }
4068
+ }
4069
+ }
4070
+ }
4071
+ }
4072
+ const specFile = join3(projectDir, "SPEC.dog");
4073
+ const specContent = existsSync3(specFile) ? readFileSync3(specFile, "utf-8") : "";
3689
4074
  console.log(source_default.bold(`
3690
- Simulation: ${scenario} (project: ${opts.project})
3691
- `));
3692
- console.log(source_default.gray("Simulation engine : reads SPEC.dog scenarios, walks through steps, checks pre/postconditions."));
3693
- console.log(source_default.gray("Full engine coming in a future release."));
4075
+ Simulation: ${scenario}`));
4076
+ console.log(source_default.gray(`Project: ${projectDir.split("/").pop()} | Entities: ${entities.length} | Relationships: ${relationships.length}`));
4077
+ const steps = [];
4078
+ const stepMatches = specContent.match(/\[(\d+)\/\d+\]\s*(.+)/g);
4079
+ if (stepMatches) {
4080
+ for (const m of stepMatches) {
4081
+ const step = m.replace(/\[\d+\/\d+\]\s*/, "");
4082
+ if (step)
4083
+ steps.push(step);
4084
+ }
4085
+ }
4086
+ if (steps.length === 0) {
4087
+ console.log(source_default.yellow(`
4088
+ No scenario steps found in SPEC.dog.`));
4089
+ console.log(source_default.gray(" Add steps like: [1/3] User taps button"));
4090
+ return;
4091
+ }
4092
+ let passed = 0, failed = 0;
4093
+ for (let i = 0;i < steps.length; i++) {
4094
+ const step = steps[i];
4095
+ console.log(source_default.cyan(`
4096
+ [${i + 1}/${steps.length}] ${step}`));
4097
+ let foundRef = false;
4098
+ for (const e of entities) {
4099
+ if (step.toLowerCase().includes(e)) {
4100
+ console.log(source_default.green(` ✓ References entity: ${e}`));
4101
+ foundRef = true;
4102
+ }
4103
+ }
4104
+ for (const r of relationships) {
4105
+ const src = (r.s || r.source || "").toLowerCase();
4106
+ const tgt = (r.t || r.target || "").toLowerCase();
4107
+ const verb = (r.v || r.verb || "").toLowerCase();
4108
+ if (step.toLowerCase().includes(src) && step.toLowerCase().includes(tgt)) {
4109
+ console.log(source_default.green(` ✓ References relationship: ${src} → ${tgt}`));
4110
+ foundRef = true;
4111
+ }
4112
+ if (verb && step.toLowerCase().includes(verb)) {
4113
+ console.log(source_default.green(` ✓ References verb: ${verb}`));
4114
+ foundRef = true;
4115
+ }
4116
+ }
4117
+ if (!foundRef) {
4118
+ console.log(source_default.yellow(` ⚠ No entity or relationship references found`));
4119
+ failed++;
4120
+ } else {
4121
+ passed++;
4122
+ }
4123
+ }
4124
+ console.log(source_default.bold(`
4125
+ RESULT: ${failed === 0 ? source_default.green("Success") : source_default.yellow("Partial")} (${passed}/${steps.length} steps passed)`));
4126
+ if (failed > 0)
4127
+ console.log(source_default.red(` ${failed} steps have no spec references — entities or relationships may be missing.`));
4128
+ if (failed === 0)
4129
+ console.log(source_default.green(" All steps reference known entities and relationships."));
3694
4130
  });
3695
4131
  program2.command("staleness [dir]").action((d = ".") => {
3696
4132
  const dir = resolvePath2(d);
3697
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
4133
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3698
4134
  console.log(source_default.bold(`Staleness Audit
3699
4135
  `));
3700
4136
  for (const dd of dirs) {
3701
- if (!existsSync2(dd))
4137
+ if (!existsSync3(dd))
3702
4138
  continue;
3703
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4139
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3704
4140
  for (const p of projects) {
3705
- const pd = join2(dd, p);
3706
- if (!existsSync2(join2(pd, "SPEC.dog")))
4141
+ const pd = join3(dd, p);
4142
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3707
4143
  continue;
3708
- if (!existsSync2(pd))
4144
+ if (!existsSync3(pd))
3709
4145
  continue;
3710
- const planFile = join2(pd, "plan.dog");
3711
- if (!existsSync2(planFile)) {
4146
+ const planFile = join3(pd, "plan.dog");
4147
+ if (!existsSync3(planFile)) {
3712
4148
  console.log(source_default.yellow(` ${p}: No plan.dog`));
3713
4149
  continue;
3714
4150
  }
3715
- const plan = readFileSync2(planFile, "utf-8");
4151
+ const plan = readFileSync3(planFile, "utf-8");
3716
4152
  const tasks = [...plan.matchAll(/^\s*- \[([ x])\]\s+(.+)/gm)];
3717
4153
  let issues = 0;
3718
4154
  for (const m of tasks) {
@@ -3723,14 +4159,15 @@ program2.command("staleness [dir]").action((d = ".") => {
3723
4159
  const phase = phaseMatch ? parseInt(phaseMatch[1]) : 99;
3724
4160
  if (phase > 3)
3725
4161
  continue;
3726
- if (text.includes("npm publish") || text.includes("npm install")) {
3727
- try {
3728
- const pkg2 = JSON.parse(readFileSync2(join2(resolvePath2("."), "packages/dotdog/package.json"), "utf-8"));
3729
- if (pkg2.version && !done) {
4162
+ if ((text.includes("npm publish") || text.includes("npm install")) && !done) {
4163
+ const pkgPath = join3(resolvePath2("."), "packages/dotdog/package.json");
4164
+ if (existsSync3(pkgPath)) {
4165
+ const pkg2 = JSON.parse(readFileSync3(pkgPath, "utf-8"));
4166
+ if (pkg2.version) {
3730
4167
  console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
3731
4168
  issues++;
3732
4169
  }
3733
- } catch {}
4170
+ }
3734
4171
  }
3735
4172
  if (text.includes("compile")) {
3736
4173
  if (!done) {
@@ -3739,7 +4176,7 @@ program2.command("staleness [dir]").action((d = ".") => {
3739
4176
  }
3740
4177
  }
3741
4178
  if (text.includes("generate") && !done) {
3742
- if (existsSync2(join2(dir, "packages/dotdog/src/cli.ts")) || existsSync2(join2(dir, "packages/spec-cli/src/generate.ts"))) {
4179
+ if (existsSync3(join3(dir, "packages/dotdog/src/cli.ts")) || existsSync3(join3(dir, "packages/spec-cli/src/generate.ts"))) {
3743
4180
  console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
3744
4181
  issues++;
3745
4182
  }
@@ -3759,6 +4196,214 @@ program2.command("woof").action(() => {
3759
4196
  console.log(" / (_____/");
3760
4197
  console.log("/_____/ U");
3761
4198
  });
4199
+ program2.command("index [dir]").description("Build search index for semantic queries").action((d = ".") => {
4200
+ const dir = resolvePath2(d);
4201
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4202
+ let built = 0;
4203
+ for (const dd of dirs) {
4204
+ if (!existsSync3(dd))
4205
+ continue;
4206
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4207
+ for (const p of projects) {
4208
+ const pd = join3(dd, p);
4209
+ if (!existsSync3(join3(pd, "SPEC.dog")))
4210
+ continue;
4211
+ const idx = buildIndex(pd, p);
4212
+ writeFileSync2(join3(pd, `${p}.idx`), JSON.stringify(idx));
4213
+ console.log(source_default.green(` ✓ ${p} : ${idx.entries.length} sections indexed (${idx.vocabulary.length} terms)`));
4214
+ built++;
4215
+ }
4216
+ }
4217
+ if (!built)
4218
+ console.log(source_default.yellow("No projects found. Run dotdog init first."));
4219
+ });
4220
+ program2.command("search <query>").description("Semantic search across compiled specs").option("-p, --project <name>").action((query, opts) => {
4221
+ console.log(source_default.bold(`
4222
+ Search: "${query}"
4223
+ `));
4224
+ const dir = resolvePath2(".");
4225
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4226
+ let found = false;
4227
+ for (const dd of dirs) {
4228
+ if (!existsSync3(dd))
4229
+ continue;
4230
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4231
+ for (const p of projects) {
4232
+ if (opts.project && p !== opts.project)
4233
+ continue;
4234
+ const pd = join3(dd, p);
4235
+ const idxFile = join3(pd, `${p}.idx`);
4236
+ if (!existsSync3(idxFile))
4237
+ continue;
4238
+ found = true;
4239
+ const idx = JSON.parse(readFileSync3(idxFile, "utf-8"));
4240
+ const results = searchIndex(idx, query, 8);
4241
+ if (results.length === 0) {
4242
+ console.log(source_default.gray(` ${p}: No matches`));
4243
+ continue;
4244
+ }
4245
+ console.log(source_default.green(` ${p} — ${results.length} results:`));
4246
+ for (const r of results) {
4247
+ const preview = r.entry.content.replace(/\n/g, " ").slice(0, 100);
4248
+ console.log(source_default.gray(` ${Math.round(r.score * 100)}% [${r.entry.file}] ${r.entry.heading}`));
4249
+ console.log(source_default.gray(` ${preview}...`));
4250
+ }
4251
+ }
4252
+ }
4253
+ if (!found)
4254
+ console.log(source_default.yellow("No index found. Run dotdog index first."));
4255
+ });
4256
+ program2.command("predictions [dir]").description("List all predictions with status").option("-p, --project <name>").action((d = ".", opts) => {
4257
+ const dir = resolvePath2(d);
4258
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4259
+ console.log(source_default.bold(`
4260
+ Predictions
4261
+ `));
4262
+ let found = false;
4263
+ for (const dd of dirs) {
4264
+ if (!existsSync3(dd))
4265
+ continue;
4266
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4267
+ for (const p of projects) {
4268
+ if (opts.project && p !== opts.project)
4269
+ continue;
4270
+ const pd = join3(dd, p);
4271
+ if (!existsSync3(join3(pd, "SPEC.dog")))
4272
+ continue;
4273
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
4274
+ for (const f of files) {
4275
+ const content = readFileSync3(join3(pd, f), "utf-8");
4276
+ const ast = parse(content);
4277
+ for (const section of ast.sections) {
4278
+ for (const block of section.blocks) {
4279
+ if (block.kind === "prediction") {
4280
+ found = true;
4281
+ const b = block;
4282
+ const status = b.status || "pending";
4283
+ const icon = status === "correct" ? "✅" : status === "wrong" ? "❌" : status === "partial" ? "⚠️" : "⏳";
4284
+ console.log(` ${icon} ${b.statement || b.name} (${(b.confidence * 100).toFixed(0)}% confidence, ${status})`);
4285
+ if (b.measurement)
4286
+ console.log(source_default.gray(` Measurement: ${b.measurement}`));
4287
+ if (b.timeframe)
4288
+ console.log(source_default.gray(` Timeframe: ${b.timeframe}`));
4289
+ }
4290
+ }
4291
+ }
4292
+ }
4293
+ }
4294
+ }
4295
+ if (!found)
4296
+ console.log(source_default.yellow("No predictions found."));
4297
+ });
4298
+ program2.command("resolve <name>").description("Mark a prediction as correct, wrong, or partial").option("-p, --project <name>").option("--correct", "Prediction was correct").option("--wrong", "Prediction was wrong").option("--partial", "Prediction was partially correct").action((name, opts) => {
4299
+ const dir = resolvePath2(".");
4300
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4301
+ const status = opts.correct ? "correct" : opts.wrong ? "wrong" : opts.partial ? "partial" : null;
4302
+ if (!status) {
4303
+ console.log(source_default.red("Specify --correct, --wrong, or --partial"));
4304
+ return;
4305
+ }
4306
+ for (const dd of dirs) {
4307
+ if (!existsSync3(dd))
4308
+ continue;
4309
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4310
+ for (const p of projects) {
4311
+ if (opts.project && p !== opts.project)
4312
+ continue;
4313
+ const pd = join3(dd, p);
4314
+ if (!existsSync3(join3(pd, "SPEC.dog")))
4315
+ continue;
4316
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
4317
+ for (const f of files) {
4318
+ const fp = join3(pd, f);
4319
+ let content = readFileSync3(fp, "utf-8");
4320
+ const originalContent = readFileSync3(fp, "utf-8");
4321
+ const ast = parse(content);
4322
+ for (const section of ast.sections) {
4323
+ for (const block of section.blocks) {
4324
+ if (block.kind === "prediction") {
4325
+ const b = block;
4326
+ if ((b.statement || b.name || "").toLowerCase().includes(name.toLowerCase())) {
4327
+ const searchFor = `### Prediction: ${b.statement || b.name}`;
4328
+ const headingIdx = content.indexOf(searchFor);
4329
+ if (headingIdx >= 0) {
4330
+ const blockStart = content.indexOf("```yaml", headingIdx);
4331
+ const blockEnd = content.indexOf("```", blockStart + 7);
4332
+ if (blockStart >= 0 && blockEnd >= 0) {
4333
+ const yamlBlock = content.slice(blockStart, blockEnd + 3);
4334
+ let newYaml = yamlBlock;
4335
+ if (yamlBlock.includes("status:")) {
4336
+ newYaml = yamlBlock.replace(/status:\s*\w+/, `status: ${status}`);
4337
+ } else {
4338
+ newYaml = yamlBlock.replace(/\n\s*(trigger|timeframe|confidence|measurement):/, `
4339
+ status: ${status}
4340
+ $&`);
4341
+ }
4342
+ content = content.replace(yamlBlock, newYaml);
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
+ }
4352
+ return;
4353
+ }
4354
+ }
4355
+ }
4356
+ }
4357
+ }
4358
+ }
4359
+ }
4360
+ }
4361
+ }
4362
+ console.log(source_default.yellow(`Prediction "${name}" not found.`));
4363
+ });
4364
+ var kitDir = join3(resolve2(import.meta.dirname || "."), "..", "kits");
4365
+ var builtInKits = existsSync3(kitDir) ? readdirSync3(kitDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : [];
4366
+ var kitCmd = program2.command("kit").description("Manage spec kits");
4367
+ kitCmd.command("list").description("List available kits").action(() => {
4368
+ console.log(source_default.bold(`
4369
+ Available kits
4370
+ `));
4371
+ if (builtInKits.length === 0) {
4372
+ console.log(source_default.gray(" No built-in kits found."));
4373
+ } else {
4374
+ for (const k of builtInKits) {
4375
+ const specFile = join3(kitDir, k, "SPEC.dog");
4376
+ const desc = existsSync3(specFile) ? readFileSync3(specFile, "utf-8").split(`
4377
+ `)[1]?.replace(/^>\s*/, "") || "" : "";
4378
+ console.log(` ${source_default.green(k)} ${source_default.gray(desc)}`);
4379
+ }
4380
+ }
4381
+ console.log(source_default.gray(`
4382
+ Community kits: npm install @scope/kit-<name> then dotdog kit install <name>`));
4383
+ });
4384
+ kitCmd.command("init <kit>").description("Init a project from a kit").option("-p, --project <name>").action((kit, opts) => {
4385
+ const src = join3(kitDir, kit);
4386
+ if (!existsSync3(src)) {
4387
+ console.log(source_default.red(`Kit "${kit}" not found. Available: ${builtInKits.join(", ")}`));
4388
+ return;
4389
+ }
4390
+ const projectName = opts.project || kit;
4391
+ const dir = resolvePath2(".");
4392
+ const dest = join3(dir, "specs", projectName);
4393
+ if (existsSync3(dest)) {
4394
+ console.log(source_default.yellow(`Project "${projectName}" already exists.`));
4395
+ return;
4396
+ }
4397
+ mkdirSync(dest, { recursive: true });
4398
+ const files = readdirSync3(src).filter((f) => f.endsWith(".dog"));
4399
+ for (const f of files) {
4400
+ writeFileSync2(join3(dest, f), readFileSync3(join3(src, f), "utf-8"));
4401
+ console.log(source_default.green(` ✓ ${f}`));
4402
+ }
4403
+ console.log(source_default.gray(`
4404
+ Kit "${kit}" initialized in specs/${projectName}/`));
4405
+ console.log(source_default.gray(` Run: dotdog validate`));
4406
+ });
3762
4407
  program2.parse();
3763
4408
  export {
3764
4409
  parseToJSON,