dotdog 0.3.4 → 0.3.6
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 +679 -111
- package/kits/defi/SPEC.dog +21 -0
- package/kits/defi/constitution.dog +8 -0
- package/kits/defi/data-model.dog +131 -0
- package/kits/erc20/SPEC.dog +20 -0
- package/kits/erc20/constitution.dog +8 -0
- package/kits/erc20/data-model.dog +97 -0
- package/kits/hackathon/SPEC.dog +20 -0
- package/kits/hackathon/constitution.dog +8 -0
- package/kits/hackathon/data-model.dog +64 -0
- package/kits/nft/SPEC.dog +21 -0
- package/kits/nft/constitution.dog +8 -0
- package/kits/nft/data-model.dog +131 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -2534,8 +2534,150 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
|
2534
2534
|
var source_default = chalk;
|
|
2535
2535
|
|
|
2536
2536
|
// src/cli.ts
|
|
2537
|
-
import { existsSync as
|
|
2538
|
-
import { join as
|
|
2537
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2538
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
2539
|
+
|
|
2540
|
+
// src/index.ts
|
|
2541
|
+
import { readFileSync, readdirSync } from "fs";
|
|
2542
|
+
import { join } from "path";
|
|
2543
|
+
function tokenize(text) {
|
|
2544
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2 && !stopWords.has(t));
|
|
2545
|
+
}
|
|
2546
|
+
var stopWords = new Set([
|
|
2547
|
+
"the",
|
|
2548
|
+
"and",
|
|
2549
|
+
"for",
|
|
2550
|
+
"are",
|
|
2551
|
+
"but",
|
|
2552
|
+
"not",
|
|
2553
|
+
"you",
|
|
2554
|
+
"all",
|
|
2555
|
+
"can",
|
|
2556
|
+
"had",
|
|
2557
|
+
"her",
|
|
2558
|
+
"was",
|
|
2559
|
+
"one",
|
|
2560
|
+
"our",
|
|
2561
|
+
"out",
|
|
2562
|
+
"has",
|
|
2563
|
+
"have",
|
|
2564
|
+
"been",
|
|
2565
|
+
"some",
|
|
2566
|
+
"than",
|
|
2567
|
+
"that",
|
|
2568
|
+
"this",
|
|
2569
|
+
"with",
|
|
2570
|
+
"from",
|
|
2571
|
+
"they",
|
|
2572
|
+
"will",
|
|
2573
|
+
"would",
|
|
2574
|
+
"which",
|
|
2575
|
+
"their",
|
|
2576
|
+
"there",
|
|
2577
|
+
"about",
|
|
2578
|
+
"into",
|
|
2579
|
+
"each",
|
|
2580
|
+
"said",
|
|
2581
|
+
"does",
|
|
2582
|
+
"also",
|
|
2583
|
+
"after",
|
|
2584
|
+
"before",
|
|
2585
|
+
"other",
|
|
2586
|
+
"more",
|
|
2587
|
+
"only",
|
|
2588
|
+
"over",
|
|
2589
|
+
"such",
|
|
2590
|
+
"when",
|
|
2591
|
+
"where",
|
|
2592
|
+
"what",
|
|
2593
|
+
"who",
|
|
2594
|
+
"how",
|
|
2595
|
+
"then",
|
|
2596
|
+
"just",
|
|
2597
|
+
"very",
|
|
2598
|
+
"much",
|
|
2599
|
+
"well",
|
|
2600
|
+
"should",
|
|
2601
|
+
"could",
|
|
2602
|
+
"through",
|
|
2603
|
+
"between"
|
|
2604
|
+
]);
|
|
2605
|
+
function buildIndex(projectDir, projectName) {
|
|
2606
|
+
const files = readdirSync(projectDir).filter((f) => f.endsWith(".dog"));
|
|
2607
|
+
const entries = [];
|
|
2608
|
+
const docFreq = new Map;
|
|
2609
|
+
for (const file of files) {
|
|
2610
|
+
const content = readFileSync(join(projectDir, file), "utf-8");
|
|
2611
|
+
const sections = content.split(/\n(?=##\s)/);
|
|
2612
|
+
for (const section of sections) {
|
|
2613
|
+
const lines = section.split(`
|
|
2614
|
+
`);
|
|
2615
|
+
const heading = lines[0]?.replace(/^#+\s*/, "") || "(root)";
|
|
2616
|
+
const body = lines.slice(1).join(`
|
|
2617
|
+
`);
|
|
2618
|
+
if (body.trim().length < 20)
|
|
2619
|
+
continue;
|
|
2620
|
+
const terms = tokenize(body);
|
|
2621
|
+
if (terms.length < 5)
|
|
2622
|
+
continue;
|
|
2623
|
+
const tf = new Map;
|
|
2624
|
+
for (const t of terms) {
|
|
2625
|
+
tf.set(t, (tf.get(t) || 0) + 1);
|
|
2626
|
+
docFreq.set(t, (docFreq.get(t) || 0) + 1);
|
|
2627
|
+
}
|
|
2628
|
+
entries.push({
|
|
2629
|
+
section: heading,
|
|
2630
|
+
heading,
|
|
2631
|
+
file,
|
|
2632
|
+
content: body.slice(0, 500),
|
|
2633
|
+
vector: []
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
const vocabulary = [...docFreq.entries()].filter(([_, df2]) => df2 >= 2).sort((a, b) => b[1] - a[1]).slice(0, 1000).map(([term]) => term);
|
|
2638
|
+
const df = vocabulary.map((t) => docFreq.get(t) || 0);
|
|
2639
|
+
const N = entries.length;
|
|
2640
|
+
for (const entry of entries) {
|
|
2641
|
+
const terms = tokenize(entry.content);
|
|
2642
|
+
const tf = new Map;
|
|
2643
|
+
for (const t of terms)
|
|
2644
|
+
tf.set(t, (tf.get(t) || 0) + 1);
|
|
2645
|
+
entry.vector = vocabulary.map((term) => {
|
|
2646
|
+
const termFreq = tf.get(term) || 0;
|
|
2647
|
+
if (termFreq === 0)
|
|
2648
|
+
return 0;
|
|
2649
|
+
const docFreqTerm = docFreq.get(term) || 1;
|
|
2650
|
+
return termFreq / terms.length * Math.log(N / docFreqTerm);
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
return {
|
|
2654
|
+
version: "1.0",
|
|
2655
|
+
project: projectName,
|
|
2656
|
+
built: new Date().toISOString(),
|
|
2657
|
+
entries,
|
|
2658
|
+
vocabulary,
|
|
2659
|
+
df
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
function searchIndex(index, query, limit = 10) {
|
|
2663
|
+
const queryTerms = tokenize(query);
|
|
2664
|
+
const queryVec = index.vocabulary.map((term) => queryTerms.includes(term) ? 1 : 0);
|
|
2665
|
+
const results = index.entries.map((entry) => {
|
|
2666
|
+
let dot = 0;
|
|
2667
|
+
let entryMag = 0;
|
|
2668
|
+
let queryMag = 0;
|
|
2669
|
+
for (let j = 0;j < queryVec.length; j++) {
|
|
2670
|
+
dot += queryVec[j] * entry.vector[j];
|
|
2671
|
+
queryMag += queryVec[j] * queryVec[j];
|
|
2672
|
+
entryMag += entry.vector[j] * entry.vector[j];
|
|
2673
|
+
}
|
|
2674
|
+
const score = queryMag > 0 && entryMag > 0 ? dot / (Math.sqrt(queryMag) * Math.sqrt(entryMag)) : 0;
|
|
2675
|
+
return { entry, score };
|
|
2676
|
+
});
|
|
2677
|
+
return results.filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
// src/cli.ts
|
|
2539
2681
|
import { homedir as homedir2 } from "os";
|
|
2540
2682
|
|
|
2541
2683
|
// src/parser.ts
|
|
@@ -2809,12 +2951,11 @@ function buildPredictionNode(name, description, yaml, lineStart, lineEnd) {
|
|
|
2809
2951
|
description,
|
|
2810
2952
|
trigger: yaml.trigger || "",
|
|
2811
2953
|
timeframe: yaml.timeframe || "",
|
|
2812
|
-
confidence:
|
|
2954
|
+
confidence: yaml.confidence || 0,
|
|
2813
2955
|
measurement: yaml.measurement || "",
|
|
2814
|
-
|
|
2815
|
-
postconditions: Array.isArray(yaml.postconditions) ? yaml.postconditions : [],
|
|
2956
|
+
status: yaml.status || "pending",
|
|
2816
2957
|
yaml,
|
|
2817
|
-
lineStart
|
|
2958
|
+
lineStart,
|
|
2818
2959
|
lineEnd
|
|
2819
2960
|
};
|
|
2820
2961
|
}
|
|
@@ -2946,14 +3087,14 @@ function parseInlineObject(value) {
|
|
|
2946
3087
|
return obj;
|
|
2947
3088
|
}
|
|
2948
3089
|
// src/serve.ts
|
|
2949
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
2950
|
-
import { join, resolve } from "path";
|
|
3090
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
3091
|
+
import { join as join2, resolve } from "path";
|
|
2951
3092
|
import { homedir } from "os";
|
|
2952
3093
|
import * as readline from "readline";
|
|
2953
3094
|
function resolvePath(p) {
|
|
2954
3095
|
if (p.startsWith("~"))
|
|
2955
|
-
p =
|
|
2956
|
-
const resolved = p.startsWith("/") ? p :
|
|
3096
|
+
p = join2(homedir(), p.slice(1));
|
|
3097
|
+
const resolved = p.startsWith("/") ? p : join2(process.cwd(), p);
|
|
2957
3098
|
if (!p.startsWith("/") && !p.startsWith("~")) {
|
|
2958
3099
|
const rel = resolve(process.cwd(), p);
|
|
2959
3100
|
const cwd = process.cwd();
|
|
@@ -2968,7 +3109,25 @@ function resolvePath(p) {
|
|
|
2968
3109
|
return resolved;
|
|
2969
3110
|
}
|
|
2970
3111
|
var N = (dag) => dag.n || dag.nodes || [];
|
|
2971
|
-
|
|
3112
|
+
function nodeEdges(n) {
|
|
3113
|
+
return n.es || [];
|
|
3114
|
+
}
|
|
3115
|
+
function E(dag) {
|
|
3116
|
+
if (dag.e)
|
|
3117
|
+
return dag.e;
|
|
3118
|
+
const edges = [];
|
|
3119
|
+
const seen = new Set;
|
|
3120
|
+
for (const node of N(dag)) {
|
|
3121
|
+
for (const e of nodeEdges(node)) {
|
|
3122
|
+
const key = `${node.i || node.id}→${e.t}:${e.v}`;
|
|
3123
|
+
if (!seen.has(key)) {
|
|
3124
|
+
seen.add(key);
|
|
3125
|
+
edges.push({ s: node.i || node.id, t: e.t, v: e.v, d: e.d, c: e.c, r: e.r });
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return edges;
|
|
3130
|
+
}
|
|
2972
3131
|
var P = (dag) => dag.p || dag.project || "";
|
|
2973
3132
|
var ni = (n) => n.i || n.id || "";
|
|
2974
3133
|
var nt = (n) => n.t || n.type || "";
|
|
@@ -2981,15 +3140,15 @@ function serve(dir = ".") {
|
|
|
2981
3140
|
const root = resolvePath(dir);
|
|
2982
3141
|
const dagCache = new Map;
|
|
2983
3142
|
function loadDags() {
|
|
2984
|
-
const dirs = [
|
|
3143
|
+
const dirs = [join2(root, "projects"), join2(root, "specs"), root];
|
|
2985
3144
|
for (const dd of dirs) {
|
|
2986
|
-
if (!
|
|
3145
|
+
if (!existsSync2(dd))
|
|
2987
3146
|
continue;
|
|
2988
|
-
const projects =
|
|
3147
|
+
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2989
3148
|
for (const p of projects) {
|
|
2990
|
-
const dagFile =
|
|
2991
|
-
if (
|
|
2992
|
-
dagCache.set(p, JSON.parse(
|
|
3149
|
+
const dagFile = join2(dd, p, `${p}.dag`);
|
|
3150
|
+
if (existsSync2(dagFile)) {
|
|
3151
|
+
dagCache.set(p, JSON.parse(readFileSync2(dagFile, "utf-8")));
|
|
2993
3152
|
}
|
|
2994
3153
|
}
|
|
2995
3154
|
}
|
|
@@ -3096,6 +3255,8 @@ function serve(dir = ".") {
|
|
|
3096
3255
|
nodes: N(dag).length,
|
|
3097
3256
|
edges: E(dag).length,
|
|
3098
3257
|
version: dag.v || dag.version || "",
|
|
3258
|
+
order: dag.o || [],
|
|
3259
|
+
cycles: dag.cy !== undefined ? dag.cy : null,
|
|
3099
3260
|
savings: tk.sv || tk.savings_pct || 0,
|
|
3100
3261
|
method: tk.m || tk.method || ""
|
|
3101
3262
|
};
|
|
@@ -3138,8 +3299,8 @@ function serve(dir = ".") {
|
|
|
3138
3299
|
// src/cli.ts
|
|
3139
3300
|
function resolvePath2(p) {
|
|
3140
3301
|
if (p.startsWith("~"))
|
|
3141
|
-
p =
|
|
3142
|
-
const resolved = p.startsWith("/") ? p :
|
|
3302
|
+
p = join3(homedir2(), p.slice(1));
|
|
3303
|
+
const resolved = p.startsWith("/") ? p : join3(process.cwd(), p);
|
|
3143
3304
|
if (!p.startsWith("/") && !p.startsWith("~")) {
|
|
3144
3305
|
const rel = resolve2(process.cwd(), p);
|
|
3145
3306
|
const cwd = process.cwd();
|
|
@@ -3185,39 +3346,43 @@ function parseSections2(markdown) {
|
|
|
3185
3346
|
return sections;
|
|
3186
3347
|
}
|
|
3187
3348
|
var program2 = new Command;
|
|
3188
|
-
var pkg = JSON.parse(
|
|
3349
|
+
var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf-8"));
|
|
3189
3350
|
program2.name("spec").alias("dotdog").description("CLI for structured software specs : validate .dog, compile .dag, query via MCP").version(pkg.version);
|
|
3190
3351
|
program2.command("validate [dir]").action((d = ".") => {
|
|
3191
3352
|
const dir = resolvePath2(d);
|
|
3192
|
-
const dirs = [
|
|
3193
|
-
let found = false;
|
|
3353
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs")];
|
|
3354
|
+
let found = false, hasErrors = false;
|
|
3194
3355
|
for (const dd of dirs) {
|
|
3195
|
-
if (!
|
|
3356
|
+
if (!existsSync3(dd))
|
|
3196
3357
|
continue;
|
|
3197
|
-
const projects =
|
|
3358
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3198
3359
|
for (const p of projects) {
|
|
3199
3360
|
found = true;
|
|
3200
|
-
const pd =
|
|
3201
|
-
if (!
|
|
3361
|
+
const pd = join3(dd, p);
|
|
3362
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3202
3363
|
continue;
|
|
3203
|
-
const files =
|
|
3364
|
+
const files = existsSync3(pd) ? readdirSync3(pd).filter((f) => f.endsWith(".dog")) : [];
|
|
3204
3365
|
const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
|
|
3205
3366
|
const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
|
|
3206
3367
|
console.log(source_default.bold(`
|
|
3207
3368
|
${p} : ${files.length} .dog files, ${100 - Math.round((missing.length * 3 + optional.length) / 20 * 100)}% complete`));
|
|
3208
3369
|
for (const f of files)
|
|
3209
3370
|
console.log(source_default.gray(` ${f}`));
|
|
3210
|
-
if (missing.length)
|
|
3371
|
+
if (missing.length) {
|
|
3211
3372
|
console.log(source_default.red(` Missing required: ${missing.join(", ")}`));
|
|
3373
|
+
hasErrors = true;
|
|
3374
|
+
}
|
|
3212
3375
|
if (optional.length)
|
|
3213
3376
|
console.log(source_default.yellow(` Missing optional: ${optional.join(", ")}`));
|
|
3214
3377
|
}
|
|
3215
3378
|
}
|
|
3216
3379
|
if (!found)
|
|
3217
3380
|
console.log(source_default.yellow("No projects found. Run: spec init <project>"));
|
|
3381
|
+
if (hasErrors)
|
|
3382
|
+
process.exit(1);
|
|
3218
3383
|
});
|
|
3219
3384
|
program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data-model.dog").action((p, opts) => {
|
|
3220
|
-
const d =
|
|
3385
|
+
const d = join3(process.cwd(), "specs", p);
|
|
3221
3386
|
mkdirSync(d, { recursive: true });
|
|
3222
3387
|
const full = {
|
|
3223
3388
|
"SPEC.dog": `# Project
|
|
@@ -3265,7 +3430,7 @@ program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data
|
|
|
3265
3430
|
};
|
|
3266
3431
|
const tmpl = opts.minimal ? minimal : full;
|
|
3267
3432
|
for (const [f, c] of Object.entries(tmpl)) {
|
|
3268
|
-
|
|
3433
|
+
writeFileSync2(join3(d, f), c);
|
|
3269
3434
|
console.log(source_default.green(` ✓ ${f}`));
|
|
3270
3435
|
}
|
|
3271
3436
|
console.log(source_default.bold(`
|
|
@@ -3273,23 +3438,23 @@ Project "${p}" initialized. Fill in SPEC.dog then run spec validate.`));
|
|
|
3273
3438
|
});
|
|
3274
3439
|
program2.command("list").action(() => {
|
|
3275
3440
|
for (const d of ["projects", "specs"]) {
|
|
3276
|
-
const dd =
|
|
3277
|
-
if (!
|
|
3441
|
+
const dd = join3(process.cwd(), d);
|
|
3442
|
+
if (!existsSync3(dd))
|
|
3278
3443
|
continue;
|
|
3279
|
-
const projects =
|
|
3444
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3280
3445
|
if (!projects.length)
|
|
3281
3446
|
continue;
|
|
3282
3447
|
console.log(source_default.bold(`
|
|
3283
3448
|
${d}/`));
|
|
3284
3449
|
for (const p of projects) {
|
|
3285
|
-
const sp =
|
|
3286
|
-
const n =
|
|
3450
|
+
const sp = join3(dd, p);
|
|
3451
|
+
const n = existsSync3(sp) ? readdirSync3(sp).filter((f) => f.endsWith(".dog")).length : 0;
|
|
3287
3452
|
console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
|
|
3288
3453
|
}
|
|
3289
3454
|
}
|
|
3290
3455
|
});
|
|
3291
3456
|
program2.command("parse <file>").action((f) => {
|
|
3292
|
-
const c =
|
|
3457
|
+
const c = readFileSync3(f, "utf-8");
|
|
3293
3458
|
const s = parseSections2(c);
|
|
3294
3459
|
console.log(source_default.bold(`
|
|
3295
3460
|
${s.length} sections`));
|
|
@@ -3298,26 +3463,26 @@ ${s.length} sections`));
|
|
|
3298
3463
|
});
|
|
3299
3464
|
program2.command("compile [dir]").option("-o, --output <file>").action((d = ".", opts) => {
|
|
3300
3465
|
const dir = resolvePath2(d);
|
|
3301
|
-
const dirs = [
|
|
3466
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3302
3467
|
let found = false;
|
|
3303
3468
|
for (const dd of dirs) {
|
|
3304
|
-
if (!
|
|
3469
|
+
if (!existsSync3(dd))
|
|
3305
3470
|
continue;
|
|
3306
|
-
const projects =
|
|
3471
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3307
3472
|
for (const p of projects) {
|
|
3308
|
-
const pd =
|
|
3309
|
-
if (!
|
|
3473
|
+
const pd = join3(dd, p);
|
|
3474
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3310
3475
|
continue;
|
|
3311
|
-
if (!
|
|
3476
|
+
if (!existsSync3(pd))
|
|
3312
3477
|
continue;
|
|
3313
|
-
const files =
|
|
3478
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog")).sort();
|
|
3314
3479
|
if (!files.length)
|
|
3315
3480
|
continue;
|
|
3316
3481
|
found = true;
|
|
3317
3482
|
const sources = {};
|
|
3318
3483
|
let sourceBytes = 0, contentBytes = 0;
|
|
3319
3484
|
for (const f of files) {
|
|
3320
|
-
const content =
|
|
3485
|
+
const content = readFileSync3(join3(pd, f), "utf-8");
|
|
3321
3486
|
sources[f] = content;
|
|
3322
3487
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
3323
3488
|
sourceBytes += bytes;
|
|
@@ -3390,16 +3555,53 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3390
3555
|
}
|
|
3391
3556
|
}
|
|
3392
3557
|
}
|
|
3393
|
-
const
|
|
3558
|
+
const nodeIds = new Map;
|
|
3559
|
+
nodes.forEach((n, i) => nodeIds.set(n.i, i));
|
|
3560
|
+
const inDegree = new Array(nodes.length).fill(0);
|
|
3561
|
+
const adj = new Array(nodes.length).fill(0).map(() => []);
|
|
3562
|
+
for (const e of edges) {
|
|
3563
|
+
const si = nodeIds.get(e.s), ti = nodeIds.get(e.t);
|
|
3564
|
+
if (si !== undefined && ti !== undefined) {
|
|
3565
|
+
adj[si].push(ti);
|
|
3566
|
+
inDegree[ti]++;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
const queue = [];
|
|
3570
|
+
for (let j = 0;j < nodes.length; j++)
|
|
3571
|
+
if (inDegree[j] === 0)
|
|
3572
|
+
queue.push(j);
|
|
3573
|
+
const order = [];
|
|
3574
|
+
while (queue.length > 0) {
|
|
3575
|
+
const u = queue.shift();
|
|
3576
|
+
order.push(nodes[u].i);
|
|
3577
|
+
for (const v of adj[u]) {
|
|
3578
|
+
inDegree[v]--;
|
|
3579
|
+
if (inDegree[v] === 0)
|
|
3580
|
+
queue.push(v);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
const cycles = order.length !== nodes.length;
|
|
3584
|
+
for (let j = 0;j < nodes.length; j++) {
|
|
3585
|
+
const id = nodes[j].i;
|
|
3586
|
+
nodes[j].es = edges.filter((e) => e.s === id || e.t === id).map((e) => ({
|
|
3587
|
+
t: e.s === id ? e.t : e.s,
|
|
3588
|
+
v: e.v || "",
|
|
3589
|
+
d: e.d || "",
|
|
3590
|
+
c: e.c,
|
|
3591
|
+
r: e.r,
|
|
3592
|
+
dir: e.s === id ? "out" : "in"
|
|
3593
|
+
}));
|
|
3594
|
+
}
|
|
3595
|
+
const dag = { v: "1.5", p, c: `dotdog@${pkg.version}`, n: nodes, o: order, cy: cycles };
|
|
3394
3596
|
const dagJson = JSON.stringify(dag);
|
|
3395
3597
|
const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
|
|
3396
3598
|
const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
|
|
3397
3599
|
const contentSavingsPct = contentTokens > 0 ? Math.round((1 - dagTokens / contentTokens) * 1000) / 10 : 0;
|
|
3398
3600
|
const savingsTokens = sourceTokens - dagTokens;
|
|
3399
|
-
const outPath = opts.output ||
|
|
3601
|
+
const outPath = opts.output || join3(pd, `${p}.dag`);
|
|
3400
3602
|
const tokens = { m: "chars/4", st: sourceTokens, ct: contentTokens, dt: dagTokens, sv: allSavingsPct, cs: contentSavingsPct, saved: savingsTokens };
|
|
3401
3603
|
const report = { ...dag, tk: tokens };
|
|
3402
|
-
|
|
3604
|
+
writeFileSync2(outPath, JSON.stringify(report, null, 2));
|
|
3403
3605
|
console.log(source_default.green(` ✓ ${outPath}`));
|
|
3404
3606
|
console.log(source_default.gray(` ${nodes.length} nodes, ${edges.length} edges, ${files.length} files`));
|
|
3405
3607
|
console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${allSavingsPct}% savings, ${contentSavingsPct}% content-only, ${savingsTokens} saved)`));
|
|
@@ -3410,29 +3612,29 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3410
3612
|
});
|
|
3411
3613
|
program2.command("tokens [dir]").action((d = ".") => {
|
|
3412
3614
|
const dir = resolvePath2(d);
|
|
3413
|
-
const dirs = [
|
|
3615
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3414
3616
|
let found = false;
|
|
3415
3617
|
for (const dd of dirs) {
|
|
3416
|
-
if (!
|
|
3618
|
+
if (!existsSync3(dd))
|
|
3417
3619
|
continue;
|
|
3418
|
-
const projects =
|
|
3620
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3419
3621
|
for (const p of projects) {
|
|
3420
|
-
const pd =
|
|
3421
|
-
if (!
|
|
3622
|
+
const pd = join3(dd, p);
|
|
3623
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3422
3624
|
continue;
|
|
3423
|
-
const dagFile =
|
|
3424
|
-
if (!
|
|
3625
|
+
const dagFile = join3(pd, `${p}.dag`);
|
|
3626
|
+
if (!existsSync3(dagFile))
|
|
3425
3627
|
continue;
|
|
3426
3628
|
found = true;
|
|
3427
|
-
const dogFiles =
|
|
3629
|
+
const dogFiles = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
3428
3630
|
let sourceBytes = 0, contentBytes = 0;
|
|
3429
3631
|
for (const f of dogFiles) {
|
|
3430
|
-
const bytes = Buffer.byteLength(
|
|
3632
|
+
const bytes = Buffer.byteLength(readFileSync3(join3(pd, f), "utf-8"), "utf-8");
|
|
3431
3633
|
sourceBytes += bytes;
|
|
3432
3634
|
if (bytes >= 100)
|
|
3433
3635
|
contentBytes += bytes;
|
|
3434
3636
|
}
|
|
3435
|
-
const dagBytes = Buffer.byteLength(
|
|
3637
|
+
const dagBytes = Buffer.byteLength(readFileSync3(dagFile, "utf-8"), "utf-8");
|
|
3436
3638
|
const savings = sourceBytes > 0 ? Math.round((1 - dagBytes / sourceBytes) * 1000) / 10 : 0;
|
|
3437
3639
|
console.log(source_default.bold(`
|
|
3438
3640
|
${p}`));
|
|
@@ -3450,27 +3652,57 @@ program2.command("tokens [dir]").action((d = ".") => {
|
|
|
3450
3652
|
});
|
|
3451
3653
|
program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts) => {
|
|
3452
3654
|
const dir = resolvePath2(d);
|
|
3453
|
-
const dirs = [
|
|
3655
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3454
3656
|
for (const dd of dirs) {
|
|
3455
|
-
if (!
|
|
3657
|
+
if (!existsSync3(dd))
|
|
3456
3658
|
continue;
|
|
3457
|
-
const projects =
|
|
3659
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3458
3660
|
for (const p of projects) {
|
|
3459
|
-
const dagFile =
|
|
3460
|
-
if (!
|
|
3661
|
+
const dagFile = join3(dd, p, `${p}.dag`);
|
|
3662
|
+
if (!existsSync3(dagFile))
|
|
3461
3663
|
continue;
|
|
3462
|
-
const dag = JSON.parse(
|
|
3664
|
+
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
3665
|
+
const nodes = dag.n || dag.nodes || [];
|
|
3463
3666
|
let out = "```mermaid\ngraph LR\n";
|
|
3464
|
-
for (const n of
|
|
3465
|
-
|
|
3667
|
+
for (const n of nodes) {
|
|
3668
|
+
const raw = n.i || n.id || "";
|
|
3669
|
+
const id = raw.replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
|
|
3670
|
+
if (raw)
|
|
3671
|
+
out += ` ${id}[${raw}]
|
|
3672
|
+
`;
|
|
3673
|
+
}
|
|
3674
|
+
const seen = new Set;
|
|
3675
|
+
for (const n of nodes) {
|
|
3676
|
+
for (const e of n.es || []) {
|
|
3677
|
+
if (e.dir === "in")
|
|
3678
|
+
continue;
|
|
3679
|
+
const src = (n.i || n.id || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
|
|
3680
|
+
const tgt = (e.t || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
|
|
3681
|
+
const verb = e.v || "";
|
|
3682
|
+
const key = `${src}→${tgt}:${verb}`;
|
|
3683
|
+
if (!seen.has(key) && src && tgt) {
|
|
3684
|
+
seen.add(key);
|
|
3685
|
+
out += ` ${src} -->|${verb}| ${tgt}
|
|
3466
3686
|
`;
|
|
3467
|
-
|
|
3468
|
-
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
const legacyEdges = dag.e || dag.edges || [];
|
|
3691
|
+
for (const e of legacyEdges) {
|
|
3692
|
+
const src = (e.s || e.source || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
|
|
3693
|
+
const tgt = (e.t || e.target || "").replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
|
|
3694
|
+
const verb = e.v || e.verb || "";
|
|
3695
|
+
const key = `${src}→${tgt}:${verb}`;
|
|
3696
|
+
if (!seen.has(key) && src && tgt) {
|
|
3697
|
+
seen.add(key);
|
|
3698
|
+
out += ` ${src} -->|${verb}| ${tgt}
|
|
3469
3699
|
`;
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3470
3702
|
out += "```\n";
|
|
3471
3703
|
if (opts.save) {
|
|
3472
|
-
const outFile =
|
|
3473
|
-
|
|
3704
|
+
const outFile = join3(dd, p, `${p}.md`);
|
|
3705
|
+
writeFileSync2(outFile, `# ${p} : Spec Graph
|
|
3474
3706
|
|
|
3475
3707
|
${out}`);
|
|
3476
3708
|
console.log(source_default.green(` ✓ ${outFile}`));
|
|
@@ -3482,24 +3714,24 @@ ${out}`);
|
|
|
3482
3714
|
program2.command("serve [dir]").description("MCP server : expose .dag graph to AI agents over stdio").action((d = ".") => serve(resolvePath2(d)));
|
|
3483
3715
|
program2.command("analyze [dir]").description("Analyze a spec project : score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
|
|
3484
3716
|
const dir = resolvePath2(d);
|
|
3485
|
-
const dirs = [
|
|
3717
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3486
3718
|
console.log(source_default.bold(`
|
|
3487
3719
|
Spec Analysis
|
|
3488
3720
|
`));
|
|
3489
|
-
let found = false;
|
|
3721
|
+
let found = false, hasGaps = false;
|
|
3490
3722
|
for (const dd of dirs) {
|
|
3491
|
-
if (!
|
|
3723
|
+
if (!existsSync3(dd))
|
|
3492
3724
|
continue;
|
|
3493
|
-
const projects =
|
|
3725
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3494
3726
|
for (const p of projects) {
|
|
3495
3727
|
if (opts.project && p !== opts.project)
|
|
3496
3728
|
continue;
|
|
3497
|
-
const pd =
|
|
3498
|
-
if (!
|
|
3729
|
+
const pd = join3(dd, p);
|
|
3730
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3499
3731
|
continue;
|
|
3500
|
-
if (!
|
|
3732
|
+
if (!existsSync3(pd))
|
|
3501
3733
|
continue;
|
|
3502
|
-
const files =
|
|
3734
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
3503
3735
|
if (!files.length)
|
|
3504
3736
|
continue;
|
|
3505
3737
|
found = true;
|
|
@@ -3510,7 +3742,7 @@ Spec Analysis
|
|
|
3510
3742
|
const allRelationships = [];
|
|
3511
3743
|
const analyses = [];
|
|
3512
3744
|
for (const f of files) {
|
|
3513
|
-
const content =
|
|
3745
|
+
const content = readFileSync3(join3(pd, f), "utf-8");
|
|
3514
3746
|
const ast = parse(content);
|
|
3515
3747
|
const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
|
|
3516
3748
|
const rels = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "relationship"));
|
|
@@ -3552,6 +3784,39 @@ Spec Analysis
|
|
|
3552
3784
|
if (r.target && !entityNames.has(r.target))
|
|
3553
3785
|
gaps.push(`\uD83D\uDFE1 Relationship: unknown target "${r.target}"`);
|
|
3554
3786
|
}
|
|
3787
|
+
const contradictions = [];
|
|
3788
|
+
const relMap = new Map;
|
|
3789
|
+
for (const r of allRelationships) {
|
|
3790
|
+
const key = `${r.source}→${r.target}`;
|
|
3791
|
+
if (!relMap.has(key))
|
|
3792
|
+
relMap.set(key, []);
|
|
3793
|
+
relMap.get(key).push(r);
|
|
3794
|
+
}
|
|
3795
|
+
for (const [key, rels] of relMap) {
|
|
3796
|
+
if (rels.length > 1) {
|
|
3797
|
+
const verbs = [...new Set(rels.map((r) => r.verb))];
|
|
3798
|
+
if (verbs.length > 1)
|
|
3799
|
+
contradictions.push(`\uD83D\uDD34 Contradiction: "${key}" has conflicting verbs: ${verbs.join(", ")}`);
|
|
3800
|
+
else
|
|
3801
|
+
contradictions.push(`\uD83D\uDFE1 Duplicate: "${key}" appears ${rels.length} times with same verb "${verbs[0]}"`);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
for (const r of allRelationships) {
|
|
3805
|
+
const reverse = allRelationships.find((r2) => r2.source === r.target && r2.target === r.source);
|
|
3806
|
+
if (reverse && r.verb !== reverse.verb && r.source < r.target) {
|
|
3807
|
+
contradictions.push(`\uD83D\uDFE1 Bidirectional: "${r.source}→${r.target}" verb "${r.verb}" vs "${reverse.source}→${reverse.target}" verb "${reverse.verb}"`);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
for (const c of contradictions) {
|
|
3811
|
+
if (c.startsWith("\uD83D\uDD34"))
|
|
3812
|
+
hasGaps = true;
|
|
3813
|
+
}
|
|
3814
|
+
if (contradictions.length > 0) {
|
|
3815
|
+
console.log(source_default.bold(`
|
|
3816
|
+
Contradictions (${contradictions.length})`));
|
|
3817
|
+
for (const c of contradictions)
|
|
3818
|
+
console.log(` ${c}`);
|
|
3819
|
+
}
|
|
3555
3820
|
if (gaps.length > 0) {
|
|
3556
3821
|
console.log(source_default.bold(`
|
|
3557
3822
|
Gaps (${gaps.length})`));
|
|
@@ -3560,26 +3825,30 @@ Spec Analysis
|
|
|
3560
3825
|
} else
|
|
3561
3826
|
console.log(source_default.green(`
|
|
3562
3827
|
No gaps found.`));
|
|
3828
|
+
if (gaps.length > 0)
|
|
3829
|
+
hasGaps = true;
|
|
3563
3830
|
}
|
|
3564
3831
|
}
|
|
3565
3832
|
if (!found)
|
|
3566
3833
|
console.log(source_default.yellow("No spec projects found. Run: dotdog init <project>"));
|
|
3834
|
+
if (hasGaps)
|
|
3835
|
+
process.exit(1);
|
|
3567
3836
|
});
|
|
3568
3837
|
program2.command("generate [dir]").description("Generate missing spec files from SPEC.dog").option("-p, --project <name>").action((d = ".", opts) => {
|
|
3569
3838
|
const dir = resolvePath2(d);
|
|
3570
|
-
const dirs = [
|
|
3839
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3571
3840
|
let specContent = "", specDir = "";
|
|
3572
3841
|
for (const dd of dirs) {
|
|
3573
|
-
if (!
|
|
3842
|
+
if (!existsSync3(dd))
|
|
3574
3843
|
continue;
|
|
3575
|
-
const projects =
|
|
3844
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3576
3845
|
for (const p of projects) {
|
|
3577
3846
|
if (opts.project && p !== opts.project)
|
|
3578
3847
|
continue;
|
|
3579
|
-
const pd =
|
|
3580
|
-
const sp =
|
|
3581
|
-
if (
|
|
3582
|
-
specContent =
|
|
3848
|
+
const pd = join3(dd, p);
|
|
3849
|
+
const sp = join3(pd, "SPEC.dog");
|
|
3850
|
+
if (existsSync3(sp)) {
|
|
3851
|
+
specContent = readFileSync3(sp, "utf-8");
|
|
3583
3852
|
specDir = pd;
|
|
3584
3853
|
break;
|
|
3585
3854
|
}
|
|
@@ -3610,7 +3879,7 @@ Spec Generator
|
|
|
3610
3879
|
uiStrings.push({ screen: section.heading, element: "label", text: m });
|
|
3611
3880
|
}
|
|
3612
3881
|
}
|
|
3613
|
-
if (!
|
|
3882
|
+
if (!existsSync3(join3(specDir, "data-model.dog")) && entities.length > 0) {
|
|
3614
3883
|
let dm = `# Data Model
|
|
3615
3884
|
|
|
3616
3885
|
## Core Entities
|
|
@@ -3643,10 +3912,10 @@ ${e.description || "No description."}
|
|
|
3643
3912
|
`;
|
|
3644
3913
|
dm += "```\n\n";
|
|
3645
3914
|
}
|
|
3646
|
-
|
|
3915
|
+
writeFileSync2(join3(specDir, "data-model.dog"), dm);
|
|
3647
3916
|
console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
|
|
3648
3917
|
}
|
|
3649
|
-
if (!
|
|
3918
|
+
if (!existsSync3(join3(specDir, "COPY.dog")) && uiStrings.length > 0) {
|
|
3650
3919
|
let copy = `# App Copy
|
|
3651
3920
|
|
|
3652
3921
|
| Screen | Element | Copy |
|
|
@@ -3655,10 +3924,10 @@ ${e.description || "No description."}
|
|
|
3655
3924
|
for (const s of uiStrings)
|
|
3656
3925
|
copy += `| ${s.screen} | ${s.element} | ${s.text} |
|
|
3657
3926
|
`;
|
|
3658
|
-
|
|
3927
|
+
writeFileSync2(join3(specDir, "COPY.dog"), copy);
|
|
3659
3928
|
console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
|
|
3660
3929
|
}
|
|
3661
|
-
if (!
|
|
3930
|
+
if (!existsSync3(join3(specDir, "INDEX.dog"))) {
|
|
3662
3931
|
let idx = `# INDEX
|
|
3663
3932
|
|
|
3664
3933
|
| You are... | Start here | Then... |
|
|
@@ -3670,41 +3939,139 @@ ${e.description || "No description."}
|
|
|
3670
3939
|
`;
|
|
3671
3940
|
idx += `| Designer | SPEC.dog | COPY.dog |
|
|
3672
3941
|
`;
|
|
3673
|
-
|
|
3942
|
+
writeFileSync2(join3(specDir, "INDEX.dog"), idx);
|
|
3674
3943
|
console.log(source_default.green(" ✓ INDEX.dog"));
|
|
3675
3944
|
}
|
|
3676
3945
|
console.log(source_default.bold(`
|
|
3677
3946
|
Run dotdog validate to verify.
|
|
3678
3947
|
`));
|
|
3679
3948
|
});
|
|
3680
|
-
program2.command("simulate <scenario>").description("
|
|
3949
|
+
program2.command("simulate <scenario>").description("Walk through a scenario, check pre/postconditions").option("-p, --project <name>").action((scenario, opts) => {
|
|
3950
|
+
const dir = resolvePath2(".");
|
|
3951
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3952
|
+
let projectDir = "";
|
|
3953
|
+
for (const dd of dirs) {
|
|
3954
|
+
if (!existsSync3(dd))
|
|
3955
|
+
continue;
|
|
3956
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3957
|
+
for (const p of projects) {
|
|
3958
|
+
if (opts.project && p !== opts.project)
|
|
3959
|
+
continue;
|
|
3960
|
+
const pd = join3(dd, p);
|
|
3961
|
+
if (existsSync3(join3(pd, "SPEC.dog"))) {
|
|
3962
|
+
projectDir = pd;
|
|
3963
|
+
break;
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
if (projectDir)
|
|
3967
|
+
break;
|
|
3968
|
+
}
|
|
3969
|
+
if (!projectDir) {
|
|
3970
|
+
console.log(source_default.red("Project not found."));
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
const dagFile = join3(projectDir, `${opts.project || projectDir.split("/").pop()}.dag`);
|
|
3974
|
+
let entities = [], relationships = [];
|
|
3975
|
+
if (existsSync3(dagFile)) {
|
|
3976
|
+
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
3977
|
+
entities = (dag.n || dag.nodes || []).map((n) => (n.i || n.id || "").toLowerCase());
|
|
3978
|
+
if (dag.e || dag.edges) {
|
|
3979
|
+
relationships = dag.e || dag.edges || [];
|
|
3980
|
+
} else {
|
|
3981
|
+
const seen = new Set;
|
|
3982
|
+
for (const n of dag.n || dag.nodes || []) {
|
|
3983
|
+
for (const e of n.es || []) {
|
|
3984
|
+
if (e.dir === "in")
|
|
3985
|
+
continue;
|
|
3986
|
+
const key = `${n.i || n.id}→${e.t}:${e.v}`;
|
|
3987
|
+
if (!seen.has(key)) {
|
|
3988
|
+
seen.add(key);
|
|
3989
|
+
relationships.push({ s: n.i || n.id, t: e.t, v: e.v, d: e.d, r: e.r });
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
const specFile = join3(projectDir, "SPEC.dog");
|
|
3996
|
+
const specContent = existsSync3(specFile) ? readFileSync3(specFile, "utf-8") : "";
|
|
3681
3997
|
console.log(source_default.bold(`
|
|
3682
|
-
Simulation: ${scenario}
|
|
3683
|
-
`));
|
|
3684
|
-
|
|
3685
|
-
|
|
3998
|
+
Simulation: ${scenario}`));
|
|
3999
|
+
console.log(source_default.gray(`Project: ${projectDir.split("/").pop()} | Entities: ${entities.length} | Relationships: ${relationships.length}`));
|
|
4000
|
+
const steps = [];
|
|
4001
|
+
const stepMatches = specContent.match(/\[(\d+)\/\d+\]\s*(.+)/g);
|
|
4002
|
+
if (stepMatches) {
|
|
4003
|
+
for (const m of stepMatches) {
|
|
4004
|
+
const step = m.replace(/\[\d+\/\d+\]\s*/, "");
|
|
4005
|
+
if (step)
|
|
4006
|
+
steps.push(step);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
if (steps.length === 0) {
|
|
4010
|
+
console.log(source_default.yellow(`
|
|
4011
|
+
No scenario steps found in SPEC.dog.`));
|
|
4012
|
+
console.log(source_default.gray(" Add steps like: [1/3] User taps button"));
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
let passed = 0, failed = 0;
|
|
4016
|
+
for (let i = 0;i < steps.length; i++) {
|
|
4017
|
+
const step = steps[i];
|
|
4018
|
+
console.log(source_default.cyan(`
|
|
4019
|
+
[${i + 1}/${steps.length}] ${step}`));
|
|
4020
|
+
let foundRef = false;
|
|
4021
|
+
for (const e of entities) {
|
|
4022
|
+
if (step.toLowerCase().includes(e)) {
|
|
4023
|
+
console.log(source_default.green(` ✓ References entity: ${e}`));
|
|
4024
|
+
foundRef = true;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
for (const r of relationships) {
|
|
4028
|
+
const src = (r.s || r.source || "").toLowerCase();
|
|
4029
|
+
const tgt = (r.t || r.target || "").toLowerCase();
|
|
4030
|
+
const verb = (r.v || r.verb || "").toLowerCase();
|
|
4031
|
+
if (step.toLowerCase().includes(src) && step.toLowerCase().includes(tgt)) {
|
|
4032
|
+
console.log(source_default.green(` ✓ References relationship: ${src} → ${tgt}`));
|
|
4033
|
+
foundRef = true;
|
|
4034
|
+
}
|
|
4035
|
+
if (verb && step.toLowerCase().includes(verb)) {
|
|
4036
|
+
console.log(source_default.green(` ✓ References verb: ${verb}`));
|
|
4037
|
+
foundRef = true;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
if (!foundRef) {
|
|
4041
|
+
console.log(source_default.yellow(` ⚠ No entity or relationship references found`));
|
|
4042
|
+
failed++;
|
|
4043
|
+
} else {
|
|
4044
|
+
passed++;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
console.log(source_default.bold(`
|
|
4048
|
+
RESULT: ${failed === 0 ? source_default.green("Success") : source_default.yellow("Partial")} (${passed}/${steps.length} steps passed)`));
|
|
4049
|
+
if (failed > 0)
|
|
4050
|
+
console.log(source_default.red(` ${failed} steps have no spec references — entities or relationships may be missing.`));
|
|
4051
|
+
if (failed === 0)
|
|
4052
|
+
console.log(source_default.green(" All steps reference known entities and relationships."));
|
|
3686
4053
|
});
|
|
3687
4054
|
program2.command("staleness [dir]").action((d = ".") => {
|
|
3688
4055
|
const dir = resolvePath2(d);
|
|
3689
|
-
const dirs = [
|
|
4056
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3690
4057
|
console.log(source_default.bold(`Staleness Audit
|
|
3691
4058
|
`));
|
|
3692
4059
|
for (const dd of dirs) {
|
|
3693
|
-
if (!
|
|
4060
|
+
if (!existsSync3(dd))
|
|
3694
4061
|
continue;
|
|
3695
|
-
const projects =
|
|
4062
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3696
4063
|
for (const p of projects) {
|
|
3697
|
-
const pd =
|
|
3698
|
-
if (!
|
|
4064
|
+
const pd = join3(dd, p);
|
|
4065
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3699
4066
|
continue;
|
|
3700
|
-
if (!
|
|
4067
|
+
if (!existsSync3(pd))
|
|
3701
4068
|
continue;
|
|
3702
|
-
const planFile =
|
|
3703
|
-
if (!
|
|
4069
|
+
const planFile = join3(pd, "plan.dog");
|
|
4070
|
+
if (!existsSync3(planFile)) {
|
|
3704
4071
|
console.log(source_default.yellow(` ${p}: No plan.dog`));
|
|
3705
4072
|
continue;
|
|
3706
4073
|
}
|
|
3707
|
-
const plan =
|
|
4074
|
+
const plan = readFileSync3(planFile, "utf-8");
|
|
3708
4075
|
const tasks = [...plan.matchAll(/^\s*- \[([ x])\]\s+(.+)/gm)];
|
|
3709
4076
|
let issues = 0;
|
|
3710
4077
|
for (const m of tasks) {
|
|
@@ -3715,14 +4082,15 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
3715
4082
|
const phase = phaseMatch ? parseInt(phaseMatch[1]) : 99;
|
|
3716
4083
|
if (phase > 3)
|
|
3717
4084
|
continue;
|
|
3718
|
-
if (text.includes("npm publish") || text.includes("npm install")) {
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
4085
|
+
if ((text.includes("npm publish") || text.includes("npm install")) && !done) {
|
|
4086
|
+
const pkgPath = join3(resolvePath2("."), "packages/dotdog/package.json");
|
|
4087
|
+
if (existsSync3(pkgPath)) {
|
|
4088
|
+
const pkg2 = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
4089
|
+
if (pkg2.version) {
|
|
3722
4090
|
console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
|
|
3723
4091
|
issues++;
|
|
3724
4092
|
}
|
|
3725
|
-
}
|
|
4093
|
+
}
|
|
3726
4094
|
}
|
|
3727
4095
|
if (text.includes("compile")) {
|
|
3728
4096
|
if (!done) {
|
|
@@ -3731,7 +4099,7 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
3731
4099
|
}
|
|
3732
4100
|
}
|
|
3733
4101
|
if (text.includes("generate") && !done) {
|
|
3734
|
-
if (
|
|
4102
|
+
if (existsSync3(join3(dir, "packages/dotdog/src/cli.ts")) || existsSync3(join3(dir, "packages/spec-cli/src/generate.ts"))) {
|
|
3735
4103
|
console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
|
|
3736
4104
|
issues++;
|
|
3737
4105
|
}
|
|
@@ -3751,6 +4119,206 @@ program2.command("woof").action(() => {
|
|
|
3751
4119
|
console.log(" / (_____/");
|
|
3752
4120
|
console.log("/_____/ U");
|
|
3753
4121
|
});
|
|
4122
|
+
program2.command("index [dir]").description("Build search index for semantic queries").action((d = ".") => {
|
|
4123
|
+
const dir = resolvePath2(d);
|
|
4124
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4125
|
+
let built = 0;
|
|
4126
|
+
for (const dd of dirs) {
|
|
4127
|
+
if (!existsSync3(dd))
|
|
4128
|
+
continue;
|
|
4129
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4130
|
+
for (const p of projects) {
|
|
4131
|
+
const pd = join3(dd, p);
|
|
4132
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
4133
|
+
continue;
|
|
4134
|
+
const idx = buildIndex(pd, p);
|
|
4135
|
+
writeFileSync2(join3(pd, `${p}.idx`), JSON.stringify(idx));
|
|
4136
|
+
console.log(source_default.green(` ✓ ${p} : ${idx.entries.length} sections indexed (${idx.vocabulary.length} terms)`));
|
|
4137
|
+
built++;
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
if (!built)
|
|
4141
|
+
console.log(source_default.yellow("No projects found. Run dotdog init first."));
|
|
4142
|
+
});
|
|
4143
|
+
program2.command("search <query>").description("Semantic search across compiled specs").option("-p, --project <name>").action((query, opts) => {
|
|
4144
|
+
console.log(source_default.bold(`
|
|
4145
|
+
Search: "${query}"
|
|
4146
|
+
`));
|
|
4147
|
+
const dir = resolvePath2(".");
|
|
4148
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4149
|
+
let found = false;
|
|
4150
|
+
for (const dd of dirs) {
|
|
4151
|
+
if (!existsSync3(dd))
|
|
4152
|
+
continue;
|
|
4153
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4154
|
+
for (const p of projects) {
|
|
4155
|
+
if (opts.project && p !== opts.project)
|
|
4156
|
+
continue;
|
|
4157
|
+
const pd = join3(dd, p);
|
|
4158
|
+
const idxFile = join3(pd, `${p}.idx`);
|
|
4159
|
+
if (!existsSync3(idxFile))
|
|
4160
|
+
continue;
|
|
4161
|
+
found = true;
|
|
4162
|
+
const idx = JSON.parse(readFileSync3(idxFile, "utf-8"));
|
|
4163
|
+
const results = searchIndex(idx, query, 8);
|
|
4164
|
+
if (results.length === 0) {
|
|
4165
|
+
console.log(source_default.gray(` ${p}: No matches`));
|
|
4166
|
+
continue;
|
|
4167
|
+
}
|
|
4168
|
+
console.log(source_default.green(` ${p} — ${results.length} results:`));
|
|
4169
|
+
for (const r of results) {
|
|
4170
|
+
const preview = r.entry.content.replace(/\n/g, " ").slice(0, 100);
|
|
4171
|
+
console.log(source_default.gray(` ${Math.round(r.score * 100)}% [${r.entry.file}] ${r.entry.heading}`));
|
|
4172
|
+
console.log(source_default.gray(` ${preview}...`));
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
if (!found)
|
|
4177
|
+
console.log(source_default.yellow("No index found. Run dotdog index first."));
|
|
4178
|
+
});
|
|
4179
|
+
program2.command("predictions [dir]").description("List all predictions with status").option("-p, --project <name>").action((d = ".", opts) => {
|
|
4180
|
+
const dir = resolvePath2(d);
|
|
4181
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4182
|
+
console.log(source_default.bold(`
|
|
4183
|
+
Predictions
|
|
4184
|
+
`));
|
|
4185
|
+
let found = false;
|
|
4186
|
+
for (const dd of dirs) {
|
|
4187
|
+
if (!existsSync3(dd))
|
|
4188
|
+
continue;
|
|
4189
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4190
|
+
for (const p of projects) {
|
|
4191
|
+
if (opts.project && p !== opts.project)
|
|
4192
|
+
continue;
|
|
4193
|
+
const pd = join3(dd, p);
|
|
4194
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
4195
|
+
continue;
|
|
4196
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
4197
|
+
for (const f of files) {
|
|
4198
|
+
const content = readFileSync3(join3(pd, f), "utf-8");
|
|
4199
|
+
const ast = parse(content);
|
|
4200
|
+
for (const section of ast.sections) {
|
|
4201
|
+
for (const block of section.blocks) {
|
|
4202
|
+
if (block.kind === "prediction") {
|
|
4203
|
+
found = true;
|
|
4204
|
+
const b = block;
|
|
4205
|
+
const status = b.status || "pending";
|
|
4206
|
+
const icon = status === "correct" ? "✅" : status === "wrong" ? "❌" : status === "partial" ? "⚠️" : "⏳";
|
|
4207
|
+
console.log(` ${icon} ${b.statement || b.name} (${(b.confidence * 100).toFixed(0)}% confidence, ${status})`);
|
|
4208
|
+
if (b.measurement)
|
|
4209
|
+
console.log(source_default.gray(` Measurement: ${b.measurement}`));
|
|
4210
|
+
if (b.timeframe)
|
|
4211
|
+
console.log(source_default.gray(` Timeframe: ${b.timeframe}`));
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
if (!found)
|
|
4219
|
+
console.log(source_default.yellow("No predictions found."));
|
|
4220
|
+
});
|
|
4221
|
+
program2.command("resolve <name>").description("Mark a prediction as correct, wrong, or partial").option("-p, --project <name>").option("--correct", "Prediction was correct").option("--wrong", "Prediction was wrong").option("--partial", "Prediction was partially correct").action((name, opts) => {
|
|
4222
|
+
const dir = resolvePath2(".");
|
|
4223
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4224
|
+
const status = opts.correct ? "correct" : opts.wrong ? "wrong" : opts.partial ? "partial" : null;
|
|
4225
|
+
if (!status) {
|
|
4226
|
+
console.log(source_default.red("Specify --correct, --wrong, or --partial"));
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
for (const dd of dirs) {
|
|
4230
|
+
if (!existsSync3(dd))
|
|
4231
|
+
continue;
|
|
4232
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4233
|
+
for (const p of projects) {
|
|
4234
|
+
if (opts.project && p !== opts.project)
|
|
4235
|
+
continue;
|
|
4236
|
+
const pd = join3(dd, p);
|
|
4237
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
4238
|
+
continue;
|
|
4239
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
4240
|
+
for (const f of files) {
|
|
4241
|
+
const fp = join3(pd, f);
|
|
4242
|
+
let content = readFileSync3(fp, "utf-8");
|
|
4243
|
+
const ast = parse(content);
|
|
4244
|
+
for (const section of ast.sections) {
|
|
4245
|
+
for (const block of section.blocks) {
|
|
4246
|
+
if (block.kind === "prediction") {
|
|
4247
|
+
const b = block;
|
|
4248
|
+
if ((b.statement || b.name || "").toLowerCase().includes(name.toLowerCase())) {
|
|
4249
|
+
const searchFor = `### Prediction: ${b.statement || b.name}`;
|
|
4250
|
+
const headingIdx = content.indexOf(searchFor);
|
|
4251
|
+
if (headingIdx >= 0) {
|
|
4252
|
+
const blockStart = content.indexOf("```yaml", headingIdx);
|
|
4253
|
+
const blockEnd = content.indexOf("```", blockStart + 7);
|
|
4254
|
+
if (blockStart >= 0 && blockEnd >= 0) {
|
|
4255
|
+
const yamlBlock = content.slice(blockStart, blockEnd + 3);
|
|
4256
|
+
let newYaml = yamlBlock;
|
|
4257
|
+
if (yamlBlock.includes("status:")) {
|
|
4258
|
+
newYaml = yamlBlock.replace(/status:\s*\w+/, `status: ${status}`);
|
|
4259
|
+
} else {
|
|
4260
|
+
newYaml = yamlBlock.replace(/\n\s*(trigger|timeframe|confidence|measurement):/, `
|
|
4261
|
+
status: ${status}
|
|
4262
|
+
$1`);
|
|
4263
|
+
}
|
|
4264
|
+
content = content.replace(yamlBlock, newYaml);
|
|
4265
|
+
writeFileSync2(fp, content, "utf-8");
|
|
4266
|
+
console.log(source_default.green(` ✓ ${b.statement || b.name}: ${status}`));
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
console.log(source_default.yellow(`Prediction "${name}" not found.`));
|
|
4278
|
+
});
|
|
4279
|
+
var kitDir = join3(resolve2(import.meta.dirname || "."), "..", "kits");
|
|
4280
|
+
var builtInKits = existsSync3(kitDir) ? readdirSync3(kitDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : [];
|
|
4281
|
+
var kitCmd = program2.command("kit").description("Manage spec kits");
|
|
4282
|
+
kitCmd.command("list").description("List available kits").action(() => {
|
|
4283
|
+
console.log(source_default.bold(`
|
|
4284
|
+
Available kits
|
|
4285
|
+
`));
|
|
4286
|
+
if (builtInKits.length === 0) {
|
|
4287
|
+
console.log(source_default.gray(" No built-in kits found."));
|
|
4288
|
+
} else {
|
|
4289
|
+
for (const k of builtInKits) {
|
|
4290
|
+
const specFile = join3(kitDir, k, "SPEC.dog");
|
|
4291
|
+
const desc = existsSync3(specFile) ? readFileSync3(specFile, "utf-8").split(`
|
|
4292
|
+
`)[1]?.replace(/^>\s*/, "") || "" : "";
|
|
4293
|
+
console.log(` ${source_default.green(k)} ${source_default.gray(desc)}`);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
console.log(source_default.gray(`
|
|
4297
|
+
Community kits: npm install @scope/kit-<name> then dotdog kit install <name>`));
|
|
4298
|
+
});
|
|
4299
|
+
kitCmd.command("init <kit>").description("Init a project from a kit").option("-p, --project <name>").action((kit, opts) => {
|
|
4300
|
+
const src = join3(kitDir, kit);
|
|
4301
|
+
if (!existsSync3(src)) {
|
|
4302
|
+
console.log(source_default.red(`Kit "${kit}" not found. Available: ${builtInKits.join(", ")}`));
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
4305
|
+
const projectName = opts.project || kit;
|
|
4306
|
+
const dir = resolvePath2(".");
|
|
4307
|
+
const dest = join3(dir, "specs", projectName);
|
|
4308
|
+
if (existsSync3(dest)) {
|
|
4309
|
+
console.log(source_default.yellow(`Project "${projectName}" already exists.`));
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
mkdirSync(dest, { recursive: true });
|
|
4313
|
+
const files = readdirSync3(src).filter((f) => f.endsWith(".dog"));
|
|
4314
|
+
for (const f of files) {
|
|
4315
|
+
writeFileSync2(join3(dest, f), readFileSync3(join3(src, f), "utf-8"));
|
|
4316
|
+
console.log(source_default.green(` ✓ ${f}`));
|
|
4317
|
+
}
|
|
4318
|
+
console.log(source_default.gray(`
|
|
4319
|
+
Kit "${kit}" initialized in specs/${projectName}/`));
|
|
4320
|
+
console.log(source_default.gray(` Run: dotdog validate`));
|
|
4321
|
+
});
|
|
3754
4322
|
program2.parse();
|
|
3755
4323
|
export {
|
|
3756
4324
|
parseToJSON,
|