dotdog 0.3.5 → 0.3.6

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();
@@ -2968,7 +3109,25 @@ function resolvePath(p) {
2968
3109
  return resolved;
2969
3110
  }
2970
3111
  var N = (dag) => dag.n || dag.nodes || [];
2971
- var E = (dag) => dag.e || dag.edges || [];
3112
+ function nodeEdges(n) {
3113
+ return n.es || [];
3114
+ }
3115
+ function E(dag) {
3116
+ if (dag.e)
3117
+ return dag.e;
3118
+ const edges = [];
3119
+ const seen = new Set;
3120
+ for (const node of N(dag)) {
3121
+ for (const e of nodeEdges(node)) {
3122
+ const key = `${node.i || node.id}→${e.t}:${e.v}`;
3123
+ if (!seen.has(key)) {
3124
+ seen.add(key);
3125
+ edges.push({ s: node.i || node.id, t: e.t, v: e.v, d: e.d, c: e.c, r: e.r });
3126
+ }
3127
+ }
3128
+ }
3129
+ return edges;
3130
+ }
2972
3131
  var P = (dag) => dag.p || dag.project || "";
2973
3132
  var ni = (n) => n.i || n.id || "";
2974
3133
  var nt = (n) => n.t || n.type || "";
@@ -2981,15 +3140,15 @@ function serve(dir = ".") {
2981
3140
  const root = resolvePath(dir);
2982
3141
  const dagCache = new Map;
2983
3142
  function loadDags() {
2984
- const dirs = [join(root, "projects"), join(root, "specs"), root];
3143
+ const dirs = [join2(root, "projects"), join2(root, "specs"), root];
2985
3144
  for (const dd of dirs) {
2986
- if (!existsSync(dd))
3145
+ if (!existsSync2(dd))
2987
3146
  continue;
2988
- const projects = readdirSync(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3147
+ const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
2989
3148
  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")));
3149
+ const dagFile = join2(dd, p, `${p}.dag`);
3150
+ if (existsSync2(dagFile)) {
3151
+ dagCache.set(p, JSON.parse(readFileSync2(dagFile, "utf-8")));
2993
3152
  }
2994
3153
  }
2995
3154
  }
@@ -3096,6 +3255,8 @@ function serve(dir = ".") {
3096
3255
  nodes: N(dag).length,
3097
3256
  edges: E(dag).length,
3098
3257
  version: dag.v || dag.version || "",
3258
+ order: dag.o || [],
3259
+ cycles: dag.cy !== undefined ? dag.cy : null,
3099
3260
  savings: tk.sv || tk.savings_pct || 0,
3100
3261
  method: tk.m || tk.method || ""
3101
3262
  };
@@ -3138,8 +3299,8 @@ function serve(dir = ".") {
3138
3299
  // src/cli.ts
3139
3300
  function resolvePath2(p) {
3140
3301
  if (p.startsWith("~"))
3141
- p = join2(homedir2(), p.slice(1));
3142
- const resolved = p.startsWith("/") ? p : join2(process.cwd(), p);
3302
+ p = join3(homedir2(), p.slice(1));
3303
+ const resolved = p.startsWith("/") ? p : join3(process.cwd(), p);
3143
3304
  if (!p.startsWith("/") && !p.startsWith("~")) {
3144
3305
  const rel = resolve2(process.cwd(), p);
3145
3306
  const cwd = process.cwd();
@@ -3185,22 +3346,22 @@ function parseSections2(markdown) {
3185
3346
  return sections;
3186
3347
  }
3187
3348
  var program2 = new Command;
3188
- var pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
3349
+ var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf-8"));
3189
3350
  program2.name("spec").alias("dotdog").description("CLI for structured software specs : validate .dog, compile .dag, query via MCP").version(pkg.version);
3190
3351
  program2.command("validate [dir]").action((d = ".") => {
3191
3352
  const dir = resolvePath2(d);
3192
- const dirs = [join2(dir, "projects"), join2(dir, "specs")];
3353
+ const dirs = [join3(dir, "projects"), join3(dir, "specs")];
3193
3354
  let found = false, hasErrors = false;
3194
3355
  for (const dd of dirs) {
3195
- if (!existsSync2(dd))
3356
+ if (!existsSync3(dd))
3196
3357
  continue;
3197
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3358
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3198
3359
  for (const p of projects) {
3199
3360
  found = true;
3200
- const pd = join2(dd, p);
3201
- if (!existsSync2(join2(pd, "SPEC.dog")))
3361
+ const pd = join3(dd, p);
3362
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3202
3363
  continue;
3203
- const files = existsSync2(pd) ? readdirSync2(pd).filter((f) => f.endsWith(".dog")) : [];
3364
+ const files = existsSync3(pd) ? readdirSync3(pd).filter((f) => f.endsWith(".dog")) : [];
3204
3365
  const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
3205
3366
  const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
3206
3367
  console.log(source_default.bold(`
@@ -3221,7 +3382,7 @@ program2.command("validate [dir]").action((d = ".") => {
3221
3382
  process.exit(1);
3222
3383
  });
3223
3384
  program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data-model.dog").action((p, opts) => {
3224
- const d = join2(process.cwd(), "specs", p);
3385
+ const d = join3(process.cwd(), "specs", p);
3225
3386
  mkdirSync(d, { recursive: true });
3226
3387
  const full = {
3227
3388
  "SPEC.dog": `# Project
@@ -3269,7 +3430,7 @@ program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data
3269
3430
  };
3270
3431
  const tmpl = opts.minimal ? minimal : full;
3271
3432
  for (const [f, c] of Object.entries(tmpl)) {
3272
- writeFileSync(join2(d, f), c);
3433
+ writeFileSync2(join3(d, f), c);
3273
3434
  console.log(source_default.green(` ✓ ${f}`));
3274
3435
  }
3275
3436
  console.log(source_default.bold(`
@@ -3277,23 +3438,23 @@ Project "${p}" initialized. Fill in SPEC.dog then run spec validate.`));
3277
3438
  });
3278
3439
  program2.command("list").action(() => {
3279
3440
  for (const d of ["projects", "specs"]) {
3280
- const dd = join2(process.cwd(), d);
3281
- if (!existsSync2(dd))
3441
+ const dd = join3(process.cwd(), d);
3442
+ if (!existsSync3(dd))
3282
3443
  continue;
3283
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3444
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3284
3445
  if (!projects.length)
3285
3446
  continue;
3286
3447
  console.log(source_default.bold(`
3287
3448
  ${d}/`));
3288
3449
  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;
3450
+ const sp = join3(dd, p);
3451
+ const n = existsSync3(sp) ? readdirSync3(sp).filter((f) => f.endsWith(".dog")).length : 0;
3291
3452
  console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
3292
3453
  }
3293
3454
  }
3294
3455
  });
3295
3456
  program2.command("parse <file>").action((f) => {
3296
- const c = readFileSync2(f, "utf-8");
3457
+ const c = readFileSync3(f, "utf-8");
3297
3458
  const s = parseSections2(c);
3298
3459
  console.log(source_default.bold(`
3299
3460
  ${s.length} sections`));
@@ -3302,26 +3463,26 @@ ${s.length} sections`));
3302
3463
  });
3303
3464
  program2.command("compile [dir]").option("-o, --output <file>").action((d = ".", opts) => {
3304
3465
  const dir = resolvePath2(d);
3305
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3466
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3306
3467
  let found = false;
3307
3468
  for (const dd of dirs) {
3308
- if (!existsSync2(dd))
3469
+ if (!existsSync3(dd))
3309
3470
  continue;
3310
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3471
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3311
3472
  for (const p of projects) {
3312
- const pd = join2(dd, p);
3313
- if (!existsSync2(join2(pd, "SPEC.dog")))
3473
+ const pd = join3(dd, p);
3474
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3314
3475
  continue;
3315
- if (!existsSync2(pd))
3476
+ if (!existsSync3(pd))
3316
3477
  continue;
3317
- const files = readdirSync2(pd).filter((f) => f.endsWith(".dog")).sort();
3478
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog")).sort();
3318
3479
  if (!files.length)
3319
3480
  continue;
3320
3481
  found = true;
3321
3482
  const sources = {};
3322
3483
  let sourceBytes = 0, contentBytes = 0;
3323
3484
  for (const f of files) {
3324
- const content = readFileSync2(join2(pd, f), "utf-8");
3485
+ const content = readFileSync3(join3(pd, f), "utf-8");
3325
3486
  sources[f] = content;
3326
3487
  const bytes = Buffer.byteLength(content, "utf-8");
3327
3488
  sourceBytes += bytes;
@@ -3394,16 +3555,53 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3394
3555
  }
3395
3556
  }
3396
3557
  }
3397
- const dag = { v: "1.4", p, c: `dotdog@${pkg.version}`, n: nodes, e: edges };
3558
+ const nodeIds = new Map;
3559
+ nodes.forEach((n, i) => nodeIds.set(n.i, i));
3560
+ const inDegree = new Array(nodes.length).fill(0);
3561
+ const adj = new Array(nodes.length).fill(0).map(() => []);
3562
+ for (const e of edges) {
3563
+ const si = nodeIds.get(e.s), ti = nodeIds.get(e.t);
3564
+ if (si !== undefined && ti !== undefined) {
3565
+ adj[si].push(ti);
3566
+ inDegree[ti]++;
3567
+ }
3568
+ }
3569
+ const queue = [];
3570
+ for (let j = 0;j < nodes.length; j++)
3571
+ if (inDegree[j] === 0)
3572
+ queue.push(j);
3573
+ const order = [];
3574
+ while (queue.length > 0) {
3575
+ const u = queue.shift();
3576
+ order.push(nodes[u].i);
3577
+ for (const v of adj[u]) {
3578
+ inDegree[v]--;
3579
+ if (inDegree[v] === 0)
3580
+ queue.push(v);
3581
+ }
3582
+ }
3583
+ const cycles = order.length !== nodes.length;
3584
+ for (let j = 0;j < nodes.length; j++) {
3585
+ const id = nodes[j].i;
3586
+ nodes[j].es = edges.filter((e) => e.s === id || e.t === id).map((e) => ({
3587
+ t: e.s === id ? e.t : e.s,
3588
+ v: e.v || "",
3589
+ d: e.d || "",
3590
+ c: e.c,
3591
+ r: e.r,
3592
+ dir: e.s === id ? "out" : "in"
3593
+ }));
3594
+ }
3595
+ const dag = { v: "1.5", p, c: `dotdog@${pkg.version}`, n: nodes, o: order, cy: cycles };
3398
3596
  const dagJson = JSON.stringify(dag);
3399
3597
  const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
3400
3598
  const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
3401
3599
  const contentSavingsPct = contentTokens > 0 ? Math.round((1 - dagTokens / contentTokens) * 1000) / 10 : 0;
3402
3600
  const savingsTokens = sourceTokens - dagTokens;
3403
- const outPath = opts.output || join2(pd, `${p}.dag`);
3601
+ const outPath = opts.output || join3(pd, `${p}.dag`);
3404
3602
  const tokens = { m: "chars/4", st: sourceTokens, ct: contentTokens, dt: dagTokens, sv: allSavingsPct, cs: contentSavingsPct, saved: savingsTokens };
3405
3603
  const report = { ...dag, tk: tokens };
3406
- writeFileSync(outPath, JSON.stringify(report, null, 2));
3604
+ writeFileSync2(outPath, JSON.stringify(report, null, 2));
3407
3605
  console.log(source_default.green(` ✓ ${outPath}`));
3408
3606
  console.log(source_default.gray(` ${nodes.length} nodes, ${edges.length} edges, ${files.length} files`));
3409
3607
  console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${allSavingsPct}% savings, ${contentSavingsPct}% content-only, ${savingsTokens} saved)`));
@@ -3414,29 +3612,29 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3414
3612
  });
3415
3613
  program2.command("tokens [dir]").action((d = ".") => {
3416
3614
  const dir = resolvePath2(d);
3417
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3615
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3418
3616
  let found = false;
3419
3617
  for (const dd of dirs) {
3420
- if (!existsSync2(dd))
3618
+ if (!existsSync3(dd))
3421
3619
  continue;
3422
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3620
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3423
3621
  for (const p of projects) {
3424
- const pd = join2(dd, p);
3425
- if (!existsSync2(join2(pd, "SPEC.dog")))
3622
+ const pd = join3(dd, p);
3623
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3426
3624
  continue;
3427
- const dagFile = join2(pd, `${p}.dag`);
3428
- if (!existsSync2(dagFile))
3625
+ const dagFile = join3(pd, `${p}.dag`);
3626
+ if (!existsSync3(dagFile))
3429
3627
  continue;
3430
3628
  found = true;
3431
- const dogFiles = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
3629
+ const dogFiles = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
3432
3630
  let sourceBytes = 0, contentBytes = 0;
3433
3631
  for (const f of dogFiles) {
3434
- const bytes = Buffer.byteLength(readFileSync2(join2(pd, f), "utf-8"), "utf-8");
3632
+ const bytes = Buffer.byteLength(readFileSync3(join3(pd, f), "utf-8"), "utf-8");
3435
3633
  sourceBytes += bytes;
3436
3634
  if (bytes >= 100)
3437
3635
  contentBytes += bytes;
3438
3636
  }
3439
- const dagBytes = Buffer.byteLength(readFileSync2(dagFile, "utf-8"), "utf-8");
3637
+ const dagBytes = Buffer.byteLength(readFileSync3(dagFile, "utf-8"), "utf-8");
3440
3638
  const savings = sourceBytes > 0 ? Math.round((1 - dagBytes / sourceBytes) * 1000) / 10 : 0;
3441
3639
  console.log(source_default.bold(`
3442
3640
  ${p}`));
@@ -3454,27 +3652,57 @@ program2.command("tokens [dir]").action((d = ".") => {
3454
3652
  });
3455
3653
  program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts) => {
3456
3654
  const dir = resolvePath2(d);
3457
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3655
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3458
3656
  for (const dd of dirs) {
3459
- if (!existsSync2(dd))
3657
+ if (!existsSync3(dd))
3460
3658
  continue;
3461
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3659
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3462
3660
  for (const p of projects) {
3463
- const dagFile = join2(dd, p, `${p}.dag`);
3464
- if (!existsSync2(dagFile))
3661
+ const dagFile = join3(dd, p, `${p}.dag`);
3662
+ if (!existsSync3(dagFile))
3465
3663
  continue;
3466
- const dag = JSON.parse(readFileSync2(dagFile, "utf-8"));
3664
+ const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
3665
+ const nodes = dag.n || dag.nodes || [];
3467
3666
  let out = "```mermaid\ngraph LR\n";
3468
- for (const n of dag.nodes || [])
3469
- out += ` ${n.id.replace(/\s+/g, "_")}[${n.id}]
3667
+ for (const n of nodes) {
3668
+ const raw = n.i || n.id || "";
3669
+ const id = raw.replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3670
+ if (raw)
3671
+ out += ` ${id}[${raw}]
3672
+ `;
3673
+ }
3674
+ const seen = new Set;
3675
+ for (const n of nodes) {
3676
+ for (const e of n.es || []) {
3677
+ if (e.dir === "in")
3678
+ continue;
3679
+ const src = (n.i || n.id || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3680
+ const tgt = (e.t || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3681
+ const verb = e.v || "";
3682
+ const key = `${src}→${tgt}:${verb}`;
3683
+ if (!seen.has(key) && src && tgt) {
3684
+ seen.add(key);
3685
+ out += ` ${src} -->|${verb}| ${tgt}
3470
3686
  `;
3471
- for (const e of dag.edges || [])
3472
- out += ` ${e.source.replace(/\s+/g, "_")} -->|${e.verb || ""}| ${e.target.replace(/\s+/g, "_")}
3687
+ }
3688
+ }
3689
+ }
3690
+ const legacyEdges = dag.e || dag.edges || [];
3691
+ for (const e of legacyEdges) {
3692
+ const src = (e.s || e.source || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3693
+ const tgt = (e.t || e.target || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3694
+ const verb = e.v || e.verb || "";
3695
+ const key = `${src}→${tgt}:${verb}`;
3696
+ if (!seen.has(key) && src && tgt) {
3697
+ seen.add(key);
3698
+ out += ` ${src} -->|${verb}| ${tgt}
3473
3699
  `;
3700
+ }
3701
+ }
3474
3702
  out += "```\n";
3475
3703
  if (opts.save) {
3476
- const outFile = join2(dd, p, "..", `${p}.md`);
3477
- writeFileSync(outFile, `# ${p} : Spec Graph
3704
+ const outFile = join3(dd, p, `${p}.md`);
3705
+ writeFileSync2(outFile, `# ${p} : Spec Graph
3478
3706
 
3479
3707
  ${out}`);
3480
3708
  console.log(source_default.green(` ✓ ${outFile}`));
@@ -3486,24 +3714,24 @@ ${out}`);
3486
3714
  program2.command("serve [dir]").description("MCP server : expose .dag graph to AI agents over stdio").action((d = ".") => serve(resolvePath2(d)));
3487
3715
  program2.command("analyze [dir]").description("Analyze a spec project : score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
3488
3716
  const dir = resolvePath2(d);
3489
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3717
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3490
3718
  console.log(source_default.bold(`
3491
3719
  Spec Analysis
3492
3720
  `));
3493
3721
  let found = false, hasGaps = false;
3494
3722
  for (const dd of dirs) {
3495
- if (!existsSync2(dd))
3723
+ if (!existsSync3(dd))
3496
3724
  continue;
3497
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3725
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3498
3726
  for (const p of projects) {
3499
3727
  if (opts.project && p !== opts.project)
3500
3728
  continue;
3501
- const pd = join2(dd, p);
3502
- if (!existsSync2(join2(pd, "SPEC.dog")))
3729
+ const pd = join3(dd, p);
3730
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3503
3731
  continue;
3504
- if (!existsSync2(pd))
3732
+ if (!existsSync3(pd))
3505
3733
  continue;
3506
- const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
3734
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
3507
3735
  if (!files.length)
3508
3736
  continue;
3509
3737
  found = true;
@@ -3514,7 +3742,7 @@ Spec Analysis
3514
3742
  const allRelationships = [];
3515
3743
  const analyses = [];
3516
3744
  for (const f of files) {
3517
- const content = readFileSync2(join2(pd, f), "utf-8");
3745
+ const content = readFileSync3(join3(pd, f), "utf-8");
3518
3746
  const ast = parse(content);
3519
3747
  const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
3520
3748
  const rels = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "relationship"));
@@ -3556,6 +3784,39 @@ Spec Analysis
3556
3784
  if (r.target && !entityNames.has(r.target))
3557
3785
  gaps.push(`\uD83D\uDFE1 Relationship: unknown target "${r.target}"`);
3558
3786
  }
3787
+ const contradictions = [];
3788
+ const relMap = new Map;
3789
+ for (const r of allRelationships) {
3790
+ const key = `${r.source}→${r.target}`;
3791
+ if (!relMap.has(key))
3792
+ relMap.set(key, []);
3793
+ relMap.get(key).push(r);
3794
+ }
3795
+ for (const [key, rels] of relMap) {
3796
+ if (rels.length > 1) {
3797
+ const verbs = [...new Set(rels.map((r) => r.verb))];
3798
+ if (verbs.length > 1)
3799
+ contradictions.push(`\uD83D\uDD34 Contradiction: "${key}" has conflicting verbs: ${verbs.join(", ")}`);
3800
+ else
3801
+ contradictions.push(`\uD83D\uDFE1 Duplicate: "${key}" appears ${rels.length} times with same verb "${verbs[0]}"`);
3802
+ }
3803
+ }
3804
+ for (const r of allRelationships) {
3805
+ const reverse = allRelationships.find((r2) => r2.source === r.target && r2.target === r.source);
3806
+ if (reverse && r.verb !== reverse.verb && r.source < r.target) {
3807
+ contradictions.push(`\uD83D\uDFE1 Bidirectional: "${r.source}→${r.target}" verb "${r.verb}" vs "${reverse.source}→${reverse.target}" verb "${reverse.verb}"`);
3808
+ }
3809
+ }
3810
+ for (const c of contradictions) {
3811
+ if (c.startsWith("\uD83D\uDD34"))
3812
+ hasGaps = true;
3813
+ }
3814
+ if (contradictions.length > 0) {
3815
+ console.log(source_default.bold(`
3816
+ Contradictions (${contradictions.length})`));
3817
+ for (const c of contradictions)
3818
+ console.log(` ${c}`);
3819
+ }
3559
3820
  if (gaps.length > 0) {
3560
3821
  console.log(source_default.bold(`
3561
3822
  Gaps (${gaps.length})`));
@@ -3575,19 +3836,19 @@ Spec Analysis
3575
3836
  });
3576
3837
  program2.command("generate [dir]").description("Generate missing spec files from SPEC.dog").option("-p, --project <name>").action((d = ".", opts) => {
3577
3838
  const dir = resolvePath2(d);
3578
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3839
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3579
3840
  let specContent = "", specDir = "";
3580
3841
  for (const dd of dirs) {
3581
- if (!existsSync2(dd))
3842
+ if (!existsSync3(dd))
3582
3843
  continue;
3583
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3844
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3584
3845
  for (const p of projects) {
3585
3846
  if (opts.project && p !== opts.project)
3586
3847
  continue;
3587
- const pd = join2(dd, p);
3588
- const sp = join2(pd, "SPEC.dog");
3589
- if (existsSync2(sp)) {
3590
- specContent = readFileSync2(sp, "utf-8");
3848
+ const pd = join3(dd, p);
3849
+ const sp = join3(pd, "SPEC.dog");
3850
+ if (existsSync3(sp)) {
3851
+ specContent = readFileSync3(sp, "utf-8");
3591
3852
  specDir = pd;
3592
3853
  break;
3593
3854
  }
@@ -3618,7 +3879,7 @@ Spec Generator
3618
3879
  uiStrings.push({ screen: section.heading, element: "label", text: m });
3619
3880
  }
3620
3881
  }
3621
- if (!existsSync2(join2(specDir, "data-model.dog")) && entities.length > 0) {
3882
+ if (!existsSync3(join3(specDir, "data-model.dog")) && entities.length > 0) {
3622
3883
  let dm = `# Data Model
3623
3884
 
3624
3885
  ## Core Entities
@@ -3651,10 +3912,10 @@ ${e.description || "No description."}
3651
3912
  `;
3652
3913
  dm += "```\n\n";
3653
3914
  }
3654
- writeFileSync(join2(specDir, "data-model.dog"), dm);
3915
+ writeFileSync2(join3(specDir, "data-model.dog"), dm);
3655
3916
  console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
3656
3917
  }
3657
- if (!existsSync2(join2(specDir, "COPY.dog")) && uiStrings.length > 0) {
3918
+ if (!existsSync3(join3(specDir, "COPY.dog")) && uiStrings.length > 0) {
3658
3919
  let copy = `# App Copy
3659
3920
 
3660
3921
  | Screen | Element | Copy |
@@ -3663,10 +3924,10 @@ ${e.description || "No description."}
3663
3924
  for (const s of uiStrings)
3664
3925
  copy += `| ${s.screen} | ${s.element} | ${s.text} |
3665
3926
  `;
3666
- writeFileSync(join2(specDir, "COPY.dog"), copy);
3927
+ writeFileSync2(join3(specDir, "COPY.dog"), copy);
3667
3928
  console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
3668
3929
  }
3669
- if (!existsSync2(join2(specDir, "INDEX.dog"))) {
3930
+ if (!existsSync3(join3(specDir, "INDEX.dog"))) {
3670
3931
  let idx = `# INDEX
3671
3932
 
3672
3933
  | You are... | Start here | Then... |
@@ -3678,41 +3939,139 @@ ${e.description || "No description."}
3678
3939
  `;
3679
3940
  idx += `| Designer | SPEC.dog | COPY.dog |
3680
3941
  `;
3681
- writeFileSync(join2(specDir, "INDEX.dog"), idx);
3942
+ writeFileSync2(join3(specDir, "INDEX.dog"), idx);
3682
3943
  console.log(source_default.green(" ✓ INDEX.dog"));
3683
3944
  }
3684
3945
  console.log(source_default.bold(`
3685
3946
  Run dotdog validate to verify.
3686
3947
  `));
3687
3948
  });
3688
- program2.command("simulate <scenario>").description("Run a simulation scenario (phase 1 stub)").option("-p, --project <name>", "Project name", "default").action((scenario, opts) => {
3949
+ program2.command("simulate <scenario>").description("Walk through a scenario, check pre/postconditions").option("-p, --project <name>").action((scenario, opts) => {
3950
+ const dir = resolvePath2(".");
3951
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3952
+ let projectDir = "";
3953
+ for (const dd of dirs) {
3954
+ if (!existsSync3(dd))
3955
+ continue;
3956
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3957
+ for (const p of projects) {
3958
+ if (opts.project && p !== opts.project)
3959
+ continue;
3960
+ const pd = join3(dd, p);
3961
+ if (existsSync3(join3(pd, "SPEC.dog"))) {
3962
+ projectDir = pd;
3963
+ break;
3964
+ }
3965
+ }
3966
+ if (projectDir)
3967
+ break;
3968
+ }
3969
+ if (!projectDir) {
3970
+ console.log(source_default.red("Project not found."));
3971
+ return;
3972
+ }
3973
+ const dagFile = join3(projectDir, `${opts.project || projectDir.split("/").pop()}.dag`);
3974
+ let entities = [], relationships = [];
3975
+ if (existsSync3(dagFile)) {
3976
+ const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
3977
+ entities = (dag.n || dag.nodes || []).map((n) => (n.i || n.id || "").toLowerCase());
3978
+ if (dag.e || dag.edges) {
3979
+ relationships = dag.e || dag.edges || [];
3980
+ } else {
3981
+ const seen = new Set;
3982
+ for (const n of dag.n || dag.nodes || []) {
3983
+ for (const e of n.es || []) {
3984
+ if (e.dir === "in")
3985
+ continue;
3986
+ const key = `${n.i || n.id}→${e.t}:${e.v}`;
3987
+ if (!seen.has(key)) {
3988
+ seen.add(key);
3989
+ relationships.push({ s: n.i || n.id, t: e.t, v: e.v, d: e.d, r: e.r });
3990
+ }
3991
+ }
3992
+ }
3993
+ }
3994
+ }
3995
+ const specFile = join3(projectDir, "SPEC.dog");
3996
+ const specContent = existsSync3(specFile) ? readFileSync3(specFile, "utf-8") : "";
3689
3997
  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."));
3998
+ Simulation: ${scenario}`));
3999
+ console.log(source_default.gray(`Project: ${projectDir.split("/").pop()} | Entities: ${entities.length} | Relationships: ${relationships.length}`));
4000
+ const steps = [];
4001
+ const stepMatches = specContent.match(/\[(\d+)\/\d+\]\s*(.+)/g);
4002
+ if (stepMatches) {
4003
+ for (const m of stepMatches) {
4004
+ const step = m.replace(/\[\d+\/\d+\]\s*/, "");
4005
+ if (step)
4006
+ steps.push(step);
4007
+ }
4008
+ }
4009
+ if (steps.length === 0) {
4010
+ console.log(source_default.yellow(`
4011
+ No scenario steps found in SPEC.dog.`));
4012
+ console.log(source_default.gray(" Add steps like: [1/3] User taps button"));
4013
+ return;
4014
+ }
4015
+ let passed = 0, failed = 0;
4016
+ for (let i = 0;i < steps.length; i++) {
4017
+ const step = steps[i];
4018
+ console.log(source_default.cyan(`
4019
+ [${i + 1}/${steps.length}] ${step}`));
4020
+ let foundRef = false;
4021
+ for (const e of entities) {
4022
+ if (step.toLowerCase().includes(e)) {
4023
+ console.log(source_default.green(` ✓ References entity: ${e}`));
4024
+ foundRef = true;
4025
+ }
4026
+ }
4027
+ for (const r of relationships) {
4028
+ const src = (r.s || r.source || "").toLowerCase();
4029
+ const tgt = (r.t || r.target || "").toLowerCase();
4030
+ const verb = (r.v || r.verb || "").toLowerCase();
4031
+ if (step.toLowerCase().includes(src) && step.toLowerCase().includes(tgt)) {
4032
+ console.log(source_default.green(` ✓ References relationship: ${src} → ${tgt}`));
4033
+ foundRef = true;
4034
+ }
4035
+ if (verb && step.toLowerCase().includes(verb)) {
4036
+ console.log(source_default.green(` ✓ References verb: ${verb}`));
4037
+ foundRef = true;
4038
+ }
4039
+ }
4040
+ if (!foundRef) {
4041
+ console.log(source_default.yellow(` ⚠ No entity or relationship references found`));
4042
+ failed++;
4043
+ } else {
4044
+ passed++;
4045
+ }
4046
+ }
4047
+ console.log(source_default.bold(`
4048
+ RESULT: ${failed === 0 ? source_default.green("Success") : source_default.yellow("Partial")} (${passed}/${steps.length} steps passed)`));
4049
+ if (failed > 0)
4050
+ console.log(source_default.red(` ${failed} steps have no spec references — entities or relationships may be missing.`));
4051
+ if (failed === 0)
4052
+ console.log(source_default.green(" All steps reference known entities and relationships."));
3694
4053
  });
3695
4054
  program2.command("staleness [dir]").action((d = ".") => {
3696
4055
  const dir = resolvePath2(d);
3697
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
4056
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3698
4057
  console.log(source_default.bold(`Staleness Audit
3699
4058
  `));
3700
4059
  for (const dd of dirs) {
3701
- if (!existsSync2(dd))
4060
+ if (!existsSync3(dd))
3702
4061
  continue;
3703
- const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4062
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3704
4063
  for (const p of projects) {
3705
- const pd = join2(dd, p);
3706
- if (!existsSync2(join2(pd, "SPEC.dog")))
4064
+ const pd = join3(dd, p);
4065
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3707
4066
  continue;
3708
- if (!existsSync2(pd))
4067
+ if (!existsSync3(pd))
3709
4068
  continue;
3710
- const planFile = join2(pd, "plan.dog");
3711
- if (!existsSync2(planFile)) {
4069
+ const planFile = join3(pd, "plan.dog");
4070
+ if (!existsSync3(planFile)) {
3712
4071
  console.log(source_default.yellow(` ${p}: No plan.dog`));
3713
4072
  continue;
3714
4073
  }
3715
- const plan = readFileSync2(planFile, "utf-8");
4074
+ const plan = readFileSync3(planFile, "utf-8");
3716
4075
  const tasks = [...plan.matchAll(/^\s*- \[([ x])\]\s+(.+)/gm)];
3717
4076
  let issues = 0;
3718
4077
  for (const m of tasks) {
@@ -3723,14 +4082,15 @@ program2.command("staleness [dir]").action((d = ".") => {
3723
4082
  const phase = phaseMatch ? parseInt(phaseMatch[1]) : 99;
3724
4083
  if (phase > 3)
3725
4084
  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) {
4085
+ if ((text.includes("npm publish") || text.includes("npm install")) && !done) {
4086
+ const pkgPath = join3(resolvePath2("."), "packages/dotdog/package.json");
4087
+ if (existsSync3(pkgPath)) {
4088
+ const pkg2 = JSON.parse(readFileSync3(pkgPath, "utf-8"));
4089
+ if (pkg2.version) {
3730
4090
  console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
3731
4091
  issues++;
3732
4092
  }
3733
- } catch {}
4093
+ }
3734
4094
  }
3735
4095
  if (text.includes("compile")) {
3736
4096
  if (!done) {
@@ -3739,7 +4099,7 @@ program2.command("staleness [dir]").action((d = ".") => {
3739
4099
  }
3740
4100
  }
3741
4101
  if (text.includes("generate") && !done) {
3742
- if (existsSync2(join2(dir, "packages/dotdog/src/cli.ts")) || existsSync2(join2(dir, "packages/spec-cli/src/generate.ts"))) {
4102
+ if (existsSync3(join3(dir, "packages/dotdog/src/cli.ts")) || existsSync3(join3(dir, "packages/spec-cli/src/generate.ts"))) {
3743
4103
  console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
3744
4104
  issues++;
3745
4105
  }
@@ -3759,6 +4119,206 @@ program2.command("woof").action(() => {
3759
4119
  console.log(" / (_____/");
3760
4120
  console.log("/_____/ U");
3761
4121
  });
4122
+ program2.command("index [dir]").description("Build search index for semantic queries").action((d = ".") => {
4123
+ const dir = resolvePath2(d);
4124
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4125
+ let built = 0;
4126
+ for (const dd of dirs) {
4127
+ if (!existsSync3(dd))
4128
+ continue;
4129
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4130
+ for (const p of projects) {
4131
+ const pd = join3(dd, p);
4132
+ if (!existsSync3(join3(pd, "SPEC.dog")))
4133
+ continue;
4134
+ const idx = buildIndex(pd, p);
4135
+ writeFileSync2(join3(pd, `${p}.idx`), JSON.stringify(idx));
4136
+ console.log(source_default.green(` ✓ ${p} : ${idx.entries.length} sections indexed (${idx.vocabulary.length} terms)`));
4137
+ built++;
4138
+ }
4139
+ }
4140
+ if (!built)
4141
+ console.log(source_default.yellow("No projects found. Run dotdog init first."));
4142
+ });
4143
+ program2.command("search <query>").description("Semantic search across compiled specs").option("-p, --project <name>").action((query, opts) => {
4144
+ console.log(source_default.bold(`
4145
+ Search: "${query}"
4146
+ `));
4147
+ const dir = resolvePath2(".");
4148
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4149
+ let found = false;
4150
+ for (const dd of dirs) {
4151
+ if (!existsSync3(dd))
4152
+ continue;
4153
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4154
+ for (const p of projects) {
4155
+ if (opts.project && p !== opts.project)
4156
+ continue;
4157
+ const pd = join3(dd, p);
4158
+ const idxFile = join3(pd, `${p}.idx`);
4159
+ if (!existsSync3(idxFile))
4160
+ continue;
4161
+ found = true;
4162
+ const idx = JSON.parse(readFileSync3(idxFile, "utf-8"));
4163
+ const results = searchIndex(idx, query, 8);
4164
+ if (results.length === 0) {
4165
+ console.log(source_default.gray(` ${p}: No matches`));
4166
+ continue;
4167
+ }
4168
+ console.log(source_default.green(` ${p} — ${results.length} results:`));
4169
+ for (const r of results) {
4170
+ const preview = r.entry.content.replace(/\n/g, " ").slice(0, 100);
4171
+ console.log(source_default.gray(` ${Math.round(r.score * 100)}% [${r.entry.file}] ${r.entry.heading}`));
4172
+ console.log(source_default.gray(` ${preview}...`));
4173
+ }
4174
+ }
4175
+ }
4176
+ if (!found)
4177
+ console.log(source_default.yellow("No index found. Run dotdog index first."));
4178
+ });
4179
+ program2.command("predictions [dir]").description("List all predictions with status").option("-p, --project <name>").action((d = ".", opts) => {
4180
+ const dir = resolvePath2(d);
4181
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4182
+ console.log(source_default.bold(`
4183
+ Predictions
4184
+ `));
4185
+ let found = false;
4186
+ for (const dd of dirs) {
4187
+ if (!existsSync3(dd))
4188
+ continue;
4189
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4190
+ for (const p of projects) {
4191
+ if (opts.project && p !== opts.project)
4192
+ continue;
4193
+ const pd = join3(dd, p);
4194
+ if (!existsSync3(join3(pd, "SPEC.dog")))
4195
+ continue;
4196
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
4197
+ for (const f of files) {
4198
+ const content = readFileSync3(join3(pd, f), "utf-8");
4199
+ const ast = parse(content);
4200
+ for (const section of ast.sections) {
4201
+ for (const block of section.blocks) {
4202
+ if (block.kind === "prediction") {
4203
+ found = true;
4204
+ const b = block;
4205
+ const status = b.status || "pending";
4206
+ const icon = status === "correct" ? "✅" : status === "wrong" ? "❌" : status === "partial" ? "⚠️" : "⏳";
4207
+ console.log(` ${icon} ${b.statement || b.name} (${(b.confidence * 100).toFixed(0)}% confidence, ${status})`);
4208
+ if (b.measurement)
4209
+ console.log(source_default.gray(` Measurement: ${b.measurement}`));
4210
+ if (b.timeframe)
4211
+ console.log(source_default.gray(` Timeframe: ${b.timeframe}`));
4212
+ }
4213
+ }
4214
+ }
4215
+ }
4216
+ }
4217
+ }
4218
+ if (!found)
4219
+ console.log(source_default.yellow("No predictions found."));
4220
+ });
4221
+ 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) => {
4222
+ const dir = resolvePath2(".");
4223
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4224
+ const status = opts.correct ? "correct" : opts.wrong ? "wrong" : opts.partial ? "partial" : null;
4225
+ if (!status) {
4226
+ console.log(source_default.red("Specify --correct, --wrong, or --partial"));
4227
+ return;
4228
+ }
4229
+ for (const dd of dirs) {
4230
+ if (!existsSync3(dd))
4231
+ continue;
4232
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4233
+ for (const p of projects) {
4234
+ if (opts.project && p !== opts.project)
4235
+ continue;
4236
+ const pd = join3(dd, p);
4237
+ if (!existsSync3(join3(pd, "SPEC.dog")))
4238
+ continue;
4239
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
4240
+ for (const f of files) {
4241
+ const fp = join3(pd, f);
4242
+ let content = readFileSync3(fp, "utf-8");
4243
+ const ast = parse(content);
4244
+ for (const section of ast.sections) {
4245
+ for (const block of section.blocks) {
4246
+ if (block.kind === "prediction") {
4247
+ const b = block;
4248
+ if ((b.statement || b.name || "").toLowerCase().includes(name.toLowerCase())) {
4249
+ const searchFor = `### Prediction: ${b.statement || b.name}`;
4250
+ const headingIdx = content.indexOf(searchFor);
4251
+ if (headingIdx >= 0) {
4252
+ const blockStart = content.indexOf("```yaml", headingIdx);
4253
+ const blockEnd = content.indexOf("```", blockStart + 7);
4254
+ if (blockStart >= 0 && blockEnd >= 0) {
4255
+ const yamlBlock = content.slice(blockStart, blockEnd + 3);
4256
+ let newYaml = yamlBlock;
4257
+ if (yamlBlock.includes("status:")) {
4258
+ newYaml = yamlBlock.replace(/status:\s*\w+/, `status: ${status}`);
4259
+ } else {
4260
+ newYaml = yamlBlock.replace(/\n\s*(trigger|timeframe|confidence|measurement):/, `
4261
+ status: ${status}
4262
+ $1`);
4263
+ }
4264
+ content = content.replace(yamlBlock, newYaml);
4265
+ writeFileSync2(fp, content, "utf-8");
4266
+ console.log(source_default.green(` ✓ ${b.statement || b.name}: ${status}`));
4267
+ return;
4268
+ }
4269
+ }
4270
+ }
4271
+ }
4272
+ }
4273
+ }
4274
+ }
4275
+ }
4276
+ }
4277
+ console.log(source_default.yellow(`Prediction "${name}" not found.`));
4278
+ });
4279
+ var kitDir = join3(resolve2(import.meta.dirname || "."), "..", "kits");
4280
+ var builtInKits = existsSync3(kitDir) ? readdirSync3(kitDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : [];
4281
+ var kitCmd = program2.command("kit").description("Manage spec kits");
4282
+ kitCmd.command("list").description("List available kits").action(() => {
4283
+ console.log(source_default.bold(`
4284
+ Available kits
4285
+ `));
4286
+ if (builtInKits.length === 0) {
4287
+ console.log(source_default.gray(" No built-in kits found."));
4288
+ } else {
4289
+ for (const k of builtInKits) {
4290
+ const specFile = join3(kitDir, k, "SPEC.dog");
4291
+ const desc = existsSync3(specFile) ? readFileSync3(specFile, "utf-8").split(`
4292
+ `)[1]?.replace(/^>\s*/, "") || "" : "";
4293
+ console.log(` ${source_default.green(k)} ${source_default.gray(desc)}`);
4294
+ }
4295
+ }
4296
+ console.log(source_default.gray(`
4297
+ Community kits: npm install @scope/kit-<name> then dotdog kit install <name>`));
4298
+ });
4299
+ kitCmd.command("init <kit>").description("Init a project from a kit").option("-p, --project <name>").action((kit, opts) => {
4300
+ const src = join3(kitDir, kit);
4301
+ if (!existsSync3(src)) {
4302
+ console.log(source_default.red(`Kit "${kit}" not found. Available: ${builtInKits.join(", ")}`));
4303
+ return;
4304
+ }
4305
+ const projectName = opts.project || kit;
4306
+ const dir = resolvePath2(".");
4307
+ const dest = join3(dir, "specs", projectName);
4308
+ if (existsSync3(dest)) {
4309
+ console.log(source_default.yellow(`Project "${projectName}" already exists.`));
4310
+ return;
4311
+ }
4312
+ mkdirSync(dest, { recursive: true });
4313
+ const files = readdirSync3(src).filter((f) => f.endsWith(".dog"));
4314
+ for (const f of files) {
4315
+ writeFileSync2(join3(dest, f), readFileSync3(join3(src, f), "utf-8"));
4316
+ console.log(source_default.green(` ✓ ${f}`));
4317
+ }
4318
+ console.log(source_default.gray(`
4319
+ Kit "${kit}" initialized in specs/${projectName}/`));
4320
+ console.log(source_default.gray(` Run: dotdog validate`));
4321
+ });
3762
4322
  program2.parse();
3763
4323
  export {
3764
4324
  parseToJSON,