dotdog 0.2.4 → 0.3.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 +257 -18
- package/package.json +14 -4
package/dist/cli.js
CHANGED
|
@@ -2537,6 +2537,7 @@ var source_default = chalk;
|
|
|
2537
2537
|
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, mkdirSync, writeFileSync } from "fs";
|
|
2538
2538
|
import { join as join2 } from "path";
|
|
2539
2539
|
import { homedir as homedir2 } from "os";
|
|
2540
|
+
import { createHash } from "crypto";
|
|
2540
2541
|
|
|
2541
2542
|
// src/parser.ts
|
|
2542
2543
|
function parse(source) {
|
|
@@ -3094,7 +3095,7 @@ function parseSections2(markdown) {
|
|
|
3094
3095
|
}
|
|
3095
3096
|
var program2 = new Command;
|
|
3096
3097
|
var pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
|
|
3097
|
-
program2.name("spec").alias("dotdog").description("The spec dog — validate, analyze, generate .dog files").version(pkg.version);
|
|
3098
|
+
program2.name("spec").alias("dotdog").description("The spec dog — validate, analyze, generate, simulate .dog files").version(pkg.version);
|
|
3098
3099
|
program2.command("validate [dir]").action((d = ".") => {
|
|
3099
3100
|
const dirs = [join2(d, "projects"), join2(d, "specs")];
|
|
3100
3101
|
let found = false;
|
|
@@ -3200,28 +3201,64 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3200
3201
|
const pd = join2(dd, p, "specs");
|
|
3201
3202
|
if (!existsSync2(pd))
|
|
3202
3203
|
continue;
|
|
3203
|
-
const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
|
|
3204
|
-
|
|
3204
|
+
const files = readdirSync2(pd).filter((f) => f.endsWith(".dog")).sort();
|
|
3205
|
+
if (!files.length)
|
|
3206
|
+
continue;
|
|
3207
|
+
found = true;
|
|
3208
|
+
const sources = {};
|
|
3209
|
+
let sourceBytes = 0;
|
|
3210
|
+
const hash = createHash("sha256");
|
|
3211
|
+
for (const f of files) {
|
|
3212
|
+
const content = readFileSync2(join2(pd, f), "utf-8");
|
|
3213
|
+
sources[f] = content;
|
|
3214
|
+
sourceBytes += Buffer.byteLength(content, "utf-8");
|
|
3215
|
+
hash.update(content);
|
|
3216
|
+
hash.update(`
|
|
3217
|
+
`);
|
|
3218
|
+
}
|
|
3219
|
+
const integrity = { sha256: hash.digest("hex"), source_files: files.length, source_bytes: sourceBytes };
|
|
3220
|
+
const sourceTokens = Math.round(sourceBytes / 4);
|
|
3221
|
+
const nodes = [], edges = [];
|
|
3205
3222
|
for (const f of files) {
|
|
3206
|
-
const
|
|
3207
|
-
const
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3223
|
+
const ast = parse(sources[f]);
|
|
3224
|
+
for (const section of ast.sections) {
|
|
3225
|
+
for (const block of section.blocks) {
|
|
3226
|
+
if (block.kind === "entity") {
|
|
3227
|
+
nodes.push({
|
|
3228
|
+
id: block.name,
|
|
3229
|
+
type: block.type,
|
|
3230
|
+
description: block.description || "",
|
|
3231
|
+
file: f,
|
|
3232
|
+
properties: Object.keys(block.properties).length,
|
|
3233
|
+
states: block.states || [],
|
|
3234
|
+
chars: section.content?.length || 0
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
if (block.kind === "relationship") {
|
|
3238
|
+
edges.push({
|
|
3239
|
+
source: block.source,
|
|
3240
|
+
target: block.target,
|
|
3241
|
+
verb: block.verb,
|
|
3242
|
+
cardinality: block.cardinality,
|
|
3243
|
+
required: block.required,
|
|
3244
|
+
file: f
|
|
3245
|
+
});
|
|
3216
3246
|
}
|
|
3217
3247
|
}
|
|
3218
3248
|
}
|
|
3219
3249
|
}
|
|
3220
|
-
|
|
3221
|
-
const
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3250
|
+
const dag = { version: "1.2", project: p, compiled_at: new Date().toISOString(), compiler: `dotdog@${pkg.version}`, integrity, nodes, edges };
|
|
3251
|
+
const dagJson = JSON.stringify(dag);
|
|
3252
|
+
const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
|
|
3253
|
+
const savingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
|
|
3254
|
+
const savingsTokens = sourceTokens - dagTokens;
|
|
3255
|
+
const outPath = opts.output || join2(pd, "..", `${p}.dag`);
|
|
3256
|
+
const report = { ...dag, tokens: { source_total: sourceTokens, dag_total: dagTokens, savings_pct: savingsPct, savings_tokens: savingsTokens } };
|
|
3257
|
+
writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
3258
|
+
console.log(source_default.green(` ✓ ${outPath}`));
|
|
3259
|
+
console.log(source_default.gray(` ${nodes.length} nodes, ${edges.length} edges, ${files.length} files`));
|
|
3260
|
+
console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${savingsPct}% savings, ${savingsTokens} tokens saved)`));
|
|
3261
|
+
console.log(source_default.gray(` sha256: ${integrity.sha256.substring(0, 12)}...`));
|
|
3225
3262
|
}
|
|
3226
3263
|
}
|
|
3227
3264
|
if (!found)
|
|
@@ -3259,6 +3296,208 @@ ${out}`);
|
|
|
3259
3296
|
}
|
|
3260
3297
|
});
|
|
3261
3298
|
program2.command("serve [dir]").description("MCP server — expose .dag graph to AI agents over stdio").action((d = ".") => serve(d));
|
|
3299
|
+
program2.command("analyze [dir]").description("Analyze a spec project — score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
|
|
3300
|
+
const dir = resolvePath2(d);
|
|
3301
|
+
const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
|
|
3302
|
+
console.log(source_default.bold(`
|
|
3303
|
+
Spec Analysis
|
|
3304
|
+
`));
|
|
3305
|
+
let found = false;
|
|
3306
|
+
for (const dd of dirs) {
|
|
3307
|
+
if (!existsSync2(dd))
|
|
3308
|
+
continue;
|
|
3309
|
+
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3310
|
+
for (const p of projects) {
|
|
3311
|
+
if (opts.project && p !== opts.project)
|
|
3312
|
+
continue;
|
|
3313
|
+
const pd = join2(dd, p, "specs");
|
|
3314
|
+
if (!existsSync2(pd))
|
|
3315
|
+
continue;
|
|
3316
|
+
const files = readdirSync2(pd).filter((f) => f.endsWith(".dog"));
|
|
3317
|
+
if (!files.length)
|
|
3318
|
+
continue;
|
|
3319
|
+
found = true;
|
|
3320
|
+
console.log(source_default.bold(`
|
|
3321
|
+
${p}`));
|
|
3322
|
+
console.log(" " + "─".repeat(50));
|
|
3323
|
+
const allEntities = [];
|
|
3324
|
+
const allRelationships = [];
|
|
3325
|
+
const analyses = [];
|
|
3326
|
+
for (const f of files) {
|
|
3327
|
+
const content = readFileSync2(join2(pd, f), "utf-8");
|
|
3328
|
+
const ast = parse(content);
|
|
3329
|
+
const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
|
|
3330
|
+
const rels = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "relationship"));
|
|
3331
|
+
allEntities.push(...entities);
|
|
3332
|
+
allRelationships.push(...rels);
|
|
3333
|
+
analyses.push({ file: f, sections: ast.sections.length, size: content.length, entities: entities.length, rels: rels.length });
|
|
3334
|
+
}
|
|
3335
|
+
const missingReq = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
|
|
3336
|
+
const missingOpt = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
|
|
3337
|
+
let score = 100 - missingReq.length * 15 - missingOpt.length * 5;
|
|
3338
|
+
const noDesc = allEntities.filter((e) => !e.description || e.description.length < 10).length;
|
|
3339
|
+
score = Math.max(0, score - noDesc * 3);
|
|
3340
|
+
const noProps = allEntities.filter((e) => Object.keys(e.properties).length === 0).length;
|
|
3341
|
+
score = Math.max(0, score - noProps * 5);
|
|
3342
|
+
const noStates = allEntities.filter((e) => e.states.length === 0).length;
|
|
3343
|
+
score = Math.max(0, score - noStates * 3);
|
|
3344
|
+
console.log(` ${files.length} files | ${score}% complete`);
|
|
3345
|
+
for (const a of analyses) {
|
|
3346
|
+
const detail = a.entities > 0 ? ` (${a.entities} entities, ${a.rels} rels)` : "";
|
|
3347
|
+
console.log(source_default.gray(` ${a.file} — ${a.sections} sections, ${(a.size / 1024).toFixed(1)}KB${detail}`));
|
|
3348
|
+
}
|
|
3349
|
+
const gaps = [];
|
|
3350
|
+
for (const f of missingReq)
|
|
3351
|
+
gaps.push(`\uD83D\uDD34 ${f}: Missing required file`);
|
|
3352
|
+
for (const f of missingOpt)
|
|
3353
|
+
gaps.push(`\uD83D\uDFE1 ${f}: Missing optional file`);
|
|
3354
|
+
const entityNames = new Set(allEntities.map((e) => e.name));
|
|
3355
|
+
for (const e of allEntities) {
|
|
3356
|
+
if (!e.description || e.description.length < 10)
|
|
3357
|
+
gaps.push(`\uD83D\uDFE1 ${e.name}: No description`);
|
|
3358
|
+
if (Object.keys(e.properties).length === 0)
|
|
3359
|
+
gaps.push(`\uD83D\uDFE1 ${e.name}: No properties defined`);
|
|
3360
|
+
if (e.states.length === 0)
|
|
3361
|
+
gaps.push(`\uD83D\uDD35 ${e.name}: No states defined`);
|
|
3362
|
+
}
|
|
3363
|
+
for (const r of allRelationships) {
|
|
3364
|
+
if (r.source && !entityNames.has(r.source))
|
|
3365
|
+
gaps.push(`\uD83D\uDFE1 Relationship: unknown source "${r.source}"`);
|
|
3366
|
+
if (r.target && !entityNames.has(r.target))
|
|
3367
|
+
gaps.push(`\uD83D\uDFE1 Relationship: unknown target "${r.target}"`);
|
|
3368
|
+
}
|
|
3369
|
+
if (gaps.length > 0) {
|
|
3370
|
+
console.log(source_default.bold(`
|
|
3371
|
+
Gaps (${gaps.length})`));
|
|
3372
|
+
for (const g of gaps)
|
|
3373
|
+
console.log(` ${g}`);
|
|
3374
|
+
} else
|
|
3375
|
+
console.log(source_default.green(`
|
|
3376
|
+
No gaps found.`));
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
if (!found)
|
|
3380
|
+
console.log(source_default.yellow("No spec projects found. Run: dotdog init <project>"));
|
|
3381
|
+
});
|
|
3382
|
+
program2.command("generate [dir]").description("Generate missing spec files from SPEC.dog").option("-p, --project <name>").action((d = ".", opts) => {
|
|
3383
|
+
const dir = resolvePath2(d);
|
|
3384
|
+
const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
|
|
3385
|
+
let specContent = "", specDir = "";
|
|
3386
|
+
for (const dd of dirs) {
|
|
3387
|
+
if (!existsSync2(dd))
|
|
3388
|
+
continue;
|
|
3389
|
+
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3390
|
+
for (const p of projects) {
|
|
3391
|
+
if (opts.project && p !== opts.project)
|
|
3392
|
+
continue;
|
|
3393
|
+
const pd = join2(dd, p, "specs");
|
|
3394
|
+
const sp = join2(pd, "SPEC.dog");
|
|
3395
|
+
if (existsSync2(sp)) {
|
|
3396
|
+
specContent = readFileSync2(sp, "utf-8");
|
|
3397
|
+
specDir = pd;
|
|
3398
|
+
break;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
if (specContent)
|
|
3402
|
+
break;
|
|
3403
|
+
}
|
|
3404
|
+
if (!specContent) {
|
|
3405
|
+
console.log(source_default.red("No SPEC.dog found. Create one first."));
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
console.log(source_default.bold(`
|
|
3409
|
+
Spec Generator
|
|
3410
|
+
`));
|
|
3411
|
+
console.log(source_default.gray(` Source: ${specDir}/SPEC.dog
|
|
3412
|
+
`));
|
|
3413
|
+
const ast = parse(specContent);
|
|
3414
|
+
const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
|
|
3415
|
+
const uiStrings = [];
|
|
3416
|
+
for (const section of ast.sections) {
|
|
3417
|
+
const h = section.heading.toLowerCase();
|
|
3418
|
+
if (h.includes("what the user sees") || h.includes("screen")) {
|
|
3419
|
+
const text = section.blocks.filter((b) => b.kind === "prose").map((b) => b.content).join(`
|
|
3420
|
+
`);
|
|
3421
|
+
for (const m of text.match(/\[([^\]]+)\]/g) || [])
|
|
3422
|
+
uiStrings.push({ screen: section.heading, element: "button", text: m });
|
|
3423
|
+
for (const m of text.match(/"([^"]+)"/g) || [])
|
|
3424
|
+
uiStrings.push({ screen: section.heading, element: "label", text: m });
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
if (!existsSync2(join2(specDir, "data-model.dog")) && entities.length > 0) {
|
|
3428
|
+
let dm = `# Data Model
|
|
3429
|
+
|
|
3430
|
+
## Core Entities
|
|
3431
|
+
|
|
3432
|
+
`;
|
|
3433
|
+
for (const e of entities) {
|
|
3434
|
+
dm += `### Entity: ${e.name}
|
|
3435
|
+
|
|
3436
|
+
${e.description || "No description."}
|
|
3437
|
+
|
|
3438
|
+
`;
|
|
3439
|
+
dm += "```yaml\n";
|
|
3440
|
+
dm += `entity: ${e.name}
|
|
3441
|
+
`;
|
|
3442
|
+
dm += `type: entity
|
|
3443
|
+
`;
|
|
3444
|
+
dm += `properties:
|
|
3445
|
+
`;
|
|
3446
|
+
for (const [k, v] of Object.entries(e.properties)) {
|
|
3447
|
+
dm += ` ${k}:
|
|
3448
|
+
`;
|
|
3449
|
+
dm += ` type: ${v.type}
|
|
3450
|
+
`;
|
|
3451
|
+
if (v.required)
|
|
3452
|
+
dm += ` required: true
|
|
3453
|
+
`;
|
|
3454
|
+
}
|
|
3455
|
+
if (e.states.length > 0)
|
|
3456
|
+
dm += `states: [${e.states.join(", ")}]
|
|
3457
|
+
`;
|
|
3458
|
+
dm += "```\n\n";
|
|
3459
|
+
}
|
|
3460
|
+
writeFileSync(join2(specDir, "data-model.dog"), dm);
|
|
3461
|
+
console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
|
|
3462
|
+
}
|
|
3463
|
+
if (!existsSync2(join2(specDir, "COPY.dog")) && uiStrings.length > 0) {
|
|
3464
|
+
let copy = `# App Copy
|
|
3465
|
+
|
|
3466
|
+
| Screen | Element | Copy |
|
|
3467
|
+
|--------|---------|------|
|
|
3468
|
+
`;
|
|
3469
|
+
for (const s of uiStrings)
|
|
3470
|
+
copy += `| ${s.screen} | ${s.element} | ${s.text} |
|
|
3471
|
+
`;
|
|
3472
|
+
writeFileSync(join2(specDir, "COPY.dog"), copy);
|
|
3473
|
+
console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
|
|
3474
|
+
}
|
|
3475
|
+
if (!existsSync2(join2(specDir, "INDEX.dog"))) {
|
|
3476
|
+
let idx = `# INDEX
|
|
3477
|
+
|
|
3478
|
+
| You are... | Start here | Then... |
|
|
3479
|
+
|------------|-----------|---------|
|
|
3480
|
+
`;
|
|
3481
|
+
idx += `| Developer | SPEC.dog | data-model.dog → plan.dog |
|
|
3482
|
+
`;
|
|
3483
|
+
idx += `| AI agent | data-model.dog | COPY.dog → SPEC.dog |
|
|
3484
|
+
`;
|
|
3485
|
+
idx += `| Designer | SPEC.dog | COPY.dog |
|
|
3486
|
+
`;
|
|
3487
|
+
writeFileSync(join2(specDir, "INDEX.dog"), idx);
|
|
3488
|
+
console.log(source_default.green(" ✓ INDEX.dog"));
|
|
3489
|
+
}
|
|
3490
|
+
console.log(source_default.bold(`
|
|
3491
|
+
Run dotdog validate to verify.
|
|
3492
|
+
`));
|
|
3493
|
+
});
|
|
3494
|
+
program2.command("simulate <scenario>").description("Run a simulation scenario (phase 1 stub)").option("-p, --project <name>", "Project name", "default").action((scenario, opts) => {
|
|
3495
|
+
console.log(source_default.bold(`
|
|
3496
|
+
Simulation: ${scenario} (project: ${opts.project})
|
|
3497
|
+
`));
|
|
3498
|
+
console.log(source_default.gray("Simulation engine — reads SPEC.dog scenarios, walks through steps, checks pre/postconditions."));
|
|
3499
|
+
console.log(source_default.gray("Full engine coming in a future release."));
|
|
3500
|
+
});
|
|
3262
3501
|
program2.command("staleness [dir]").action((d = ".") => {
|
|
3263
3502
|
const dir = resolvePath2(d);
|
|
3264
3503
|
const dirs = [join2(dir, "projects"), join2(dir, "specs"), dir];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotdog",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The spec dog
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The spec dog — structured, AI-queryable software specifications. Write .dog specs, compile to .dag graphs, query via MCP. Built for humans and AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
7
7
|
"bin": {
|
|
@@ -16,16 +16,26 @@
|
|
|
16
16
|
],
|
|
17
17
|
"keywords": [
|
|
18
18
|
"spec",
|
|
19
|
-
"dogfood",
|
|
20
19
|
"specification",
|
|
21
20
|
"ai",
|
|
21
|
+
"agent",
|
|
22
|
+
"mcp",
|
|
22
23
|
"documentation",
|
|
24
|
+
"cli",
|
|
25
|
+
"spec-driven-development",
|
|
26
|
+
"graph",
|
|
27
|
+
"markdown",
|
|
28
|
+
"yaml",
|
|
23
29
|
"dotdog",
|
|
30
|
+
"dag",
|
|
24
31
|
"dog"
|
|
25
32
|
],
|
|
26
33
|
"license": "MIT",
|
|
27
34
|
"author": "specdog",
|
|
28
|
-
"repository":
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/specdog/dotdog.git"
|
|
38
|
+
},
|
|
29
39
|
"dependencies": {
|
|
30
40
|
"commander": "^15.0.0",
|
|
31
41
|
"chalk": "^5.6.0"
|