dotdog 0.4.1 → 0.5.1

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
@@ -2751,7 +2751,7 @@ function parseBlocks(lines, start, end) {
2751
2751
  let i = start;
2752
2752
  while (i < end) {
2753
2753
  const line = lines[i];
2754
- const entityMatch = line.match(/^###\s+Entity:\s*(.+)/);
2754
+ const entityMatch = line.match(/^#{3,5}\s+Entity:\s*(.+)/);
2755
2755
  if (entityMatch) {
2756
2756
  const result = parseStructuredBlock(lines, i, end, "entity", entityMatch[1]);
2757
2757
  if (result) {
@@ -2760,7 +2760,7 @@ function parseBlocks(lines, start, end) {
2760
2760
  continue;
2761
2761
  }
2762
2762
  }
2763
- const relMatch = line.match(/^###\s+Relationship:\s*(.+)/);
2763
+ const relMatch = line.match(/^#{3,5}\s+Relationship:\s*(.+)/);
2764
2764
  if (relMatch) {
2765
2765
  const result = parseStructuredBlock(lines, i, end, "relationship", relMatch[1]);
2766
2766
  if (result) {
@@ -2769,7 +2769,7 @@ function parseBlocks(lines, start, end) {
2769
2769
  continue;
2770
2770
  }
2771
2771
  }
2772
- const eventMatch = line.match(/^###\s+Event:\s*(.+)/);
2772
+ const eventMatch = line.match(/^#{3,5}\s+Event:\s*(.+)/);
2773
2773
  if (eventMatch) {
2774
2774
  const result = parseStructuredBlock(lines, i, end, "event", eventMatch[1]);
2775
2775
  if (result) {
@@ -2778,7 +2778,7 @@ function parseBlocks(lines, start, end) {
2778
2778
  continue;
2779
2779
  }
2780
2780
  }
2781
- const predMatch = line.match(/^###\s+Prediction:\s*(.+)/);
2781
+ const predMatch = line.match(/^#{3,5}\s+Prediction:\s*(.+)/);
2782
2782
  if (predMatch) {
2783
2783
  const result = parseStructuredBlock(lines, i, end, "prediction", predMatch[1]);
2784
2784
  if (result) {
@@ -2787,6 +2787,83 @@ function parseBlocks(lines, start, end) {
2787
2787
  continue;
2788
2788
  }
2789
2789
  }
2790
+ if (lines[i].startsWith("```")) {
2791
+ let yamlEnd = i + 1;
2792
+ while (yamlEnd < end && !lines[yamlEnd].startsWith("```"))
2793
+ yamlEnd++;
2794
+ if (yamlEnd < end) {
2795
+ const yamlContent = lines.slice(i + 1, yamlEnd);
2796
+ const yaml = parseSimpleYAML(yamlContent);
2797
+ if (yaml.prediction) {
2798
+ blocks.push({
2799
+ kind: "prediction",
2800
+ statement: yaml.prediction || "",
2801
+ description: yaml.description || "",
2802
+ trigger: yaml.trigger || "",
2803
+ timeframe: yaml.timeframe || "",
2804
+ confidence: yaml.confidence || 0,
2805
+ measurement: yaml.measurement || "",
2806
+ status: yaml.status || "pending",
2807
+ yaml,
2808
+ lineStart: i + 1,
2809
+ lineEnd: yamlEnd
2810
+ });
2811
+ i = yamlEnd + 1;
2812
+ continue;
2813
+ }
2814
+ if (yaml.entity) {
2815
+ blocks.push({
2816
+ kind: "entity",
2817
+ name: yaml.entity || "",
2818
+ description: yaml.description || "",
2819
+ type: yaml.type || "node",
2820
+ properties: {},
2821
+ states: Array.isArray(yaml.states) ? yaml.states : [],
2822
+ lifecycle: [],
2823
+ yaml,
2824
+ lineStart: i + 1,
2825
+ lineEnd: yamlEnd
2826
+ });
2827
+ i = yamlEnd + 1;
2828
+ continue;
2829
+ }
2830
+ if (yaml.relationship || yaml.verb) {
2831
+ blocks.push({
2832
+ kind: "relationship",
2833
+ source: yaml.source || "",
2834
+ target: yaml.target || "",
2835
+ verb: yaml.verb || "connects",
2836
+ description: yaml.description || "",
2837
+ cardinality: yaml.cardinality || "N:M",
2838
+ required: false,
2839
+ cascade: "none",
2840
+ invariants: [],
2841
+ yaml,
2842
+ lineStart: i + 1,
2843
+ lineEnd: yamlEnd
2844
+ });
2845
+ i = yamlEnd + 1;
2846
+ continue;
2847
+ }
2848
+ if (yaml.event) {
2849
+ blocks.push({
2850
+ kind: "event",
2851
+ name: yaml.event || "",
2852
+ trigger: yaml.trigger || "",
2853
+ payload: {},
2854
+ preconditions: [],
2855
+ postconditions: [],
2856
+ sideEffects: [],
2857
+ probability: null,
2858
+ yaml,
2859
+ lineStart: i + 1,
2860
+ lineEnd: yamlEnd
2861
+ });
2862
+ i = yamlEnd + 1;
2863
+ continue;
2864
+ }
2865
+ }
2866
+ }
2790
2867
  if (/^\|.+\|/.test(line) && i + 1 < end && /^\|[-| ]+\|/.test(lines[i + 1])) {
2791
2868
  const table = parseTable(lines, i, end);
2792
2869
  if (table) {
@@ -2795,8 +2872,31 @@ function parseBlocks(lines, start, end) {
2795
2872
  continue;
2796
2873
  }
2797
2874
  }
2875
+ if (lines[i].startsWith("```")) {
2876
+ let yamlEnd = i + 1;
2877
+ while (yamlEnd < end && !lines[yamlEnd].startsWith("```"))
2878
+ yamlEnd++;
2879
+ if (yamlEnd < end && yamlEnd > i + 1) {
2880
+ const yamlContent = lines.slice(i + 1, yamlEnd);
2881
+ const yaml = parseSimpleYAML(yamlContent);
2882
+ const key = yaml.prediction ? "prediction" : yaml.entity ? "entity" : yaml.event ? "event" : yaml.relationship || yaml.verb ? "relationship" : null;
2883
+ if (key) {
2884
+ if (key === "prediction") {
2885
+ blocks.push({ kind: "prediction", statement: yaml.prediction || "", description: yaml.description || "", trigger: yaml.trigger || "", timeframe: yaml.timeframe || "", confidence: yaml.confidence || 0, measurement: yaml.measurement || "", status: yaml.status || "pending", yaml, lineStart: i + 1, lineEnd: yamlEnd });
2886
+ } else if (key === "entity") {
2887
+ blocks.push({ kind: "entity", name: yaml.entity || "", description: yaml.description || "", type: yaml.type || "node", properties: {}, states: Array.isArray(yaml.states) ? yaml.states : [], lifecycle: [], yaml, lineStart: i + 1, lineEnd: yamlEnd });
2888
+ } else if (key === "event") {
2889
+ blocks.push({ kind: "event", name: yaml.event || "", trigger: yaml.trigger || "", payload: {}, preconditions: [], postconditions: [], sideEffects: [], probability: null, yaml, lineStart: i + 1, lineEnd: yamlEnd });
2890
+ } else if (key === "relationship") {
2891
+ blocks.push({ kind: "relationship", source: yaml.source || "", target: yaml.target || "", verb: yaml.verb || "connects", description: yaml.description || "", cardinality: yaml.cardinality || "N:M", required: false, cascade: "none", invariants: [], yaml, lineStart: i + 1, lineEnd: yamlEnd });
2892
+ }
2893
+ i = yamlEnd + 1;
2894
+ continue;
2895
+ }
2896
+ }
2897
+ }
2798
2898
  const proseStart = i;
2799
- while (i < end && !isBlockStart(lines[i])) {
2899
+ while (i < end && !isBlockStart(lines[i]) && !lines[i].startsWith("```")) {
2800
2900
  i++;
2801
2901
  }
2802
2902
  const proseLines = lines.slice(proseStart, i).filter((l) => l.trim() !== "" || i === proseStart + 1);
@@ -2813,7 +2913,7 @@ function parseBlocks(lines, start, end) {
2813
2913
  return blocks;
2814
2914
  }
2815
2915
  function isBlockStart(line) {
2816
- return /^###\s+(Entity|Relationship|Event|Prediction):/.test(line) || /^\|.+\|/.test(line);
2916
+ return /^#{3,5}\s+(Entity|Relationship|Event|Prediction):/.test(line) || /^\|.+\|/.test(line);
2817
2917
  }
2818
2918
  function parseStructuredBlock(lines, start, end, kind, headerRest) {
2819
2919
  let i = start + 1;
@@ -3125,7 +3225,7 @@ function E(dag) {
3125
3225
  const seen = new Set;
3126
3226
  for (const node of N(dag)) {
3127
3227
  for (const e of nodeEdges(node)) {
3128
- const src = isV2(node) ? String(node[0]) : node.i || node.id || "";
3228
+ const src = nodeId(node);
3129
3229
  const key = `${src}→${e.t}:${e.v}`;
3130
3230
  if (!seen.has(key)) {
3131
3231
  seen.add(key);
@@ -3136,8 +3236,14 @@ function E(dag) {
3136
3236
  return edges;
3137
3237
  }
3138
3238
  var P = (dag) => dag.p || dag.project || "";
3139
- var ni = (n) => isV2(n) ? String(n[0]) : n.i || n.id || "";
3239
+ var nodeId = (n) => isV2(n) ? String(n[0]) : n.i || n.id || "";
3240
+ var nodeName = (n) => isV2(n) ? n[1] || String(n[0]) : n.i || n.id || n.name || "";
3241
+ function nodeMatches(n, value) {
3242
+ const q = (value || "").toLowerCase();
3243
+ return nodeId(n).toLowerCase() === q || nodeName(n).toLowerCase() === q;
3244
+ }
3140
3245
  var nt = (n) => isV2(n) ? n[2] || "" : n.t || n.type || "";
3246
+ var nd = (n) => isV2(n) ? n[3] || "" : n.d || n.description || "";
3141
3247
  function np(n) {
3142
3248
  if (isV2(n)) {
3143
3249
  const flat = n[4] || [];
@@ -3177,7 +3283,7 @@ function serve(dir = ".") {
3177
3283
  jsonrpc: "2.0",
3178
3284
  id,
3179
3285
  result: {
3180
- protocolVersion: "0.1.0",
3286
+ protocolVersion: "2024-11-05",
3181
3287
  serverInfo: { name: "spec-serve", version: "0.1.0" },
3182
3288
  capabilities: { tools: {} }
3183
3289
  }
@@ -3215,11 +3321,12 @@ function serve(dir = ".") {
3215
3321
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
3216
3322
  if (!dag)
3217
3323
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
3218
- const node = N(dag).find((n) => ni(n).toLowerCase() === (args.name || "").toLowerCase());
3324
+ const node = N(dag).find((n) => nodeMatches(n, args.name || ""));
3219
3325
  if (!node)
3220
3326
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
3221
- const edges = E(dag).filter((e) => es(e).toLowerCase() === ni(node).toLowerCase() || et(e).toLowerCase() === ni(node).toLowerCase());
3222
- return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(isV2(node) ? { id: String(node[0]), name: node[1] || "", type: node[2] || "", description: node[3] || "", properties: np(node), states: ns(node), edges } : { ...node, edges }) }] } };
3327
+ const idForEdges = nodeId(node);
3328
+ const edges = E(dag).filter((e) => es(e).toLowerCase() === idForEdges.toLowerCase() || et(e).toLowerCase() === idForEdges.toLowerCase());
3329
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(isV2(node) ? { id: nodeId(node), name: nodeName(node), type: nt(node), description: nd(node), properties: np(node), states: ns(node), edges } : { ...node, edges }) }] } };
3223
3330
  }
3224
3331
  if (name === "traverse") {
3225
3332
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
@@ -3229,23 +3336,25 @@ function serve(dir = ".") {
3229
3336
  const visitedNodes = new Set;
3230
3337
  const visitedEdges = new Set;
3231
3338
  const subgraph = { nodes: [], edges: [] };
3232
- const queue = [{ id: args.from, depth: 0 }];
3339
+ const start = N(dag).find((n) => nodeMatches(n, args.from || ""));
3340
+ const queue = [{ id: start ? nodeId(start) : args.from, depth: 0 }];
3233
3341
  while (queue.length > 0) {
3234
3342
  const curr = queue.shift();
3235
- if (visitedNodes.has(curr.id) || curr.depth > depth)
3343
+ const node = N(dag).find((n) => nodeMatches(n, curr.id));
3344
+ const currId = node ? nodeId(node) : curr.id;
3345
+ if (visitedNodes.has(currId) || curr.depth > depth)
3236
3346
  continue;
3237
- visitedNodes.add(curr.id);
3238
- const node = N(dag).find((n) => ni(n).toLowerCase() === curr.id.toLowerCase());
3347
+ visitedNodes.add(currId);
3239
3348
  if (node)
3240
- subgraph.nodes.push(isV2(node) ? { id: String(node[0]), name: node[1] || "", type: node[2] || "", description: node[3] || "", properties: np(node), states: ns(node), edges: nodeEdges(node) } : node);
3241
- const edges = E(dag).filter((e) => es(e).toLowerCase() === curr.id.toLowerCase() || et(e).toLowerCase() === curr.id.toLowerCase());
3349
+ subgraph.nodes.push(isV2(node) ? { id: nodeId(node), name: nodeName(node), type: nt(node), description: nd(node), properties: np(node), states: ns(node), edges: nodeEdges(node) } : node);
3350
+ const edges = E(dag).filter((e) => es(e).toLowerCase() === currId.toLowerCase() || et(e).toLowerCase() === currId.toLowerCase());
3242
3351
  for (const e of edges) {
3243
3352
  const edgeKey = `${es(e)}→${et(e)}`;
3244
3353
  if (!visitedEdges.has(edgeKey)) {
3245
3354
  visitedEdges.add(edgeKey);
3246
3355
  subgraph.edges.push(e);
3247
3356
  }
3248
- const next = es(e).toLowerCase() === curr.id.toLowerCase() ? et(e) : es(e);
3357
+ const next = es(e).toLowerCase() === currId.toLowerCase() ? et(e) : es(e);
3249
3358
  if (!visitedNodes.has(next))
3250
3359
  queue.push({ id: next, depth: curr.depth + 1 });
3251
3360
  }
@@ -3258,7 +3367,7 @@ function serve(dir = ".") {
3258
3367
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
3259
3368
  const q = (args.q || "").toLowerCase();
3260
3369
  const type = (args.type || "").toLowerCase();
3261
- const results = N(dag).filter((n) => ni(n).toLowerCase().includes(q) && (!type || nt(n).toLowerCase().includes(type)));
3370
+ const results = N(dag).filter((n) => (nodeName(n).toLowerCase().includes(q) || nodeId(n).toLowerCase().includes(q) || nt(n).toLowerCase().includes(q)) && (!type || nt(n).toLowerCase().includes(type)));
3262
3371
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(results) }] } };
3263
3372
  }
3264
3373
  if (name === "summary") {
@@ -3282,11 +3391,11 @@ function serve(dir = ".") {
3282
3391
  const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
3283
3392
  if (!dag)
3284
3393
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
3285
- const node = N(dag).find((n) => ni(n).toLowerCase() === (args.entity || "").toLowerCase());
3394
+ const node = N(dag).find((n) => nodeMatches(n, args.entity || ""));
3286
3395
  if (!node)
3287
3396
  return { jsonrpc: "2.0", id, error: { code: 404, message: "Entity not found" } };
3288
3397
  return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({
3289
- entity: ni(node),
3398
+ entity: nodeName(node),
3290
3399
  properties: np(node),
3291
3400
  states: ns(node),
3292
3401
  lifecycle: nl(node)
@@ -3381,7 +3490,7 @@ program2.command("validate [dir]").action((d = ".") => {
3381
3490
  const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
3382
3491
  const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
3383
3492
  console.log(source_default.bold(`
3384
- ${p} : ${files.length} .dog files, ${100 - Math.round((missing.length * 3 + optional.length) / 20 * 100)}% complete`));
3493
+ ${p} : ${files.length} .dog files, ${Math.max(0, 100 - Math.round(missing.length * 3 / 20 * 100))}% complete`));
3385
3494
  for (const f of files)
3386
3495
  console.log(source_default.gray(` ${f}`));
3387
3496
  if (missing.length) {
@@ -3389,7 +3498,7 @@ program2.command("validate [dir]").action((d = ".") => {
3389
3498
  hasErrors = true;
3390
3499
  }
3391
3500
  if (optional.length)
3392
- console.log(source_default.yellow(` Missing optional: ${optional.join(", ")}`));
3501
+ console.log(source_default.gray(` Optional: ${optional.join(", ")} — not required for 100%`));
3393
3502
  }
3394
3503
  }
3395
3504
  if (!found)
@@ -3592,18 +3701,18 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3592
3701
  nodes.forEach((n, i) => nodeIds.set(n.i, i));
3593
3702
  const v2nodes = [];
3594
3703
  for (let j = 0;j < nodes.length; j++) {
3595
- const nd = nodes[j];
3704
+ const nd2 = nodes[j];
3596
3705
  const props = [];
3597
- if (nd.p)
3598
- for (const [k, v] of Object.entries(nd.p))
3706
+ if (nd2.p)
3707
+ for (const [k, v] of Object.entries(nd2.p))
3599
3708
  props.push(k, v);
3600
- const states = nd.s || [];
3709
+ const states = nd2.s || [];
3601
3710
  const outEdges = [];
3602
3711
  const seen = new Set;
3603
3712
  for (const e of edges) {
3604
- if (e.s !== nd.i && e.t !== nd.i)
3713
+ if (e.s !== nd2.i && e.t !== nd2.i)
3605
3714
  continue;
3606
- const tid = nodeIds.get(e.s === nd.i ? e.t : e.s);
3715
+ const tid = nodeIds.get(e.s === nd2.i ? e.t : e.s);
3607
3716
  if (tid === undefined)
3608
3717
  continue;
3609
3718
  const key = `${j}→${tid}:${e.v}`;
@@ -3617,17 +3726,17 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
3617
3726
  ee.push(1);
3618
3727
  outEdges.push(ee);
3619
3728
  }
3620
- const entry = [j, nd.i || "", nd.t || "", nd.d || "", props, states, outEdges];
3621
- if (nd.g === "prediction") {
3729
+ const entry = [j, nd2.i || "", nd2.t || "", nd2.d || "", props, states, outEdges];
3730
+ if (nd2.g === "prediction") {
3622
3731
  const f = [];
3623
- if (nd.cf)
3624
- f.push(nd.cf);
3625
- if (nd.tf)
3626
- f.push(nd.tf);
3627
- if (nd.tg)
3628
- f.push(nd.tg);
3629
- if (nd.ms)
3630
- f.push(nd.ms);
3732
+ if (nd2.cf)
3733
+ f.push(nd2.cf);
3734
+ if (nd2.tf)
3735
+ f.push(nd2.tf);
3736
+ if (nd2.tg)
3737
+ f.push(nd2.tg);
3738
+ if (nd2.ms)
3739
+ f.push(nd2.ms);
3631
3740
  if (f.length)
3632
3741
  entry.push(f);
3633
3742
  }
@@ -3709,7 +3818,7 @@ program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts)
3709
3818
  const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
3710
3819
  const nodes = dag.n || dag.nodes || [];
3711
3820
  const isV22 = (n) => Array.isArray(n) && typeof n[0] === "number";
3712
- const nodeName = (n) => isV22(n) ? nodes[n[0]] ? nodes[n[0]][1] || String(n[0]) : String(n[0]) : n.i || n.id || "";
3821
+ const nodeName2 = (n) => isV22(n) ? nodes[n[0]] ? nodes[n[0]][1] || String(n[0]) : String(n[0]) : n.i || n.id || "";
3713
3822
  const slug = (s) => s.replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
3714
3823
  let out = "```mermaid\ngraph LR\n";
3715
3824
  for (const n of nodes) {
@@ -3823,7 +3932,7 @@ Spec Analysis
3823
3932
  for (const f of missingReq)
3824
3933
  gaps.push(`\uD83D\uDD34 ${f}: Missing required file`);
3825
3934
  for (const f of missingOpt)
3826
- gaps.push(`\uD83D\uDFE1 ${f}: Missing optional file`);
3935
+ gaps.push(`ℹ️ ${f}: Optional file not present`);
3827
3936
  const entityNames = new Set(allEntities.map((e) => e.name));
3828
3937
  for (const e of allEntities) {
3829
3938
  if (!e.description || e.description.length < 10)
@@ -4189,6 +4298,142 @@ program2.command("staleness [dir]").action((d = ".") => {
4189
4298
  }
4190
4299
  }
4191
4300
  });
4301
+ program2.command("verify [dir]").description("Verify spec-code alignment. --init auto-generates verify section in plan.dog").option("-i, --init", "Auto-generate verify section from codebase scan").action((d = ".", opts) => {
4302
+ const dir = resolvePath2(d);
4303
+ const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
4304
+ console.log(source_default.bold(opts.init ? `Auto-Generating Verify Section
4305
+ ` : `Verification Audit
4306
+ `));
4307
+ for (const dd of dirs) {
4308
+ if (!existsSync3(dd))
4309
+ continue;
4310
+ const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4311
+ for (const p of projects) {
4312
+ const pd = join3(dd, p);
4313
+ if (!existsSync3(join3(pd, "SPEC.dog")))
4314
+ continue;
4315
+ const planFile = join3(pd, "plan.dog");
4316
+ if (!existsSync3(planFile)) {
4317
+ console.log(source_default.yellow(` ${p}: No plan.dog`));
4318
+ continue;
4319
+ }
4320
+ const dagFile = join3(pd, `${p}.dag`);
4321
+ if (!existsSync3(dagFile)) {
4322
+ console.log(source_default.yellow(` ${p}: No .dag file. Run compile first.`));
4323
+ continue;
4324
+ }
4325
+ const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
4326
+ const plan = readFileSync3(planFile, "utf-8");
4327
+ if (opts.init) {
4328
+ const entities = [];
4329
+ const props = new Map;
4330
+ for (const node of dag.n || []) {
4331
+ const name = node[1] || String(node[0]);
4332
+ entities.push(name);
4333
+ if (node[4])
4334
+ props.set(name, node[4].map((p2) => p2[0]));
4335
+ }
4336
+ let verify = `
4337
+ ## Verify
4338
+
4339
+ `;
4340
+ for (const entity of entities) {
4341
+ const nameLower = entity.toLowerCase().replace(/[^a-z0-9]/g, "");
4342
+ let matchFile = "";
4343
+ const skip = new Set(["node_modules", ".git", "dist", ".bun", "dev", "build"]);
4344
+ const codeDirs = [join3(dir, "src"), join3(dir, "lib"), join3(dir, "app"), dir];
4345
+ for (const cd of codeDirs) {
4346
+ if (!existsSync3(cd))
4347
+ continue;
4348
+ try {
4349
+ const allFiles = readdirSync3(cd, { recursive: true }).filter((f) => {
4350
+ if (!(f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".py") || f.endsWith(".sol") || f.endsWith(".go")))
4351
+ return false;
4352
+ for (const part of f.split("/")) {
4353
+ if (skip.has(part))
4354
+ return false;
4355
+ }
4356
+ return true;
4357
+ });
4358
+ const match = allFiles.find((f) => f.toLowerCase().includes(nameLower));
4359
+ if (match) {
4360
+ matchFile = join3(cd, match).replace(dir, ".");
4361
+ break;
4362
+ }
4363
+ } catch (_) {}
4364
+ }
4365
+ verify += `### Entity: ${entity}
4366
+ `;
4367
+ if (matchFile) {
4368
+ verify += ` file: ${matchFile}
4369
+ `;
4370
+ const fullPath = join3(dir, matchFile.replace("./", ""));
4371
+ if (existsSync3(fullPath)) {
4372
+ const code = readFileSync3(fullPath, "utf-8");
4373
+ const codeProps = [...code.matchAll(/\b(\w+)\s*[:?]\s*\w+/g)].map((m) => m[1]).filter((v, i, a) => a.indexOf(v) === i);
4374
+ if (codeProps.length > 0) {
4375
+ verify += ` properties: [${codeProps.slice(0, 10).join(", ")}]
4376
+ `;
4377
+ }
4378
+ }
4379
+ } else {
4380
+ verify += ` # no matching file found — map manually
4381
+ `;
4382
+ }
4383
+ verify += `
4384
+ `;
4385
+ }
4386
+ const updatedPlan = plan.includes("## Verify") ? plan : plan + verify;
4387
+ writeFileSync2(planFile, updatedPlan);
4388
+ console.log(source_default.green(` ${p}: Verify section generated in plan.dog`));
4389
+ } else {
4390
+ const verifyMatch = plan.match(/## Verify\n([\s\S]*?)(?=\n## |$)/);
4391
+ if (!verifyMatch) {
4392
+ console.log(source_default.yellow(` ${p}: No ## Verify section. Run: dotdog verify --init`));
4393
+ continue;
4394
+ }
4395
+ const verifyBlock = verifyMatch[1];
4396
+ const entityBlocks = [...verifyBlock.matchAll(/### Entity: (\w+)\n([\s\S]*?)(?=### Entity:|$)/g)];
4397
+ let checks = 0, passed = 0;
4398
+ for (const [, ename, ebody] of entityBlocks) {
4399
+ checks++;
4400
+ const fileMatch = ebody.match(/file:\s*(.+)/);
4401
+ const propMatch = ebody.match(/properties:\s*\[([^\]]+)\]/);
4402
+ if (!fileMatch)
4403
+ continue;
4404
+ const filePath = join3(dir, fileMatch[1].trim().replace("./", ""));
4405
+ if (!existsSync3(filePath)) {
4406
+ console.log(source_default.red(` ✗ ${ename}: file ${fileMatch[1].trim()} not found`));
4407
+ continue;
4408
+ }
4409
+ const code = readFileSync3(filePath, "utf-8");
4410
+ const cleanCode = code.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
4411
+ if (propMatch) {
4412
+ const props = propMatch[1].split(",").map((s) => s.trim());
4413
+ let propPass = 0;
4414
+ for (const prop of props) {
4415
+ const snakeVariant = prop.replace(/_/g, "");
4416
+ const camelVariant = prop.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
4417
+ if (cleanCode.includes(prop) || cleanCode.includes(snakeVariant) || cleanCode.includes(camelVariant)) {
4418
+ propPass++;
4419
+ } else {
4420
+ console.log(source_default.yellow(` ⚠ ${ename}.${prop}: not found in ${fileMatch[1].trim()}`));
4421
+ }
4422
+ }
4423
+ if (propPass === props.length)
4424
+ passed++;
4425
+ }
4426
+ }
4427
+ if (checks === 0)
4428
+ console.log(source_default.yellow(` ${p}: No entities mapped. Run: dotdog verify --init`));
4429
+ else if (passed === checks)
4430
+ console.log(source_default.green(` ${p}: ${passed}/${checks} entities verified`));
4431
+ else
4432
+ console.log(source_default.bold(` ${p}: ${passed}/${checks} entities verified`));
4433
+ }
4434
+ }
4435
+ }
4436
+ });
4192
4437
  program2.command("woof").action(() => {
4193
4438
  console.log(" / \\__");
4194
4439
  console.log(" ( @\\___");
@@ -4324,8 +4569,19 @@ program2.command("resolve <name>").description("Mark a prediction as correct, wr
4324
4569
  if (block.kind === "prediction") {
4325
4570
  const b = block;
4326
4571
  if ((b.statement || b.name || "").toLowerCase().includes(name.toLowerCase())) {
4327
- const searchFor = `### Prediction: ${b.statement || b.name}`;
4328
- const headingIdx = content.indexOf(searchFor);
4572
+ let headingIdx = -1;
4573
+ const stmt = b.statement || b.name || "";
4574
+ for (const prefix of ["###", "####", "#####"]) {
4575
+ for (const fmt of [`${prefix} Prediction: ${stmt}`, `${prefix} ${stmt}`]) {
4576
+ const idx = content.indexOf(fmt);
4577
+ if (idx >= 0) {
4578
+ headingIdx = idx;
4579
+ break;
4580
+ }
4581
+ }
4582
+ if (headingIdx >= 0)
4583
+ break;
4584
+ }
4329
4585
  if (headingIdx >= 0) {
4330
4586
  const blockStart = content.indexOf("```yaml", headingIdx);
4331
4587
  const blockEnd = content.indexOf("```", blockStart + 7);
@@ -0,0 +1,43 @@
1
+ # E-Commerce Platform
2
+
3
+ ## Product
4
+
5
+ An online store. Customers browse products, add to cart, checkout, and track orders.
6
+
7
+ ## What the User Sees
8
+
9
+ ### Screen: Product Page
10
+
11
+ +------------------------------------------+
12
+ | [Logo] Shop Categories Cart(3) |
13
+ |------------------------------------------|
14
+ | |
15
+ | [Product Image] |
16
+ | |
17
+ | Wireless Headphones $79 |
18
+ | 4.2 stars (128 reviews) |
19
+ | [Add to Cart] |
20
+ +------------------------------------------+
21
+
22
+ ### Screen: Checkout
23
+
24
+ +------------------------------------------+
25
+ | Checkout |
26
+ |------------------------------------------|
27
+ | Shipping: Alex Smith |
28
+ | 123 Main St, NY 10001 |
29
+ | |
30
+ | Payment: Visa ****4242 |
31
+ | |
32
+ | Order total: $79.00 |
33
+ | [Place Order] |
34
+ +------------------------------------------+
35
+
36
+ ## User Stories
37
+
38
+ | ID | Story | Pri | Acceptance |
39
+ |----|-------|-----|------------|
40
+ | US-01 | Customer adds product to cart | P0 | Cart count updates |
41
+ | US-02 | Customer checks out | P0 | Order created, inventory reduced |
42
+ | US-03 | Customer tracks order | P1 | Status and tracking number visible |
43
+ | US-04 | Customer leaves a review | P2 | Review appears on product page |
@@ -0,0 +1,9 @@
1
+ # E-Commerce Platform — Constitution
2
+
3
+ ## Core Rules
4
+
5
+ - Inventory is source of truth — never oversell
6
+ - Prices are immutable once an order is placed
7
+ - Payment is atomic — charge and fulfill or refund
8
+ - Customer data is encrypted at rest
9
+ - Orders flow through defined state machine
@@ -0,0 +1,58 @@
1
+ # E-Commerce — Data Model
2
+
3
+ ## Entities
4
+
5
+ ### Entity: Product
6
+
7
+ An item available for purchase.
8
+
9
+ ```yaml
10
+ entity: Product
11
+ type: entity
12
+ properties:
13
+ id:
14
+ type: string
15
+ required: true
16
+ name:
17
+ type: string
18
+ required: true
19
+ price_cents:
20
+ type: number
21
+ required: true
22
+ inventory:
23
+ type: number
24
+ required: true
25
+ default: 0
26
+ states: [draft, published, archived, out_of_stock]
27
+ lifecycle: draft → published → out_of_stock → archived
28
+ ```
29
+
30
+ ### Entity: Order
31
+
32
+ A completed purchase.
33
+
34
+ ```yaml
35
+ entity: Order
36
+ type: entity
37
+ properties:
38
+ id:
39
+ type: string
40
+ required: true
41
+ total_cents:
42
+ type: number
43
+ required: true
44
+ customer_email:
45
+ type: string
46
+ required: true
47
+ states: [pending, confirmed, shipped, delivered, refunded]
48
+ lifecycle: pending → confirmed → shipped → delivered
49
+ ```
50
+
51
+ ### Relationship: Order → Product
52
+
53
+ ```yaml
54
+ relationship: Order → Product
55
+ verb: contains
56
+ cardinality: N:N
57
+ required: true
58
+ ```
@@ -0,0 +1,37 @@
1
+ # SaaS Platform
2
+
3
+ ## Product
4
+
5
+ A multi-tenant SaaS platform. Organizations sign up, invite team members, manage subscriptions, and use the product.
6
+
7
+ ## What the User Sees
8
+
9
+ ### Screen: Dashboard
10
+
11
+ +------------------------------------------+
12
+ | Dashboard [Settings] |
13
+ |------------------------------------------|
14
+ | Welcome back, Alex |
15
+ | |
16
+ | [Projects: 12] [Team: 5] [Usage: 67%]|
17
+ +------------------------------------------+
18
+
19
+ ### Screen: Billing
20
+
21
+ +------------------------------------------+
22
+ | Billing [Upgrade] |
23
+ |------------------------------------------|
24
+ | Plan: Pro — $29/mo |
25
+ | Next invoice: Jun 30 — $29.00 |
26
+ | |
27
+ | Payment method: Visa ****4242 |
28
+ +------------------------------------------+
29
+
30
+ ## User Stories
31
+
32
+ | ID | Story | Pri | Acceptance |
33
+ |----|-------|-----|------------|
34
+ | US-01 | User creates an organization | P0 | Org appears in workspace selector |
35
+ | US-02 | User invites team member | P0 | Invitee receives email, joins org |
36
+ | US-03 | Org upgrades subscription | P1 | Plan changes, invoice generated |
37
+ | US-04 | Admin views usage report | P2 | Usage metrics displayed by project |
@@ -0,0 +1,98 @@
1
+ # SaaS Platform — Data Model
2
+
3
+ ## Entities
4
+
5
+ ### Entity: Organization
6
+
7
+ A workspace that contains projects and members.
8
+
9
+ ```yaml
10
+ entity: Organization
11
+ type: entity
12
+ properties:
13
+ id:
14
+ type: string
15
+ required: true
16
+ name:
17
+ type: string
18
+ required: true
19
+ plan:
20
+ type: enum
21
+ required: true
22
+ values: [free, pro, enterprise]
23
+ seats:
24
+ type: number
25
+ required: true
26
+ default: 1
27
+ states: [active, suspended, deleted]
28
+ lifecycle: active → suspended → deleted
29
+ ```
30
+
31
+ ### Entity: User
32
+
33
+ A person with access to one or more organizations.
34
+
35
+ ```yaml
36
+ entity: User
37
+ type: entity
38
+ properties:
39
+ id:
40
+ type: string
41
+ required: true
42
+ email:
43
+ type: string
44
+ required: true
45
+ name:
46
+ type: string
47
+ required: true
48
+ role:
49
+ type: enum
50
+ required: true
51
+ values: [owner, admin, member]
52
+ states: [active, invited, suspended]
53
+ lifecycle: invited → active → suspended
54
+ ```
55
+
56
+ ### Entity: Subscription
57
+
58
+ Billing plan for an organization.
59
+
60
+ ```yaml
61
+ entity: Subscription
62
+ type: entity
63
+ properties:
64
+ id:
65
+ type: string
66
+ required: true
67
+ plan:
68
+ type: enum
69
+ required: true
70
+ values: [free, pro, enterprise]
71
+ price_cents:
72
+ type: number
73
+ required: true
74
+ billing_cycle:
75
+ type: enum
76
+ required: true
77
+ values: [monthly, annual]
78
+ states: [active, past_due, canceled]
79
+ lifecycle: active → past_due → canceled
80
+ ```
81
+
82
+ ### Relationship: Organization → User
83
+
84
+ ```yaml
85
+ relationship: Organization → User
86
+ verb: has_member
87
+ cardinality: N:N
88
+ required: true
89
+ ```
90
+
91
+ ### Relationship: Organization → Subscription
92
+
93
+ ```yaml
94
+ relationship: Organization → Subscription
95
+ verb: has
96
+ cardinality: 1:1
97
+ required: true
98
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotdog",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "CLI tool for structured software specifications. Validate .dog files, compile .dag graphs, query via MCP.",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -35,7 +35,7 @@
35
35
  "yaml"
36
36
  ],
37
37
  "license": "MIT",
38
- "author": "specdog",
38
+ "author": "Justin Diclemente",
39
39
  "repository": "github:specdog/dotdog",
40
40
  "dependencies": {
41
41
  "commander": "^15.0.0",