dotdog 0.2.5 → 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 +238 -16
- 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,45 +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");
|
|
3205
3211
|
for (const f of files) {
|
|
3206
3212
|
const content = readFileSync2(join2(pd, f), "utf-8");
|
|
3207
|
-
|
|
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 = [];
|
|
3222
|
+
for (const f of files) {
|
|
3223
|
+
const ast = parse(sources[f]);
|
|
3208
3224
|
for (const section of ast.sections) {
|
|
3209
3225
|
for (const block of section.blocks) {
|
|
3210
3226
|
if (block.kind === "entity") {
|
|
3211
|
-
|
|
3227
|
+
nodes.push({
|
|
3212
3228
|
id: block.name,
|
|
3213
3229
|
type: block.type,
|
|
3214
3230
|
description: block.description || "",
|
|
3215
3231
|
file: f,
|
|
3216
|
-
properties: block.properties,
|
|
3232
|
+
properties: Object.keys(block.properties).length,
|
|
3217
3233
|
states: block.states || [],
|
|
3218
|
-
lifecycle: block.lifecycle || [],
|
|
3219
3234
|
chars: section.content?.length || 0
|
|
3220
3235
|
});
|
|
3221
3236
|
}
|
|
3222
3237
|
if (block.kind === "relationship") {
|
|
3223
|
-
|
|
3238
|
+
edges.push({
|
|
3224
3239
|
source: block.source,
|
|
3225
3240
|
target: block.target,
|
|
3226
3241
|
verb: block.verb,
|
|
3227
3242
|
cardinality: block.cardinality,
|
|
3228
3243
|
required: block.required,
|
|
3229
|
-
|
|
3230
|
-
file: f,
|
|
3231
|
-
section: section.heading
|
|
3244
|
+
file: f
|
|
3232
3245
|
});
|
|
3233
3246
|
}
|
|
3234
3247
|
}
|
|
3235
3248
|
}
|
|
3236
3249
|
}
|
|
3237
|
-
|
|
3238
|
-
const
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
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)}...`));
|
|
3242
3262
|
}
|
|
3243
3263
|
}
|
|
3244
3264
|
if (!found)
|
|
@@ -3276,6 +3296,208 @@ ${out}`);
|
|
|
3276
3296
|
}
|
|
3277
3297
|
});
|
|
3278
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
|
+
});
|
|
3279
3501
|
program2.command("staleness [dir]").action((d = ".") => {
|
|
3280
3502
|
const dir = resolvePath2(d);
|
|
3281
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"
|