dotdog 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +178 -33
- 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
|
@@ -3125,7 +3125,7 @@ function E(dag) {
|
|
|
3125
3125
|
const seen = new Set;
|
|
3126
3126
|
for (const node of N(dag)) {
|
|
3127
3127
|
for (const e of nodeEdges(node)) {
|
|
3128
|
-
const src =
|
|
3128
|
+
const src = nodeId(node);
|
|
3129
3129
|
const key = `${src}→${e.t}:${e.v}`;
|
|
3130
3130
|
if (!seen.has(key)) {
|
|
3131
3131
|
seen.add(key);
|
|
@@ -3136,8 +3136,14 @@ function E(dag) {
|
|
|
3136
3136
|
return edges;
|
|
3137
3137
|
}
|
|
3138
3138
|
var P = (dag) => dag.p || dag.project || "";
|
|
3139
|
-
var
|
|
3139
|
+
var nodeId = (n) => isV2(n) ? String(n[0]) : n.i || n.id || "";
|
|
3140
|
+
var nodeName = (n) => isV2(n) ? n[1] || String(n[0]) : n.i || n.id || n.name || "";
|
|
3141
|
+
function nodeMatches(n, value) {
|
|
3142
|
+
const q = (value || "").toLowerCase();
|
|
3143
|
+
return nodeId(n).toLowerCase() === q || nodeName(n).toLowerCase() === q;
|
|
3144
|
+
}
|
|
3140
3145
|
var nt = (n) => isV2(n) ? n[2] || "" : n.t || n.type || "";
|
|
3146
|
+
var nd = (n) => isV2(n) ? n[3] || "" : n.d || n.description || "";
|
|
3141
3147
|
function np(n) {
|
|
3142
3148
|
if (isV2(n)) {
|
|
3143
3149
|
const flat = n[4] || [];
|
|
@@ -3177,7 +3183,7 @@ function serve(dir = ".") {
|
|
|
3177
3183
|
jsonrpc: "2.0",
|
|
3178
3184
|
id,
|
|
3179
3185
|
result: {
|
|
3180
|
-
protocolVersion: "
|
|
3186
|
+
protocolVersion: "2024-11-05",
|
|
3181
3187
|
serverInfo: { name: "spec-serve", version: "0.1.0" },
|
|
3182
3188
|
capabilities: { tools: {} }
|
|
3183
3189
|
}
|
|
@@ -3215,11 +3221,12 @@ function serve(dir = ".") {
|
|
|
3215
3221
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
3216
3222
|
if (!dag)
|
|
3217
3223
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
3218
|
-
const node = N(dag).find((n) =>
|
|
3224
|
+
const node = N(dag).find((n) => nodeMatches(n, args.name || ""));
|
|
3219
3225
|
if (!node)
|
|
3220
3226
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
|
|
3221
|
-
const
|
|
3222
|
-
|
|
3227
|
+
const idForEdges = nodeId(node);
|
|
3228
|
+
const edges = E(dag).filter((e) => es(e).toLowerCase() === idForEdges.toLowerCase() || et(e).toLowerCase() === idForEdges.toLowerCase());
|
|
3229
|
+
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
3230
|
}
|
|
3224
3231
|
if (name === "traverse") {
|
|
3225
3232
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
@@ -3229,23 +3236,25 @@ function serve(dir = ".") {
|
|
|
3229
3236
|
const visitedNodes = new Set;
|
|
3230
3237
|
const visitedEdges = new Set;
|
|
3231
3238
|
const subgraph = { nodes: [], edges: [] };
|
|
3232
|
-
const
|
|
3239
|
+
const start = N(dag).find((n) => nodeMatches(n, args.from || ""));
|
|
3240
|
+
const queue = [{ id: start ? nodeId(start) : args.from, depth: 0 }];
|
|
3233
3241
|
while (queue.length > 0) {
|
|
3234
3242
|
const curr = queue.shift();
|
|
3235
|
-
|
|
3243
|
+
const node = N(dag).find((n) => nodeMatches(n, curr.id));
|
|
3244
|
+
const currId = node ? nodeId(node) : curr.id;
|
|
3245
|
+
if (visitedNodes.has(currId) || curr.depth > depth)
|
|
3236
3246
|
continue;
|
|
3237
|
-
visitedNodes.add(
|
|
3238
|
-
const node = N(dag).find((n) => ni(n).toLowerCase() === curr.id.toLowerCase());
|
|
3247
|
+
visitedNodes.add(currId);
|
|
3239
3248
|
if (node)
|
|
3240
|
-
subgraph.nodes.push(isV2(node) ? { id:
|
|
3241
|
-
const edges = E(dag).filter((e) => es(e).toLowerCase() ===
|
|
3249
|
+
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);
|
|
3250
|
+
const edges = E(dag).filter((e) => es(e).toLowerCase() === currId.toLowerCase() || et(e).toLowerCase() === currId.toLowerCase());
|
|
3242
3251
|
for (const e of edges) {
|
|
3243
3252
|
const edgeKey = `${es(e)}→${et(e)}`;
|
|
3244
3253
|
if (!visitedEdges.has(edgeKey)) {
|
|
3245
3254
|
visitedEdges.add(edgeKey);
|
|
3246
3255
|
subgraph.edges.push(e);
|
|
3247
3256
|
}
|
|
3248
|
-
const next = es(e).toLowerCase() ===
|
|
3257
|
+
const next = es(e).toLowerCase() === currId.toLowerCase() ? et(e) : es(e);
|
|
3249
3258
|
if (!visitedNodes.has(next))
|
|
3250
3259
|
queue.push({ id: next, depth: curr.depth + 1 });
|
|
3251
3260
|
}
|
|
@@ -3258,7 +3267,7 @@ function serve(dir = ".") {
|
|
|
3258
3267
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
3259
3268
|
const q = (args.q || "").toLowerCase();
|
|
3260
3269
|
const type = (args.type || "").toLowerCase();
|
|
3261
|
-
const results = N(dag).filter((n) =>
|
|
3270
|
+
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
3271
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(results) }] } };
|
|
3263
3272
|
}
|
|
3264
3273
|
if (name === "summary") {
|
|
@@ -3282,11 +3291,11 @@ function serve(dir = ".") {
|
|
|
3282
3291
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
3283
3292
|
if (!dag)
|
|
3284
3293
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Project not found" } };
|
|
3285
|
-
const node = N(dag).find((n) =>
|
|
3294
|
+
const node = N(dag).find((n) => nodeMatches(n, args.entity || ""));
|
|
3286
3295
|
if (!node)
|
|
3287
3296
|
return { jsonrpc: "2.0", id, error: { code: 404, message: "Entity not found" } };
|
|
3288
3297
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({
|
|
3289
|
-
entity:
|
|
3298
|
+
entity: nodeName(node),
|
|
3290
3299
|
properties: np(node),
|
|
3291
3300
|
states: ns(node),
|
|
3292
3301
|
lifecycle: nl(node)
|
|
@@ -3592,18 +3601,18 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3592
3601
|
nodes.forEach((n, i) => nodeIds.set(n.i, i));
|
|
3593
3602
|
const v2nodes = [];
|
|
3594
3603
|
for (let j = 0;j < nodes.length; j++) {
|
|
3595
|
-
const
|
|
3604
|
+
const nd2 = nodes[j];
|
|
3596
3605
|
const props = [];
|
|
3597
|
-
if (
|
|
3598
|
-
for (const [k, v] of Object.entries(
|
|
3606
|
+
if (nd2.p)
|
|
3607
|
+
for (const [k, v] of Object.entries(nd2.p))
|
|
3599
3608
|
props.push(k, v);
|
|
3600
|
-
const states =
|
|
3609
|
+
const states = nd2.s || [];
|
|
3601
3610
|
const outEdges = [];
|
|
3602
3611
|
const seen = new Set;
|
|
3603
3612
|
for (const e of edges) {
|
|
3604
|
-
if (e.s !==
|
|
3613
|
+
if (e.s !== nd2.i && e.t !== nd2.i)
|
|
3605
3614
|
continue;
|
|
3606
|
-
const tid = nodeIds.get(e.s ===
|
|
3615
|
+
const tid = nodeIds.get(e.s === nd2.i ? e.t : e.s);
|
|
3607
3616
|
if (tid === undefined)
|
|
3608
3617
|
continue;
|
|
3609
3618
|
const key = `${j}→${tid}:${e.v}`;
|
|
@@ -3617,17 +3626,17 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3617
3626
|
ee.push(1);
|
|
3618
3627
|
outEdges.push(ee);
|
|
3619
3628
|
}
|
|
3620
|
-
const entry = [j,
|
|
3621
|
-
if (
|
|
3629
|
+
const entry = [j, nd2.i || "", nd2.t || "", nd2.d || "", props, states, outEdges];
|
|
3630
|
+
if (nd2.g === "prediction") {
|
|
3622
3631
|
const f = [];
|
|
3623
|
-
if (
|
|
3624
|
-
f.push(
|
|
3625
|
-
if (
|
|
3626
|
-
f.push(
|
|
3627
|
-
if (
|
|
3628
|
-
f.push(
|
|
3629
|
-
if (
|
|
3630
|
-
f.push(
|
|
3632
|
+
if (nd2.cf)
|
|
3633
|
+
f.push(nd2.cf);
|
|
3634
|
+
if (nd2.tf)
|
|
3635
|
+
f.push(nd2.tf);
|
|
3636
|
+
if (nd2.tg)
|
|
3637
|
+
f.push(nd2.tg);
|
|
3638
|
+
if (nd2.ms)
|
|
3639
|
+
f.push(nd2.ms);
|
|
3631
3640
|
if (f.length)
|
|
3632
3641
|
entry.push(f);
|
|
3633
3642
|
}
|
|
@@ -3709,7 +3718,7 @@ program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts)
|
|
|
3709
3718
|
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
3710
3719
|
const nodes = dag.n || dag.nodes || [];
|
|
3711
3720
|
const isV22 = (n) => Array.isArray(n) && typeof n[0] === "number";
|
|
3712
|
-
const
|
|
3721
|
+
const nodeName2 = (n) => isV22(n) ? nodes[n[0]] ? nodes[n[0]][1] || String(n[0]) : String(n[0]) : n.i || n.id || "";
|
|
3713
3722
|
const slug = (s) => s.replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
|
|
3714
3723
|
let out = "```mermaid\ngraph LR\n";
|
|
3715
3724
|
for (const n of nodes) {
|
|
@@ -4189,6 +4198,142 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
4189
4198
|
}
|
|
4190
4199
|
}
|
|
4191
4200
|
});
|
|
4201
|
+
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) => {
|
|
4202
|
+
const dir = resolvePath2(d);
|
|
4203
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4204
|
+
console.log(source_default.bold(opts.init ? `Auto-Generating Verify Section
|
|
4205
|
+
` : `Verification Audit
|
|
4206
|
+
`));
|
|
4207
|
+
for (const dd of dirs) {
|
|
4208
|
+
if (!existsSync3(dd))
|
|
4209
|
+
continue;
|
|
4210
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4211
|
+
for (const p of projects) {
|
|
4212
|
+
const pd = join3(dd, p);
|
|
4213
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
4214
|
+
continue;
|
|
4215
|
+
const planFile = join3(pd, "plan.dog");
|
|
4216
|
+
if (!existsSync3(planFile)) {
|
|
4217
|
+
console.log(source_default.yellow(` ${p}: No plan.dog`));
|
|
4218
|
+
continue;
|
|
4219
|
+
}
|
|
4220
|
+
const dagFile = join3(pd, `${p}.dag`);
|
|
4221
|
+
if (!existsSync3(dagFile)) {
|
|
4222
|
+
console.log(source_default.yellow(` ${p}: No .dag file. Run compile first.`));
|
|
4223
|
+
continue;
|
|
4224
|
+
}
|
|
4225
|
+
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
4226
|
+
const plan = readFileSync3(planFile, "utf-8");
|
|
4227
|
+
if (opts.init) {
|
|
4228
|
+
const entities = [];
|
|
4229
|
+
const props = new Map;
|
|
4230
|
+
for (const node of dag.n || []) {
|
|
4231
|
+
const name = node[1] || String(node[0]);
|
|
4232
|
+
entities.push(name);
|
|
4233
|
+
if (node[4])
|
|
4234
|
+
props.set(name, node[4].map((p2) => p2[0]));
|
|
4235
|
+
}
|
|
4236
|
+
let verify = `
|
|
4237
|
+
## Verify
|
|
4238
|
+
|
|
4239
|
+
`;
|
|
4240
|
+
for (const entity of entities) {
|
|
4241
|
+
const nameLower = entity.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4242
|
+
let matchFile = "";
|
|
4243
|
+
const skip = new Set(["node_modules", ".git", "dist", ".bun", "dev", "build"]);
|
|
4244
|
+
const codeDirs = [join3(dir, "src"), join3(dir, "lib"), join3(dir, "app"), dir];
|
|
4245
|
+
for (const cd of codeDirs) {
|
|
4246
|
+
if (!existsSync3(cd))
|
|
4247
|
+
continue;
|
|
4248
|
+
try {
|
|
4249
|
+
const allFiles = readdirSync3(cd, { recursive: true }).filter((f) => {
|
|
4250
|
+
if (!(f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".py") || f.endsWith(".sol") || f.endsWith(".go")))
|
|
4251
|
+
return false;
|
|
4252
|
+
for (const part of f.split("/")) {
|
|
4253
|
+
if (skip.has(part))
|
|
4254
|
+
return false;
|
|
4255
|
+
}
|
|
4256
|
+
return true;
|
|
4257
|
+
});
|
|
4258
|
+
const match = allFiles.find((f) => f.toLowerCase().includes(nameLower));
|
|
4259
|
+
if (match) {
|
|
4260
|
+
matchFile = join3(cd, match).replace(dir, ".");
|
|
4261
|
+
break;
|
|
4262
|
+
}
|
|
4263
|
+
} catch (_) {}
|
|
4264
|
+
}
|
|
4265
|
+
verify += `### Entity: ${entity}
|
|
4266
|
+
`;
|
|
4267
|
+
if (matchFile) {
|
|
4268
|
+
verify += ` file: ${matchFile}
|
|
4269
|
+
`;
|
|
4270
|
+
const fullPath = join3(dir, matchFile.replace("./", ""));
|
|
4271
|
+
if (existsSync3(fullPath)) {
|
|
4272
|
+
const code = readFileSync3(fullPath, "utf-8");
|
|
4273
|
+
const codeProps = [...code.matchAll(/\b(\w+)\s*[:?]\s*\w+/g)].map((m) => m[1]).filter((v, i, a) => a.indexOf(v) === i);
|
|
4274
|
+
if (codeProps.length > 0) {
|
|
4275
|
+
verify += ` properties: [${codeProps.slice(0, 10).join(", ")}]
|
|
4276
|
+
`;
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
} else {
|
|
4280
|
+
verify += ` # no matching file found — map manually
|
|
4281
|
+
`;
|
|
4282
|
+
}
|
|
4283
|
+
verify += `
|
|
4284
|
+
`;
|
|
4285
|
+
}
|
|
4286
|
+
const updatedPlan = plan.includes("## Verify") ? plan : plan + verify;
|
|
4287
|
+
writeFileSync2(planFile, updatedPlan);
|
|
4288
|
+
console.log(source_default.green(` ${p}: Verify section generated in plan.dog`));
|
|
4289
|
+
} else {
|
|
4290
|
+
const verifyMatch = plan.match(/## Verify\n([\s\S]*?)(?=\n## |$)/);
|
|
4291
|
+
if (!verifyMatch) {
|
|
4292
|
+
console.log(source_default.yellow(` ${p}: No ## Verify section. Run: dotdog verify --init`));
|
|
4293
|
+
continue;
|
|
4294
|
+
}
|
|
4295
|
+
const verifyBlock = verifyMatch[1];
|
|
4296
|
+
const entityBlocks = [...verifyBlock.matchAll(/### Entity: (\w+)\n([\s\S]*?)(?=### Entity:|$)/g)];
|
|
4297
|
+
let checks = 0, passed = 0;
|
|
4298
|
+
for (const [, ename, ebody] of entityBlocks) {
|
|
4299
|
+
checks++;
|
|
4300
|
+
const fileMatch = ebody.match(/file:\s*(.+)/);
|
|
4301
|
+
const propMatch = ebody.match(/properties:\s*\[([^\]]+)\]/);
|
|
4302
|
+
if (!fileMatch)
|
|
4303
|
+
continue;
|
|
4304
|
+
const filePath = join3(dir, fileMatch[1].trim().replace("./", ""));
|
|
4305
|
+
if (!existsSync3(filePath)) {
|
|
4306
|
+
console.log(source_default.red(` ✗ ${ename}: file ${fileMatch[1].trim()} not found`));
|
|
4307
|
+
continue;
|
|
4308
|
+
}
|
|
4309
|
+
const code = readFileSync3(filePath, "utf-8");
|
|
4310
|
+
const cleanCode = code.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
4311
|
+
if (propMatch) {
|
|
4312
|
+
const props = propMatch[1].split(",").map((s) => s.trim());
|
|
4313
|
+
let propPass = 0;
|
|
4314
|
+
for (const prop of props) {
|
|
4315
|
+
const snakeVariant = prop.replace(/_/g, "");
|
|
4316
|
+
const camelVariant = prop.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
4317
|
+
if (cleanCode.includes(prop) || cleanCode.includes(snakeVariant) || cleanCode.includes(camelVariant)) {
|
|
4318
|
+
propPass++;
|
|
4319
|
+
} else {
|
|
4320
|
+
console.log(source_default.yellow(` ⚠ ${ename}.${prop}: not found in ${fileMatch[1].trim()}`));
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
if (propPass === props.length)
|
|
4324
|
+
passed++;
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
if (checks === 0)
|
|
4328
|
+
console.log(source_default.yellow(` ${p}: No entities mapped. Run: dotdog verify --init`));
|
|
4329
|
+
else if (passed === checks)
|
|
4330
|
+
console.log(source_default.green(` ${p}: ${passed}/${checks} entities verified`));
|
|
4331
|
+
else
|
|
4332
|
+
console.log(source_default.bold(` ${p}: ${passed}/${checks} entities verified`));
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
});
|
|
4192
4337
|
program2.command("woof").action(() => {
|
|
4193
4338
|
console.log(" / \\__");
|
|
4194
4339
|
console.log(" ( @\\___");
|
|
@@ -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.0",
|
|
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",
|