dotdog 0.3.4 → 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,39 +3346,43 @@ 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")];
3193
- let found = false;
3353
+ const dirs = [join3(dir, "projects"), join3(dir, "specs")];
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(`
3207
3368
  ${p} : ${files.length} .dog files, ${100 - Math.round((missing.length * 3 + optional.length) / 20 * 100)}% complete`));
3208
3369
  for (const f of files)
3209
3370
  console.log(source_default.gray(` ${f}`));
3210
- if (missing.length)
3371
+ if (missing.length) {
3211
3372
  console.log(source_default.red(` Missing required: ${missing.join(", ")}`));
3373
+ hasErrors = true;
3374
+ }
3212
3375
  if (optional.length)
3213
3376
  console.log(source_default.yellow(` Missing optional: ${optional.join(", ")}`));
3214
3377
  }
3215
3378
  }
3216
3379
  if (!found)
3217
3380
  console.log(source_default.yellow("No projects found. Run: spec init <project>"));
3381
+ if (hasErrors)
3382
+ process.exit(1);
3218
3383
  });
3219
3384
  program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data-model.dog").action((p, opts) => {
3220
- const d = join2(process.cwd(), "specs", p);
3385
+ const d = join3(process.cwd(), "specs", p);
3221
3386
  mkdirSync(d, { recursive: true });
3222
3387
  const full = {
3223
3388
  "SPEC.dog": `# Project
@@ -3265,7 +3430,7 @@ program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data
3265
3430
  };
3266
3431
  const tmpl = opts.minimal ? minimal : full;
3267
3432
  for (const [f, c] of Object.entries(tmpl)) {
3268
- writeFileSync(join2(d, f), c);
3433
+ writeFileSync2(join3(d, f), c);
3269
3434
  console.log(source_default.green(` ✓ ${f}`));
3270
3435
  }
3271
3436
  console.log(source_default.bold(`
@@ -3273,23 +3438,23 @@ Project "${p}" initialized. Fill in SPEC.dog then run spec validate.`));
3273
3438
  });
3274
3439
  program2.command("list").action(() => {
3275
3440
  for (const d of ["projects", "specs"]) {
3276
- const dd = join2(process.cwd(), d);
3277
- if (!existsSync2(dd))
3441
+ const dd = join3(process.cwd(), d);
3442
+ if (!existsSync3(dd))
3278
3443
  continue;
3279
- 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);
3280
3445
  if (!projects.length)
3281
3446
  continue;
3282
3447
  console.log(source_default.bold(`
3283
3448
  ${d}/`));
3284
3449
  for (const p of projects) {
3285
- const sp = join2(dd, p);
3286
- 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;
3287
3452
  console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
3288
3453
  }
3289
3454
  }
3290
3455
  });
3291
3456
  program2.command("parse <file>").action((f) => {
3292
- const c = readFileSync2(f, "utf-8");
3457
+ const c = readFileSync3(f, "utf-8");
3293
3458
  const s = parseSections2(c);
3294
3459
  console.log(source_default.bold(`
3295
3460
  ${s.length} sections`));
@@ -3298,26 +3463,26 @@ ${s.length} sections`));
3298
3463
  });
3299
3464
  program2.command("compile [dir]").option("-o, --output <file>").action((d = ".", opts) => {
3300
3465
  const dir = resolvePath2(d);
3301
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3466
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3302
3467
  let found = false;
3303
3468
  for (const dd of dirs) {
3304
- if (!existsSync2(dd))
3469
+ if (!existsSync3(dd))
3305
3470
  continue;
3306
- 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);
3307
3472
  for (const p of projects) {
3308
- const pd = join2(dd, p);
3309
- if (!existsSync2(join2(pd, "SPEC.dog")))
3473
+ const pd = join3(dd, p);
3474
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3310
3475
  continue;
3311
- if (!existsSync2(pd))
3476
+ if (!existsSync3(pd))
3312
3477
  continue;
3313
- const files = readdirSync2(pd).filter((f) => f.endsWith(".dog")).sort();
3478
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog")).sort();
3314
3479
  if (!files.length)
3315
3480
  continue;
3316
3481
  found = true;
3317
3482
  const sources = {};
3318
3483
  let sourceBytes = 0, contentBytes = 0;
3319
3484
  for (const f of files) {
3320
- const content = readFileSync2(join2(pd, f), "utf-8");
3485
+ const content = readFileSync3(join3(pd, f), "utf-8");
3321
3486
  sources[f] = content;
3322
3487
  const bytes = Buffer.byteLength(content, "utf-8");
3323
3488
  sourceBytes += bytes;
@@ -3390,16 +3555,53 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3390
3555
  }
3391
3556
  }
3392
3557
  }
3393
- 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 };
3394
3596
  const dagJson = JSON.stringify(dag);
3395
3597
  const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
3396
3598
  const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
3397
3599
  const contentSavingsPct = contentTokens > 0 ? Math.round((1 - dagTokens / contentTokens) * 1000) / 10 : 0;
3398
3600
  const savingsTokens = sourceTokens - dagTokens;
3399
- const outPath = opts.output || join2(pd, `${p}.dag`);
3601
+ const outPath = opts.output || join3(pd, `${p}.dag`);
3400
3602
  const tokens = { m: "chars/4", st: sourceTokens, ct: contentTokens, dt: dagTokens, sv: allSavingsPct, cs: contentSavingsPct, saved: savingsTokens };
3401
3603
  const report = { ...dag, tk: tokens };
3402
- writeFileSync(outPath, JSON.stringify(report, null, 2));
3604
+ writeFileSync2(outPath, JSON.stringify(report, null, 2));
3403
3605
  console.log(source_default.green(` ✓ ${outPath}`));
3404
3606
  console.log(source_default.gray(` ${nodes.length} nodes, ${edges.length} edges, ${files.length} files`));
3405
3607
  console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${allSavingsPct}% savings, ${contentSavingsPct}% content-only, ${savingsTokens} saved)`));
@@ -3410,29 +3612,29 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3410
3612
  });
3411
3613
  program2.command("tokens [dir]").action((d = ".") => {
3412
3614
  const dir = resolvePath2(d);
3413
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3615
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3414
3616
  let found = false;
3415
3617
  for (const dd of dirs) {
3416
- if (!existsSync2(dd))
3618
+ if (!existsSync3(dd))
3417
3619
  continue;
3418
- 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);
3419
3621
  for (const p of projects) {
3420
- const pd = join2(dd, p);
3421
- if (!existsSync2(join2(pd, "SPEC.dog")))
3622
+ const pd = join3(dd, p);
3623
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3422
3624
  continue;
3423
- const dagFile = join2(pd, `${p}.dag`);
3424
- if (!existsSync2(dagFile))
3625
+ const dagFile = join3(pd, `${p}.dag`);
3626
+ if (!existsSync3(dagFile))
3425
3627
  continue;
3426
3628
  found = true;
3427
- const dogFiles = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
3629
+ const dogFiles = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
3428
3630
  let sourceBytes = 0, contentBytes = 0;
3429
3631
  for (const f of dogFiles) {
3430
- 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");
3431
3633
  sourceBytes += bytes;
3432
3634
  if (bytes >= 100)
3433
3635
  contentBytes += bytes;
3434
3636
  }
3435
- const dagBytes = Buffer.byteLength(readFileSync2(dagFile, "utf-8"), "utf-8");
3637
+ const dagBytes = Buffer.byteLength(readFileSync3(dagFile, "utf-8"), "utf-8");
3436
3638
  const savings = sourceBytes > 0 ? Math.round((1 - dagBytes / sourceBytes) * 1000) / 10 : 0;
3437
3639
  console.log(source_default.bold(`
3438
3640
  ${p}`));
@@ -3450,27 +3652,57 @@ program2.command("tokens [dir]").action((d = ".") => {
3450
3652
  });
3451
3653
  program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts) => {
3452
3654
  const dir = resolvePath2(d);
3453
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3655
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3454
3656
  for (const dd of dirs) {
3455
- if (!existsSync2(dd))
3657
+ if (!existsSync3(dd))
3456
3658
  continue;
3457
- 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);
3458
3660
  for (const p of projects) {
3459
- const dagFile = join2(dd, p, `${p}.dag`);
3460
- if (!existsSync2(dagFile))
3661
+ const dagFile = join3(dd, p, `${p}.dag`);
3662
+ if (!existsSync3(dagFile))
3461
3663
  continue;
3462
- const dag = JSON.parse(readFileSync2(dagFile, "utf-8"));
3664
+ const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
3665
+ const nodes = dag.n || dag.nodes || [];
3463
3666
  let out = "```mermaid\ngraph LR\n";
3464
- for (const n of dag.nodes || [])
3465
- 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}
3466
3686
  `;
3467
- for (const e of dag.edges || [])
3468
- 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}
3469
3699
  `;
3700
+ }
3701
+ }
3470
3702
  out += "```\n";
3471
3703
  if (opts.save) {
3472
- const outFile = join2(dd, p, "..", `${p}.md`);
3473
- writeFileSync(outFile, `# ${p} : Spec Graph
3704
+ const outFile = join3(dd, p, `${p}.md`);
3705
+ writeFileSync2(outFile, `# ${p} : Spec Graph
3474
3706
 
3475
3707
  ${out}`);
3476
3708
  console.log(source_default.green(` ✓ ${outFile}`));
@@ -3482,24 +3714,24 @@ ${out}`);
3482
3714
  program2.command("serve [dir]").description("MCP server : expose .dag graph to AI agents over stdio").action((d = ".") => serve(resolvePath2(d)));
3483
3715
  program2.command("analyze [dir]").description("Analyze a spec project : score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
3484
3716
  const dir = resolvePath2(d);
3485
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3717
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3486
3718
  console.log(source_default.bold(`
3487
3719
  Spec Analysis
3488
3720
  `));
3489
- let found = false;
3721
+ let found = false, hasGaps = false;
3490
3722
  for (const dd of dirs) {
3491
- if (!existsSync2(dd))
3723
+ if (!existsSync3(dd))
3492
3724
  continue;
3493
- 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);
3494
3726
  for (const p of projects) {
3495
3727
  if (opts.project && p !== opts.project)
3496
3728
  continue;
3497
- const pd = join2(dd, p);
3498
- if (!existsSync2(join2(pd, "SPEC.dog")))
3729
+ const pd = join3(dd, p);
3730
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3499
3731
  continue;
3500
- if (!existsSync2(pd))
3732
+ if (!existsSync3(pd))
3501
3733
  continue;
3502
- const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
3734
+ const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
3503
3735
  if (!files.length)
3504
3736
  continue;
3505
3737
  found = true;
@@ -3510,7 +3742,7 @@ Spec Analysis
3510
3742
  const allRelationships = [];
3511
3743
  const analyses = [];
3512
3744
  for (const f of files) {
3513
- const content = readFileSync2(join2(pd, f), "utf-8");
3745
+ const content = readFileSync3(join3(pd, f), "utf-8");
3514
3746
  const ast = parse(content);
3515
3747
  const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
3516
3748
  const rels = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "relationship"));
@@ -3552,6 +3784,39 @@ Spec Analysis
3552
3784
  if (r.target && !entityNames.has(r.target))
3553
3785
  gaps.push(`\uD83D\uDFE1 Relationship: unknown target "${r.target}"`);
3554
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
+ }
3555
3820
  if (gaps.length > 0) {
3556
3821
  console.log(source_default.bold(`
3557
3822
  Gaps (${gaps.length})`));
@@ -3560,26 +3825,30 @@ Spec Analysis
3560
3825
  } else
3561
3826
  console.log(source_default.green(`
3562
3827
  No gaps found.`));
3828
+ if (gaps.length > 0)
3829
+ hasGaps = true;
3563
3830
  }
3564
3831
  }
3565
3832
  if (!found)
3566
3833
  console.log(source_default.yellow("No spec projects found. Run: dotdog init <project>"));
3834
+ if (hasGaps)
3835
+ process.exit(1);
3567
3836
  });
3568
3837
  program2.command("generate [dir]").description("Generate missing spec files from SPEC.dog").option("-p, --project <name>").action((d = ".", opts) => {
3569
3838
  const dir = resolvePath2(d);
3570
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
3839
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3571
3840
  let specContent = "", specDir = "";
3572
3841
  for (const dd of dirs) {
3573
- if (!existsSync2(dd))
3842
+ if (!existsSync3(dd))
3574
3843
  continue;
3575
- 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);
3576
3845
  for (const p of projects) {
3577
3846
  if (opts.project && p !== opts.project)
3578
3847
  continue;
3579
- const pd = join2(dd, p);
3580
- const sp = join2(pd, "SPEC.dog");
3581
- if (existsSync2(sp)) {
3582
- 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");
3583
3852
  specDir = pd;
3584
3853
  break;
3585
3854
  }
@@ -3610,7 +3879,7 @@ Spec Generator
3610
3879
  uiStrings.push({ screen: section.heading, element: "label", text: m });
3611
3880
  }
3612
3881
  }
3613
- if (!existsSync2(join2(specDir, "data-model.dog")) && entities.length > 0) {
3882
+ if (!existsSync3(join3(specDir, "data-model.dog")) && entities.length > 0) {
3614
3883
  let dm = `# Data Model
3615
3884
 
3616
3885
  ## Core Entities
@@ -3643,10 +3912,10 @@ ${e.description || "No description."}
3643
3912
  `;
3644
3913
  dm += "```\n\n";
3645
3914
  }
3646
- writeFileSync(join2(specDir, "data-model.dog"), dm);
3915
+ writeFileSync2(join3(specDir, "data-model.dog"), dm);
3647
3916
  console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
3648
3917
  }
3649
- if (!existsSync2(join2(specDir, "COPY.dog")) && uiStrings.length > 0) {
3918
+ if (!existsSync3(join3(specDir, "COPY.dog")) && uiStrings.length > 0) {
3650
3919
  let copy = `# App Copy
3651
3920
 
3652
3921
  | Screen | Element | Copy |
@@ -3655,10 +3924,10 @@ ${e.description || "No description."}
3655
3924
  for (const s of uiStrings)
3656
3925
  copy += `| ${s.screen} | ${s.element} | ${s.text} |
3657
3926
  `;
3658
- writeFileSync(join2(specDir, "COPY.dog"), copy);
3927
+ writeFileSync2(join3(specDir, "COPY.dog"), copy);
3659
3928
  console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
3660
3929
  }
3661
- if (!existsSync2(join2(specDir, "INDEX.dog"))) {
3930
+ if (!existsSync3(join3(specDir, "INDEX.dog"))) {
3662
3931
  let idx = `# INDEX
3663
3932
 
3664
3933
  | You are... | Start here | Then... |
@@ -3670,41 +3939,139 @@ ${e.description || "No description."}
3670
3939
  `;
3671
3940
  idx += `| Designer | SPEC.dog | COPY.dog |
3672
3941
  `;
3673
- writeFileSync(join2(specDir, "INDEX.dog"), idx);
3942
+ writeFileSync2(join3(specDir, "INDEX.dog"), idx);
3674
3943
  console.log(source_default.green(" ✓ INDEX.dog"));
3675
3944
  }
3676
3945
  console.log(source_default.bold(`
3677
3946
  Run dotdog validate to verify.
3678
3947
  `));
3679
3948
  });
3680
- 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") : "";
3681
3997
  console.log(source_default.bold(`
3682
- Simulation: ${scenario} (project: ${opts.project})
3683
- `));
3684
- console.log(source_default.gray("Simulation engine : reads SPEC.dog scenarios, walks through steps, checks pre/postconditions."));
3685
- 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."));
3686
4053
  });
3687
4054
  program2.command("staleness [dir]").action((d = ".") => {
3688
4055
  const dir = resolvePath2(d);
3689
- const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
4056
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
3690
4057
  console.log(source_default.bold(`Staleness Audit
3691
4058
  `));
3692
4059
  for (const dd of dirs) {
3693
- if (!existsSync2(dd))
4060
+ if (!existsSync3(dd))
3694
4061
  continue;
3695
- 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);
3696
4063
  for (const p of projects) {
3697
- const pd = join2(dd, p);
3698
- if (!existsSync2(join2(pd, "SPEC.dog")))
4064
+ const pd = join3(dd, p);
4065
+ if (!existsSync3(join3(pd, "SPEC.dog")))
3699
4066
  continue;
3700
- if (!existsSync2(pd))
4067
+ if (!existsSync3(pd))
3701
4068
  continue;
3702
- const planFile = join2(pd, "plan.dog");
3703
- if (!existsSync2(planFile)) {
4069
+ const planFile = join3(pd, "plan.dog");
4070
+ if (!existsSync3(planFile)) {
3704
4071
  console.log(source_default.yellow(` ${p}: No plan.dog`));
3705
4072
  continue;
3706
4073
  }
3707
- const plan = readFileSync2(planFile, "utf-8");
4074
+ const plan = readFileSync3(planFile, "utf-8");
3708
4075
  const tasks = [...plan.matchAll(/^\s*- \[([ x])\]\s+(.+)/gm)];
3709
4076
  let issues = 0;
3710
4077
  for (const m of tasks) {
@@ -3715,14 +4082,15 @@ program2.command("staleness [dir]").action((d = ".") => {
3715
4082
  const phase = phaseMatch ? parseInt(phaseMatch[1]) : 99;
3716
4083
  if (phase > 3)
3717
4084
  continue;
3718
- if (text.includes("npm publish") || text.includes("npm install")) {
3719
- try {
3720
- const pkg2 = JSON.parse(readFileSync2(join2(resolvePath2("."), "packages/dotdog/package.json"), "utf-8"));
3721
- 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) {
3722
4090
  console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
3723
4091
  issues++;
3724
4092
  }
3725
- } catch {}
4093
+ }
3726
4094
  }
3727
4095
  if (text.includes("compile")) {
3728
4096
  if (!done) {
@@ -3731,7 +4099,7 @@ program2.command("staleness [dir]").action((d = ".") => {
3731
4099
  }
3732
4100
  }
3733
4101
  if (text.includes("generate") && !done) {
3734
- 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"))) {
3735
4103
  console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
3736
4104
  issues++;
3737
4105
  }
@@ -3751,6 +4119,206 @@ program2.command("woof").action(() => {
3751
4119
  console.log(" / (_____/");
3752
4120
  console.log("/_____/ U");
3753
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
+ });
3754
4322
  program2.parse();
3755
4323
  export {
3756
4324
  parseToJSON,