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 +300 -44
- package/kits/ecommerce/SPEC.dog +43 -0
- package/kits/ecommerce/constitution.dog +9 -0
- package/kits/ecommerce/data-model.dog +58 -0
- package/kits/saas/SPEC.dog +37 -0
- package/kits/saas/data-model.dog +98 -0
- package/package.json +2 -2
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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: "
|
|
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) =>
|
|
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
|
|
3222
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
3241
|
-
const edges = E(dag).filter((e) => es(e).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() ===
|
|
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) =>
|
|
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) =>
|
|
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:
|
|
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(
|
|
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.
|
|
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
|
|
3704
|
+
const nd2 = nodes[j];
|
|
3596
3705
|
const props = [];
|
|
3597
|
-
if (
|
|
3598
|
-
for (const [k, v] of Object.entries(
|
|
3706
|
+
if (nd2.p)
|
|
3707
|
+
for (const [k, v] of Object.entries(nd2.p))
|
|
3599
3708
|
props.push(k, v);
|
|
3600
|
-
const states =
|
|
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 !==
|
|
3713
|
+
if (e.s !== nd2.i && e.t !== nd2.i)
|
|
3605
3714
|
continue;
|
|
3606
|
-
const tid = nodeIds.get(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,
|
|
3621
|
-
if (
|
|
3729
|
+
const entry = [j, nd2.i || "", nd2.t || "", nd2.d || "", props, states, outEdges];
|
|
3730
|
+
if (nd2.g === "prediction") {
|
|
3622
3731
|
const f = [];
|
|
3623
|
-
if (
|
|
3624
|
-
f.push(
|
|
3625
|
-
if (
|
|
3626
|
-
f.push(
|
|
3627
|
-
if (
|
|
3628
|
-
f.push(
|
|
3629
|
-
if (
|
|
3630
|
-
f.push(
|
|
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
|
|
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(
|
|
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
|
-
|
|
4328
|
-
const
|
|
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.
|
|
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": "
|
|
38
|
+
"author": "Justin Diclemente",
|
|
39
39
|
"repository": "github:specdog/dotdog",
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"commander": "^15.0.0",
|