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.
Files changed (2) hide show
  1. package/dist/cli.js +238 -16
  2. 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
- const dag = { version: "1.1", project: p, compiled_at: new Date().toISOString(), nodes: [], edges: [], files: files.length };
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
- const ast = parse(content);
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
- dag.nodes.push({
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
- dag.edges.push({
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
- cascade: block.cascade,
3230
- file: f,
3231
- section: section.heading
3244
+ file: f
3232
3245
  });
3233
3246
  }
3234
3247
  }
3235
3248
  }
3236
3249
  }
3237
- found = true;
3238
- const out = opts.output || join2(pd, "..", `${p}.dag`);
3239
- writeFileSync(out, JSON.stringify(dag, null, 2));
3240
- console.log(source_default.green(` ✓ ${out}`));
3241
- console.log(source_default.gray(` ${dag.nodes.length} nodes, ${dag.edges.length} edges, ${dag.files} files`));
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.2.5",
4
- "description": "The spec dog \u2014 validate, analyze, parse, and generate .dog spec genome files",
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": "github:specdog/dotdog",
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"