dotdog 0.3.5 → 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 +668 -108
- 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,22 +3346,22 @@ 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 = [
|
|
3353
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs")];
|
|
3193
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(`
|
|
@@ -3221,7 +3382,7 @@ program2.command("validate [dir]").action((d = ".") => {
|
|
|
3221
3382
|
process.exit(1);
|
|
3222
3383
|
});
|
|
3223
3384
|
program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data-model.dog").action((p, opts) => {
|
|
3224
|
-
const d =
|
|
3385
|
+
const d = join3(process.cwd(), "specs", p);
|
|
3225
3386
|
mkdirSync(d, { recursive: true });
|
|
3226
3387
|
const full = {
|
|
3227
3388
|
"SPEC.dog": `# Project
|
|
@@ -3269,7 +3430,7 @@ program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data
|
|
|
3269
3430
|
};
|
|
3270
3431
|
const tmpl = opts.minimal ? minimal : full;
|
|
3271
3432
|
for (const [f, c] of Object.entries(tmpl)) {
|
|
3272
|
-
|
|
3433
|
+
writeFileSync2(join3(d, f), c);
|
|
3273
3434
|
console.log(source_default.green(` ✓ ${f}`));
|
|
3274
3435
|
}
|
|
3275
3436
|
console.log(source_default.bold(`
|
|
@@ -3277,23 +3438,23 @@ Project "${p}" initialized. Fill in SPEC.dog then run spec validate.`));
|
|
|
3277
3438
|
});
|
|
3278
3439
|
program2.command("list").action(() => {
|
|
3279
3440
|
for (const d of ["projects", "specs"]) {
|
|
3280
|
-
const dd =
|
|
3281
|
-
if (!
|
|
3441
|
+
const dd = join3(process.cwd(), d);
|
|
3442
|
+
if (!existsSync3(dd))
|
|
3282
3443
|
continue;
|
|
3283
|
-
const projects =
|
|
3444
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3284
3445
|
if (!projects.length)
|
|
3285
3446
|
continue;
|
|
3286
3447
|
console.log(source_default.bold(`
|
|
3287
3448
|
${d}/`));
|
|
3288
3449
|
for (const p of projects) {
|
|
3289
|
-
const sp =
|
|
3290
|
-
const n =
|
|
3450
|
+
const sp = join3(dd, p);
|
|
3451
|
+
const n = existsSync3(sp) ? readdirSync3(sp).filter((f) => f.endsWith(".dog")).length : 0;
|
|
3291
3452
|
console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
|
|
3292
3453
|
}
|
|
3293
3454
|
}
|
|
3294
3455
|
});
|
|
3295
3456
|
program2.command("parse <file>").action((f) => {
|
|
3296
|
-
const c =
|
|
3457
|
+
const c = readFileSync3(f, "utf-8");
|
|
3297
3458
|
const s = parseSections2(c);
|
|
3298
3459
|
console.log(source_default.bold(`
|
|
3299
3460
|
${s.length} sections`));
|
|
@@ -3302,26 +3463,26 @@ ${s.length} sections`));
|
|
|
3302
3463
|
});
|
|
3303
3464
|
program2.command("compile [dir]").option("-o, --output <file>").action((d = ".", opts) => {
|
|
3304
3465
|
const dir = resolvePath2(d);
|
|
3305
|
-
const dirs = [
|
|
3466
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3306
3467
|
let found = false;
|
|
3307
3468
|
for (const dd of dirs) {
|
|
3308
|
-
if (!
|
|
3469
|
+
if (!existsSync3(dd))
|
|
3309
3470
|
continue;
|
|
3310
|
-
const projects =
|
|
3471
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3311
3472
|
for (const p of projects) {
|
|
3312
|
-
const pd =
|
|
3313
|
-
if (!
|
|
3473
|
+
const pd = join3(dd, p);
|
|
3474
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3314
3475
|
continue;
|
|
3315
|
-
if (!
|
|
3476
|
+
if (!existsSync3(pd))
|
|
3316
3477
|
continue;
|
|
3317
|
-
const files =
|
|
3478
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog")).sort();
|
|
3318
3479
|
if (!files.length)
|
|
3319
3480
|
continue;
|
|
3320
3481
|
found = true;
|
|
3321
3482
|
const sources = {};
|
|
3322
3483
|
let sourceBytes = 0, contentBytes = 0;
|
|
3323
3484
|
for (const f of files) {
|
|
3324
|
-
const content =
|
|
3485
|
+
const content = readFileSync3(join3(pd, f), "utf-8");
|
|
3325
3486
|
sources[f] = content;
|
|
3326
3487
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
3327
3488
|
sourceBytes += bytes;
|
|
@@ -3394,16 +3555,53 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3394
3555
|
}
|
|
3395
3556
|
}
|
|
3396
3557
|
}
|
|
3397
|
-
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 };
|
|
3398
3596
|
const dagJson = JSON.stringify(dag);
|
|
3399
3597
|
const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
|
|
3400
3598
|
const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
|
|
3401
3599
|
const contentSavingsPct = contentTokens > 0 ? Math.round((1 - dagTokens / contentTokens) * 1000) / 10 : 0;
|
|
3402
3600
|
const savingsTokens = sourceTokens - dagTokens;
|
|
3403
|
-
const outPath = opts.output ||
|
|
3601
|
+
const outPath = opts.output || join3(pd, `${p}.dag`);
|
|
3404
3602
|
const tokens = { m: "chars/4", st: sourceTokens, ct: contentTokens, dt: dagTokens, sv: allSavingsPct, cs: contentSavingsPct, saved: savingsTokens };
|
|
3405
3603
|
const report = { ...dag, tk: tokens };
|
|
3406
|
-
|
|
3604
|
+
writeFileSync2(outPath, JSON.stringify(report, null, 2));
|
|
3407
3605
|
console.log(source_default.green(` ✓ ${outPath}`));
|
|
3408
3606
|
console.log(source_default.gray(` ${nodes.length} nodes, ${edges.length} edges, ${files.length} files`));
|
|
3409
3607
|
console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${allSavingsPct}% savings, ${contentSavingsPct}% content-only, ${savingsTokens} saved)`));
|
|
@@ -3414,29 +3612,29 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3414
3612
|
});
|
|
3415
3613
|
program2.command("tokens [dir]").action((d = ".") => {
|
|
3416
3614
|
const dir = resolvePath2(d);
|
|
3417
|
-
const dirs = [
|
|
3615
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3418
3616
|
let found = false;
|
|
3419
3617
|
for (const dd of dirs) {
|
|
3420
|
-
if (!
|
|
3618
|
+
if (!existsSync3(dd))
|
|
3421
3619
|
continue;
|
|
3422
|
-
const projects =
|
|
3620
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3423
3621
|
for (const p of projects) {
|
|
3424
|
-
const pd =
|
|
3425
|
-
if (!
|
|
3622
|
+
const pd = join3(dd, p);
|
|
3623
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3426
3624
|
continue;
|
|
3427
|
-
const dagFile =
|
|
3428
|
-
if (!
|
|
3625
|
+
const dagFile = join3(pd, `${p}.dag`);
|
|
3626
|
+
if (!existsSync3(dagFile))
|
|
3429
3627
|
continue;
|
|
3430
3628
|
found = true;
|
|
3431
|
-
const dogFiles =
|
|
3629
|
+
const dogFiles = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
3432
3630
|
let sourceBytes = 0, contentBytes = 0;
|
|
3433
3631
|
for (const f of dogFiles) {
|
|
3434
|
-
const bytes = Buffer.byteLength(
|
|
3632
|
+
const bytes = Buffer.byteLength(readFileSync3(join3(pd, f), "utf-8"), "utf-8");
|
|
3435
3633
|
sourceBytes += bytes;
|
|
3436
3634
|
if (bytes >= 100)
|
|
3437
3635
|
contentBytes += bytes;
|
|
3438
3636
|
}
|
|
3439
|
-
const dagBytes = Buffer.byteLength(
|
|
3637
|
+
const dagBytes = Buffer.byteLength(readFileSync3(dagFile, "utf-8"), "utf-8");
|
|
3440
3638
|
const savings = sourceBytes > 0 ? Math.round((1 - dagBytes / sourceBytes) * 1000) / 10 : 0;
|
|
3441
3639
|
console.log(source_default.bold(`
|
|
3442
3640
|
${p}`));
|
|
@@ -3454,27 +3652,57 @@ program2.command("tokens [dir]").action((d = ".") => {
|
|
|
3454
3652
|
});
|
|
3455
3653
|
program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts) => {
|
|
3456
3654
|
const dir = resolvePath2(d);
|
|
3457
|
-
const dirs = [
|
|
3655
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3458
3656
|
for (const dd of dirs) {
|
|
3459
|
-
if (!
|
|
3657
|
+
if (!existsSync3(dd))
|
|
3460
3658
|
continue;
|
|
3461
|
-
const projects =
|
|
3659
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3462
3660
|
for (const p of projects) {
|
|
3463
|
-
const dagFile =
|
|
3464
|
-
if (!
|
|
3661
|
+
const dagFile = join3(dd, p, `${p}.dag`);
|
|
3662
|
+
if (!existsSync3(dagFile))
|
|
3465
3663
|
continue;
|
|
3466
|
-
const dag = JSON.parse(
|
|
3664
|
+
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
3665
|
+
const nodes = dag.n || dag.nodes || [];
|
|
3467
3666
|
let out = "```mermaid\ngraph LR\n";
|
|
3468
|
-
for (const n of
|
|
3469
|
-
|
|
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}
|
|
3470
3686
|
`;
|
|
3471
|
-
|
|
3472
|
-
|
|
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}
|
|
3473
3699
|
`;
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3474
3702
|
out += "```\n";
|
|
3475
3703
|
if (opts.save) {
|
|
3476
|
-
const outFile =
|
|
3477
|
-
|
|
3704
|
+
const outFile = join3(dd, p, `${p}.md`);
|
|
3705
|
+
writeFileSync2(outFile, `# ${p} : Spec Graph
|
|
3478
3706
|
|
|
3479
3707
|
${out}`);
|
|
3480
3708
|
console.log(source_default.green(` ✓ ${outFile}`));
|
|
@@ -3486,24 +3714,24 @@ ${out}`);
|
|
|
3486
3714
|
program2.command("serve [dir]").description("MCP server : expose .dag graph to AI agents over stdio").action((d = ".") => serve(resolvePath2(d)));
|
|
3487
3715
|
program2.command("analyze [dir]").description("Analyze a spec project : score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
|
|
3488
3716
|
const dir = resolvePath2(d);
|
|
3489
|
-
const dirs = [
|
|
3717
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3490
3718
|
console.log(source_default.bold(`
|
|
3491
3719
|
Spec Analysis
|
|
3492
3720
|
`));
|
|
3493
3721
|
let found = false, hasGaps = false;
|
|
3494
3722
|
for (const dd of dirs) {
|
|
3495
|
-
if (!
|
|
3723
|
+
if (!existsSync3(dd))
|
|
3496
3724
|
continue;
|
|
3497
|
-
const projects =
|
|
3725
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3498
3726
|
for (const p of projects) {
|
|
3499
3727
|
if (opts.project && p !== opts.project)
|
|
3500
3728
|
continue;
|
|
3501
|
-
const pd =
|
|
3502
|
-
if (!
|
|
3729
|
+
const pd = join3(dd, p);
|
|
3730
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3503
3731
|
continue;
|
|
3504
|
-
if (!
|
|
3732
|
+
if (!existsSync3(pd))
|
|
3505
3733
|
continue;
|
|
3506
|
-
const files =
|
|
3734
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
3507
3735
|
if (!files.length)
|
|
3508
3736
|
continue;
|
|
3509
3737
|
found = true;
|
|
@@ -3514,7 +3742,7 @@ Spec Analysis
|
|
|
3514
3742
|
const allRelationships = [];
|
|
3515
3743
|
const analyses = [];
|
|
3516
3744
|
for (const f of files) {
|
|
3517
|
-
const content =
|
|
3745
|
+
const content = readFileSync3(join3(pd, f), "utf-8");
|
|
3518
3746
|
const ast = parse(content);
|
|
3519
3747
|
const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
|
|
3520
3748
|
const rels = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "relationship"));
|
|
@@ -3556,6 +3784,39 @@ Spec Analysis
|
|
|
3556
3784
|
if (r.target && !entityNames.has(r.target))
|
|
3557
3785
|
gaps.push(`\uD83D\uDFE1 Relationship: unknown target "${r.target}"`);
|
|
3558
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
|
+
}
|
|
3559
3820
|
if (gaps.length > 0) {
|
|
3560
3821
|
console.log(source_default.bold(`
|
|
3561
3822
|
Gaps (${gaps.length})`));
|
|
@@ -3575,19 +3836,19 @@ Spec Analysis
|
|
|
3575
3836
|
});
|
|
3576
3837
|
program2.command("generate [dir]").description("Generate missing spec files from SPEC.dog").option("-p, --project <name>").action((d = ".", opts) => {
|
|
3577
3838
|
const dir = resolvePath2(d);
|
|
3578
|
-
const dirs = [
|
|
3839
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3579
3840
|
let specContent = "", specDir = "";
|
|
3580
3841
|
for (const dd of dirs) {
|
|
3581
|
-
if (!
|
|
3842
|
+
if (!existsSync3(dd))
|
|
3582
3843
|
continue;
|
|
3583
|
-
const projects =
|
|
3844
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3584
3845
|
for (const p of projects) {
|
|
3585
3846
|
if (opts.project && p !== opts.project)
|
|
3586
3847
|
continue;
|
|
3587
|
-
const pd =
|
|
3588
|
-
const sp =
|
|
3589
|
-
if (
|
|
3590
|
-
specContent =
|
|
3848
|
+
const pd = join3(dd, p);
|
|
3849
|
+
const sp = join3(pd, "SPEC.dog");
|
|
3850
|
+
if (existsSync3(sp)) {
|
|
3851
|
+
specContent = readFileSync3(sp, "utf-8");
|
|
3591
3852
|
specDir = pd;
|
|
3592
3853
|
break;
|
|
3593
3854
|
}
|
|
@@ -3618,7 +3879,7 @@ Spec Generator
|
|
|
3618
3879
|
uiStrings.push({ screen: section.heading, element: "label", text: m });
|
|
3619
3880
|
}
|
|
3620
3881
|
}
|
|
3621
|
-
if (!
|
|
3882
|
+
if (!existsSync3(join3(specDir, "data-model.dog")) && entities.length > 0) {
|
|
3622
3883
|
let dm = `# Data Model
|
|
3623
3884
|
|
|
3624
3885
|
## Core Entities
|
|
@@ -3651,10 +3912,10 @@ ${e.description || "No description."}
|
|
|
3651
3912
|
`;
|
|
3652
3913
|
dm += "```\n\n";
|
|
3653
3914
|
}
|
|
3654
|
-
|
|
3915
|
+
writeFileSync2(join3(specDir, "data-model.dog"), dm);
|
|
3655
3916
|
console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
|
|
3656
3917
|
}
|
|
3657
|
-
if (!
|
|
3918
|
+
if (!existsSync3(join3(specDir, "COPY.dog")) && uiStrings.length > 0) {
|
|
3658
3919
|
let copy = `# App Copy
|
|
3659
3920
|
|
|
3660
3921
|
| Screen | Element | Copy |
|
|
@@ -3663,10 +3924,10 @@ ${e.description || "No description."}
|
|
|
3663
3924
|
for (const s of uiStrings)
|
|
3664
3925
|
copy += `| ${s.screen} | ${s.element} | ${s.text} |
|
|
3665
3926
|
`;
|
|
3666
|
-
|
|
3927
|
+
writeFileSync2(join3(specDir, "COPY.dog"), copy);
|
|
3667
3928
|
console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
|
|
3668
3929
|
}
|
|
3669
|
-
if (!
|
|
3930
|
+
if (!existsSync3(join3(specDir, "INDEX.dog"))) {
|
|
3670
3931
|
let idx = `# INDEX
|
|
3671
3932
|
|
|
3672
3933
|
| You are... | Start here | Then... |
|
|
@@ -3678,41 +3939,139 @@ ${e.description || "No description."}
|
|
|
3678
3939
|
`;
|
|
3679
3940
|
idx += `| Designer | SPEC.dog | COPY.dog |
|
|
3680
3941
|
`;
|
|
3681
|
-
|
|
3942
|
+
writeFileSync2(join3(specDir, "INDEX.dog"), idx);
|
|
3682
3943
|
console.log(source_default.green(" ✓ INDEX.dog"));
|
|
3683
3944
|
}
|
|
3684
3945
|
console.log(source_default.bold(`
|
|
3685
3946
|
Run dotdog validate to verify.
|
|
3686
3947
|
`));
|
|
3687
3948
|
});
|
|
3688
|
-
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") : "";
|
|
3689
3997
|
console.log(source_default.bold(`
|
|
3690
|
-
Simulation: ${scenario}
|
|
3691
|
-
`));
|
|
3692
|
-
|
|
3693
|
-
|
|
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."));
|
|
3694
4053
|
});
|
|
3695
4054
|
program2.command("staleness [dir]").action((d = ".") => {
|
|
3696
4055
|
const dir = resolvePath2(d);
|
|
3697
|
-
const dirs = [
|
|
4056
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3698
4057
|
console.log(source_default.bold(`Staleness Audit
|
|
3699
4058
|
`));
|
|
3700
4059
|
for (const dd of dirs) {
|
|
3701
|
-
if (!
|
|
4060
|
+
if (!existsSync3(dd))
|
|
3702
4061
|
continue;
|
|
3703
|
-
const projects =
|
|
4062
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3704
4063
|
for (const p of projects) {
|
|
3705
|
-
const pd =
|
|
3706
|
-
if (!
|
|
4064
|
+
const pd = join3(dd, p);
|
|
4065
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3707
4066
|
continue;
|
|
3708
|
-
if (!
|
|
4067
|
+
if (!existsSync3(pd))
|
|
3709
4068
|
continue;
|
|
3710
|
-
const planFile =
|
|
3711
|
-
if (!
|
|
4069
|
+
const planFile = join3(pd, "plan.dog");
|
|
4070
|
+
if (!existsSync3(planFile)) {
|
|
3712
4071
|
console.log(source_default.yellow(` ${p}: No plan.dog`));
|
|
3713
4072
|
continue;
|
|
3714
4073
|
}
|
|
3715
|
-
const plan =
|
|
4074
|
+
const plan = readFileSync3(planFile, "utf-8");
|
|
3716
4075
|
const tasks = [...plan.matchAll(/^\s*- \[([ x])\]\s+(.+)/gm)];
|
|
3717
4076
|
let issues = 0;
|
|
3718
4077
|
for (const m of tasks) {
|
|
@@ -3723,14 +4082,15 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
3723
4082
|
const phase = phaseMatch ? parseInt(phaseMatch[1]) : 99;
|
|
3724
4083
|
if (phase > 3)
|
|
3725
4084
|
continue;
|
|
3726
|
-
if (text.includes("npm publish") || text.includes("npm install")) {
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
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) {
|
|
3730
4090
|
console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
|
|
3731
4091
|
issues++;
|
|
3732
4092
|
}
|
|
3733
|
-
}
|
|
4093
|
+
}
|
|
3734
4094
|
}
|
|
3735
4095
|
if (text.includes("compile")) {
|
|
3736
4096
|
if (!done) {
|
|
@@ -3739,7 +4099,7 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
3739
4099
|
}
|
|
3740
4100
|
}
|
|
3741
4101
|
if (text.includes("generate") && !done) {
|
|
3742
|
-
if (
|
|
4102
|
+
if (existsSync3(join3(dir, "packages/dotdog/src/cli.ts")) || existsSync3(join3(dir, "packages/spec-cli/src/generate.ts"))) {
|
|
3743
4103
|
console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
|
|
3744
4104
|
issues++;
|
|
3745
4105
|
}
|
|
@@ -3759,6 +4119,206 @@ program2.command("woof").action(() => {
|
|
|
3759
4119
|
console.log(" / (_____/");
|
|
3760
4120
|
console.log("/_____/ U");
|
|
3761
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
|
+
});
|
|
3762
4322
|
program2.parse();
|
|
3763
4323
|
export {
|
|
3764
4324
|
parseToJSON,
|