dotdog 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -52
- package/dist/cli.js +763 -118
- 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();
|
|
@@ -2967,29 +3108,63 @@ function resolvePath(p) {
|
|
|
2967
3108
|
}
|
|
2968
3109
|
return resolved;
|
|
2969
3110
|
}
|
|
3111
|
+
var isV2 = (n) => Array.isArray(n) && typeof n[0] === "number";
|
|
2970
3112
|
var N = (dag) => dag.n || dag.nodes || [];
|
|
2971
|
-
|
|
3113
|
+
function edgeToObj(n, tgtIdx, v2e) {
|
|
3114
|
+
return { t: String(tgtIdx), v: v2e[1] || "", c: v2e[2] || "", r: v2e[3] || 0 };
|
|
3115
|
+
}
|
|
3116
|
+
function nodeEdges(n) {
|
|
3117
|
+
if (isV2(n))
|
|
3118
|
+
return (n[6] || []).map((e) => edgeToObj(n, e[0], e));
|
|
3119
|
+
return n.es || [];
|
|
3120
|
+
}
|
|
3121
|
+
function E(dag) {
|
|
3122
|
+
if (dag.e)
|
|
3123
|
+
return dag.e;
|
|
3124
|
+
const edges = [];
|
|
3125
|
+
const seen = new Set;
|
|
3126
|
+
for (const node of N(dag)) {
|
|
3127
|
+
for (const e of nodeEdges(node)) {
|
|
3128
|
+
const src = isV2(node) ? String(node[0]) : node.i || node.id || "";
|
|
3129
|
+
const key = `${src}→${e.t}:${e.v}`;
|
|
3130
|
+
if (!seen.has(key)) {
|
|
3131
|
+
seen.add(key);
|
|
3132
|
+
edges.push({ s: src, t: e.t, v: e.v, d: e.d, c: e.c, r: e.r });
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
return edges;
|
|
3137
|
+
}
|
|
2972
3138
|
var P = (dag) => dag.p || dag.project || "";
|
|
2973
|
-
var ni = (n) => n.i || n.id || "";
|
|
2974
|
-
var nt = (n) => n.t || n.type || "";
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
3139
|
+
var ni = (n) => isV2(n) ? String(n[0]) : n.i || n.id || "";
|
|
3140
|
+
var nt = (n) => isV2(n) ? n[2] || "" : n.t || n.type || "";
|
|
3141
|
+
function np(n) {
|
|
3142
|
+
if (isV2(n)) {
|
|
3143
|
+
const flat = n[4] || [];
|
|
3144
|
+
const obj = {};
|
|
3145
|
+
for (let i = 0;i < flat.length; i += 2)
|
|
3146
|
+
obj[flat[i]] = flat[i + 1] || "";
|
|
3147
|
+
return obj;
|
|
3148
|
+
}
|
|
3149
|
+
return n.p || n.properties || {};
|
|
3150
|
+
}
|
|
3151
|
+
var ns = (n) => isV2(n) ? n[5] || [] : n.s || n.states || [];
|
|
3152
|
+
var nl = (n) => isV2(n) ? [] : n.l || n.lifecycle || [];
|
|
2978
3153
|
var es = (e) => e.s || e.source || "";
|
|
2979
3154
|
var et = (e) => e.t || e.target || "";
|
|
2980
3155
|
function serve(dir = ".") {
|
|
2981
3156
|
const root = resolvePath(dir);
|
|
2982
3157
|
const dagCache = new Map;
|
|
2983
3158
|
function loadDags() {
|
|
2984
|
-
const dirs = [
|
|
3159
|
+
const dirs = [join2(root, "projects"), join2(root, "specs"), root];
|
|
2985
3160
|
for (const dd of dirs) {
|
|
2986
|
-
if (!
|
|
3161
|
+
if (!existsSync2(dd))
|
|
2987
3162
|
continue;
|
|
2988
|
-
const projects =
|
|
3163
|
+
const projects = readdirSync2(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2989
3164
|
for (const p of projects) {
|
|
2990
|
-
const dagFile =
|
|
2991
|
-
if (
|
|
2992
|
-
dagCache.set(p, JSON.parse(
|
|
3165
|
+
const dagFile = join2(dd, p, `${p}.dag`);
|
|
3166
|
+
if (existsSync2(dagFile)) {
|
|
3167
|
+
dagCache.set(p, JSON.parse(readFileSync2(dagFile, "utf-8")));
|
|
2993
3168
|
}
|
|
2994
3169
|
}
|
|
2995
3170
|
}
|
|
@@ -3044,7 +3219,7 @@ function serve(dir = ".") {
|
|
|
3044
3219
|
if (!node)
|
|
3045
3220
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "{}" }] } };
|
|
3046
3221
|
const edges = E(dag).filter((e) => es(e).toLowerCase() === ni(node).toLowerCase() || et(e).toLowerCase() === ni(node).toLowerCase());
|
|
3047
|
-
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({ ...node, edges }) }] } };
|
|
3222
|
+
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(isV2(node) ? { id: String(node[0]), name: node[1] || "", type: node[2] || "", description: node[3] || "", properties: np(node), states: ns(node), edges } : { ...node, edges }) }] } };
|
|
3048
3223
|
}
|
|
3049
3224
|
if (name === "traverse") {
|
|
3050
3225
|
const dag = dagCache.get(args.project || [...dagCache.keys()][0] || "");
|
|
@@ -3062,7 +3237,7 @@ function serve(dir = ".") {
|
|
|
3062
3237
|
visitedNodes.add(curr.id);
|
|
3063
3238
|
const node = N(dag).find((n) => ni(n).toLowerCase() === curr.id.toLowerCase());
|
|
3064
3239
|
if (node)
|
|
3065
|
-
subgraph.nodes.push(node);
|
|
3240
|
+
subgraph.nodes.push(isV2(node) ? { id: String(node[0]), name: node[1] || "", type: node[2] || "", description: node[3] || "", properties: np(node), states: ns(node), edges: nodeEdges(node) } : node);
|
|
3066
3241
|
const edges = E(dag).filter((e) => es(e).toLowerCase() === curr.id.toLowerCase() || et(e).toLowerCase() === curr.id.toLowerCase());
|
|
3067
3242
|
for (const e of edges) {
|
|
3068
3243
|
const edgeKey = `${es(e)}→${et(e)}`;
|
|
@@ -3096,6 +3271,8 @@ function serve(dir = ".") {
|
|
|
3096
3271
|
nodes: N(dag).length,
|
|
3097
3272
|
edges: E(dag).length,
|
|
3098
3273
|
version: dag.v || dag.version || "",
|
|
3274
|
+
order: dag.o || [],
|
|
3275
|
+
cycles: dag.cy !== undefined ? dag.cy : null,
|
|
3099
3276
|
savings: tk.sv || tk.savings_pct || 0,
|
|
3100
3277
|
method: tk.m || tk.method || ""
|
|
3101
3278
|
};
|
|
@@ -3138,8 +3315,8 @@ function serve(dir = ".") {
|
|
|
3138
3315
|
// src/cli.ts
|
|
3139
3316
|
function resolvePath2(p) {
|
|
3140
3317
|
if (p.startsWith("~"))
|
|
3141
|
-
p =
|
|
3142
|
-
const resolved = p.startsWith("/") ? p :
|
|
3318
|
+
p = join3(homedir2(), p.slice(1));
|
|
3319
|
+
const resolved = p.startsWith("/") ? p : join3(process.cwd(), p);
|
|
3143
3320
|
if (!p.startsWith("/") && !p.startsWith("~")) {
|
|
3144
3321
|
const rel = resolve2(process.cwd(), p);
|
|
3145
3322
|
const cwd = process.cwd();
|
|
@@ -3185,22 +3362,22 @@ function parseSections2(markdown) {
|
|
|
3185
3362
|
return sections;
|
|
3186
3363
|
}
|
|
3187
3364
|
var program2 = new Command;
|
|
3188
|
-
var pkg = JSON.parse(
|
|
3365
|
+
var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf-8"));
|
|
3189
3366
|
program2.name("spec").alias("dotdog").description("CLI for structured software specs : validate .dog, compile .dag, query via MCP").version(pkg.version);
|
|
3190
3367
|
program2.command("validate [dir]").action((d = ".") => {
|
|
3191
3368
|
const dir = resolvePath2(d);
|
|
3192
|
-
const dirs = [
|
|
3369
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs")];
|
|
3193
3370
|
let found = false, hasErrors = false;
|
|
3194
3371
|
for (const dd of dirs) {
|
|
3195
|
-
if (!
|
|
3372
|
+
if (!existsSync3(dd))
|
|
3196
3373
|
continue;
|
|
3197
|
-
const projects =
|
|
3374
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3198
3375
|
for (const p of projects) {
|
|
3199
3376
|
found = true;
|
|
3200
|
-
const pd =
|
|
3201
|
-
if (!
|
|
3377
|
+
const pd = join3(dd, p);
|
|
3378
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3202
3379
|
continue;
|
|
3203
|
-
const files =
|
|
3380
|
+
const files = existsSync3(pd) ? readdirSync3(pd).filter((f) => f.endsWith(".dog")) : [];
|
|
3204
3381
|
const missing = ["SPEC.dog", "constitution.dog", "data-model.dog"].filter((f) => !files.includes(f));
|
|
3205
3382
|
const optional = ["COPY.dog", "plan.dog", "DESIGN-SYSTEM.dog", "INDEX.dog"].filter((f) => !files.includes(f));
|
|
3206
3383
|
console.log(source_default.bold(`
|
|
@@ -3221,7 +3398,7 @@ program2.command("validate [dir]").action((d = ".") => {
|
|
|
3221
3398
|
process.exit(1);
|
|
3222
3399
|
});
|
|
3223
3400
|
program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data-model.dog").action((p, opts) => {
|
|
3224
|
-
const d =
|
|
3401
|
+
const d = join3(process.cwd(), "specs", p);
|
|
3225
3402
|
mkdirSync(d, { recursive: true });
|
|
3226
3403
|
const full = {
|
|
3227
3404
|
"SPEC.dog": `# Project
|
|
@@ -3269,7 +3446,13 @@ program2.command("init <project>").option("-m, --minimal", "Only SPEC.dog + data
|
|
|
3269
3446
|
};
|
|
3270
3447
|
const tmpl = opts.minimal ? minimal : full;
|
|
3271
3448
|
for (const [f, c] of Object.entries(tmpl)) {
|
|
3272
|
-
|
|
3449
|
+
writeFileSync2(join3(d, f), c);
|
|
3450
|
+
try {
|
|
3451
|
+
parse(c);
|
|
3452
|
+
} catch (_) {
|
|
3453
|
+
console.log(source_default.red(` ✗ Template ${f} is invalid`));
|
|
3454
|
+
process.exit(1);
|
|
3455
|
+
}
|
|
3273
3456
|
console.log(source_default.green(` ✓ ${f}`));
|
|
3274
3457
|
}
|
|
3275
3458
|
console.log(source_default.bold(`
|
|
@@ -3277,23 +3460,23 @@ Project "${p}" initialized. Fill in SPEC.dog then run spec validate.`));
|
|
|
3277
3460
|
});
|
|
3278
3461
|
program2.command("list").action(() => {
|
|
3279
3462
|
for (const d of ["projects", "specs"]) {
|
|
3280
|
-
const dd =
|
|
3281
|
-
if (!
|
|
3463
|
+
const dd = join3(process.cwd(), d);
|
|
3464
|
+
if (!existsSync3(dd))
|
|
3282
3465
|
continue;
|
|
3283
|
-
const projects =
|
|
3466
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3284
3467
|
if (!projects.length)
|
|
3285
3468
|
continue;
|
|
3286
3469
|
console.log(source_default.bold(`
|
|
3287
3470
|
${d}/`));
|
|
3288
3471
|
for (const p of projects) {
|
|
3289
|
-
const sp =
|
|
3290
|
-
const n =
|
|
3472
|
+
const sp = join3(dd, p);
|
|
3473
|
+
const n = existsSync3(sp) ? readdirSync3(sp).filter((f) => f.endsWith(".dog")).length : 0;
|
|
3291
3474
|
console.log(` ${source_default.cyan(p)} : ${n} .dog files`);
|
|
3292
3475
|
}
|
|
3293
3476
|
}
|
|
3294
3477
|
});
|
|
3295
3478
|
program2.command("parse <file>").action((f) => {
|
|
3296
|
-
const c =
|
|
3479
|
+
const c = readFileSync3(f, "utf-8");
|
|
3297
3480
|
const s = parseSections2(c);
|
|
3298
3481
|
console.log(source_default.bold(`
|
|
3299
3482
|
${s.length} sections`));
|
|
@@ -3302,26 +3485,26 @@ ${s.length} sections`));
|
|
|
3302
3485
|
});
|
|
3303
3486
|
program2.command("compile [dir]").option("-o, --output <file>").action((d = ".", opts) => {
|
|
3304
3487
|
const dir = resolvePath2(d);
|
|
3305
|
-
const dirs = [
|
|
3488
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3306
3489
|
let found = false;
|
|
3307
3490
|
for (const dd of dirs) {
|
|
3308
|
-
if (!
|
|
3491
|
+
if (!existsSync3(dd))
|
|
3309
3492
|
continue;
|
|
3310
|
-
const projects =
|
|
3493
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3311
3494
|
for (const p of projects) {
|
|
3312
|
-
const pd =
|
|
3313
|
-
if (!
|
|
3495
|
+
const pd = join3(dd, p);
|
|
3496
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3314
3497
|
continue;
|
|
3315
|
-
if (!
|
|
3498
|
+
if (!existsSync3(pd))
|
|
3316
3499
|
continue;
|
|
3317
|
-
const files =
|
|
3500
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog")).sort();
|
|
3318
3501
|
if (!files.length)
|
|
3319
3502
|
continue;
|
|
3320
3503
|
found = true;
|
|
3321
3504
|
const sources = {};
|
|
3322
3505
|
let sourceBytes = 0, contentBytes = 0;
|
|
3323
3506
|
for (const f of files) {
|
|
3324
|
-
const content =
|
|
3507
|
+
const content = readFileSync3(join3(pd, f), "utf-8");
|
|
3325
3508
|
sources[f] = content;
|
|
3326
3509
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
3327
3510
|
sourceBytes += bytes;
|
|
@@ -3394,16 +3577,72 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3394
3577
|
}
|
|
3395
3578
|
}
|
|
3396
3579
|
}
|
|
3397
|
-
const
|
|
3580
|
+
const entityNames = new Set(nodes.map((n) => n.i));
|
|
3581
|
+
for (const e of edges) {
|
|
3582
|
+
if (e.s && !entityNames.has(e.s)) {
|
|
3583
|
+
console.log(source_default.red(` ✗ Unknown relationship source "${e.s}" (target: "${e.t}")`));
|
|
3584
|
+
process.exit(1);
|
|
3585
|
+
}
|
|
3586
|
+
if (e.t && !entityNames.has(e.t)) {
|
|
3587
|
+
console.log(source_default.red(` ✗ Unknown relationship target "${e.t}" (source: "${e.s}")`));
|
|
3588
|
+
process.exit(1);
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
const nodeIds = new Map;
|
|
3592
|
+
nodes.forEach((n, i) => nodeIds.set(n.i, i));
|
|
3593
|
+
const v2nodes = [];
|
|
3594
|
+
for (let j = 0;j < nodes.length; j++) {
|
|
3595
|
+
const nd = nodes[j];
|
|
3596
|
+
const props = [];
|
|
3597
|
+
if (nd.p)
|
|
3598
|
+
for (const [k, v] of Object.entries(nd.p))
|
|
3599
|
+
props.push(k, v);
|
|
3600
|
+
const states = nd.s || [];
|
|
3601
|
+
const outEdges = [];
|
|
3602
|
+
const seen = new Set;
|
|
3603
|
+
for (const e of edges) {
|
|
3604
|
+
if (e.s !== nd.i && e.t !== nd.i)
|
|
3605
|
+
continue;
|
|
3606
|
+
const tid = nodeIds.get(e.s === nd.i ? e.t : e.s);
|
|
3607
|
+
if (tid === undefined)
|
|
3608
|
+
continue;
|
|
3609
|
+
const key = `${j}→${tid}:${e.v}`;
|
|
3610
|
+
if (seen.has(key))
|
|
3611
|
+
continue;
|
|
3612
|
+
seen.add(key);
|
|
3613
|
+
const ee = [tid, e.v || ""];
|
|
3614
|
+
if (e.c)
|
|
3615
|
+
ee.push(e.c);
|
|
3616
|
+
if (e.r)
|
|
3617
|
+
ee.push(1);
|
|
3618
|
+
outEdges.push(ee);
|
|
3619
|
+
}
|
|
3620
|
+
const entry = [j, nd.i || "", nd.t || "", nd.d || "", props, states, outEdges];
|
|
3621
|
+
if (nd.g === "prediction") {
|
|
3622
|
+
const f = [];
|
|
3623
|
+
if (nd.cf)
|
|
3624
|
+
f.push(nd.cf);
|
|
3625
|
+
if (nd.tf)
|
|
3626
|
+
f.push(nd.tf);
|
|
3627
|
+
if (nd.tg)
|
|
3628
|
+
f.push(nd.tg);
|
|
3629
|
+
if (nd.ms)
|
|
3630
|
+
f.push(nd.ms);
|
|
3631
|
+
if (f.length)
|
|
3632
|
+
entry.push(f);
|
|
3633
|
+
}
|
|
3634
|
+
v2nodes.push(entry);
|
|
3635
|
+
}
|
|
3636
|
+
const dag = { v: 2, p, n: v2nodes };
|
|
3398
3637
|
const dagJson = JSON.stringify(dag);
|
|
3399
3638
|
const dagTokens = Math.round(Buffer.byteLength(dagJson, "utf-8") / 4);
|
|
3400
3639
|
const allSavingsPct = sourceTokens > 0 ? Math.round((1 - dagTokens / sourceTokens) * 1000) / 10 : 0;
|
|
3401
3640
|
const contentSavingsPct = contentTokens > 0 ? Math.round((1 - dagTokens / contentTokens) * 1000) / 10 : 0;
|
|
3402
3641
|
const savingsTokens = sourceTokens - dagTokens;
|
|
3403
|
-
const outPath = opts.output ||
|
|
3642
|
+
const outPath = opts.output || join3(pd, `${p}.dag`);
|
|
3404
3643
|
const tokens = { m: "chars/4", st: sourceTokens, ct: contentTokens, dt: dagTokens, sv: allSavingsPct, cs: contentSavingsPct, saved: savingsTokens };
|
|
3405
3644
|
const report = { ...dag, tk: tokens };
|
|
3406
|
-
|
|
3645
|
+
writeFileSync2(outPath, JSON.stringify(report, null, 2));
|
|
3407
3646
|
console.log(source_default.green(` ✓ ${outPath}`));
|
|
3408
3647
|
console.log(source_default.gray(` ${nodes.length} nodes, ${edges.length} edges, ${files.length} files`));
|
|
3409
3648
|
console.log(source_default.gray(` ${sourceTokens} → ${dagTokens} tokens (${allSavingsPct}% savings, ${contentSavingsPct}% content-only, ${savingsTokens} saved)`));
|
|
@@ -3414,37 +3653,41 @@ program2.command("compile [dir]").option("-o, --output <file>").action((d = ".",
|
|
|
3414
3653
|
});
|
|
3415
3654
|
program2.command("tokens [dir]").action((d = ".") => {
|
|
3416
3655
|
const dir = resolvePath2(d);
|
|
3417
|
-
const dirs = [
|
|
3656
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3418
3657
|
let found = false;
|
|
3419
3658
|
for (const dd of dirs) {
|
|
3420
|
-
if (!
|
|
3659
|
+
if (!existsSync3(dd))
|
|
3421
3660
|
continue;
|
|
3422
|
-
const projects =
|
|
3661
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3423
3662
|
for (const p of projects) {
|
|
3424
|
-
const pd =
|
|
3425
|
-
if (!
|
|
3663
|
+
const pd = join3(dd, p);
|
|
3664
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3426
3665
|
continue;
|
|
3427
|
-
const dagFile =
|
|
3428
|
-
if (!
|
|
3666
|
+
const dagFile = join3(pd, `${p}.dag`);
|
|
3667
|
+
if (!existsSync3(dagFile))
|
|
3429
3668
|
continue;
|
|
3430
3669
|
found = true;
|
|
3431
|
-
const dogFiles =
|
|
3670
|
+
const dogFiles = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
3432
3671
|
let sourceBytes = 0, contentBytes = 0;
|
|
3433
3672
|
for (const f of dogFiles) {
|
|
3434
|
-
const bytes = Buffer.byteLength(
|
|
3673
|
+
const bytes = Buffer.byteLength(readFileSync3(join3(pd, f), "utf-8"), "utf-8");
|
|
3435
3674
|
sourceBytes += bytes;
|
|
3436
3675
|
if (bytes >= 100)
|
|
3437
3676
|
contentBytes += bytes;
|
|
3438
3677
|
}
|
|
3439
|
-
const dagBytes = Buffer.byteLength(
|
|
3678
|
+
const dagBytes = Buffer.byteLength(readFileSync3(dagFile, "utf-8"), "utf-8");
|
|
3440
3679
|
const savings = sourceBytes > 0 ? Math.round((1 - dagBytes / sourceBytes) * 1000) / 10 : 0;
|
|
3680
|
+
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
3681
|
+
const dagOnly = JSON.stringify({ v: dag.v, p: dag.p, n: dag.n });
|
|
3682
|
+
const dagOnlyBytes = Buffer.byteLength(dagOnly, "utf-8");
|
|
3683
|
+
const dagOnlyPct = sourceBytes > 0 ? Math.round((1 - dagOnlyBytes / sourceBytes) * 1000) / 10 : 0;
|
|
3441
3684
|
console.log(source_default.bold(`
|
|
3442
3685
|
${p}`));
|
|
3443
3686
|
console.log(source_default.gray(` ${dogFiles.length} .dog files: ${sourceBytes} bytes`));
|
|
3444
|
-
console.log(source_default.gray(` .dag
|
|
3445
|
-
console.log(source_default.green(` ${
|
|
3687
|
+
console.log(source_default.gray(` .dag on disk: ${dagBytes} bytes (${savings}% savings, includes metadata)`));
|
|
3688
|
+
console.log(source_default.green(` .dag payload: ${dagOnlyBytes} bytes (${dagOnlyPct}% savings, graph only)`));
|
|
3446
3689
|
if (contentBytes && contentBytes !== sourceBytes) {
|
|
3447
|
-
const cs = Math.round((1 -
|
|
3690
|
+
const cs = Math.round((1 - dagOnlyBytes / contentBytes) * 1000) / 10;
|
|
3448
3691
|
console.log(source_default.gray(` content-only: ${contentBytes} bytes → ${cs}% savings`));
|
|
3449
3692
|
}
|
|
3450
3693
|
}
|
|
@@ -3454,27 +3697,67 @@ program2.command("tokens [dir]").action((d = ".") => {
|
|
|
3454
3697
|
});
|
|
3455
3698
|
program2.command("visualize [dir]").option("-s, --save").action((d = ".", opts) => {
|
|
3456
3699
|
const dir = resolvePath2(d);
|
|
3457
|
-
const dirs = [
|
|
3700
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3458
3701
|
for (const dd of dirs) {
|
|
3459
|
-
if (!
|
|
3702
|
+
if (!existsSync3(dd))
|
|
3460
3703
|
continue;
|
|
3461
|
-
const projects =
|
|
3704
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3462
3705
|
for (const p of projects) {
|
|
3463
|
-
const dagFile =
|
|
3464
|
-
if (!
|
|
3706
|
+
const dagFile = join3(dd, p, `${p}.dag`);
|
|
3707
|
+
if (!existsSync3(dagFile))
|
|
3465
3708
|
continue;
|
|
3466
|
-
const dag = JSON.parse(
|
|
3709
|
+
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
3710
|
+
const nodes = dag.n || dag.nodes || [];
|
|
3711
|
+
const isV22 = (n) => Array.isArray(n) && typeof n[0] === "number";
|
|
3712
|
+
const nodeName = (n) => isV22(n) ? nodes[n[0]] ? nodes[n[0]][1] || String(n[0]) : String(n[0]) : n.i || n.id || "";
|
|
3713
|
+
const slug = (s) => s.replace(/\s+/g, "_").replace(/^[^a-zA-Z]+/, "n_");
|
|
3467
3714
|
let out = "```mermaid\ngraph LR\n";
|
|
3468
|
-
for (const n of
|
|
3469
|
-
|
|
3715
|
+
for (const n of nodes) {
|
|
3716
|
+
const raw = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
|
|
3717
|
+
if (raw)
|
|
3718
|
+
out += ` ${slug(raw)}[${raw}]
|
|
3470
3719
|
`;
|
|
3471
|
-
|
|
3472
|
-
|
|
3720
|
+
}
|
|
3721
|
+
const seen = new Set;
|
|
3722
|
+
for (const n of nodes) {
|
|
3723
|
+
const edges = isV22(n) ? n[6] || [] : n.es || [];
|
|
3724
|
+
for (const e of edges) {
|
|
3725
|
+
const srcName = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
|
|
3726
|
+
const src = slug(srcName);
|
|
3727
|
+
let tgtName, verb;
|
|
3728
|
+
if (isV22(n)) {
|
|
3729
|
+
const tgtNode = nodes[e[0]];
|
|
3730
|
+
tgtName = tgtNode ? tgtNode[1] || String(tgtNode[0]) : String(e[0]);
|
|
3731
|
+
verb = e[1] || "";
|
|
3732
|
+
} else {
|
|
3733
|
+
tgtName = e.t || "";
|
|
3734
|
+
verb = e.v || "";
|
|
3735
|
+
}
|
|
3736
|
+
const tgt = slug(tgtName);
|
|
3737
|
+
const key = `${src}→${tgt}:${verb}`;
|
|
3738
|
+
if (!seen.has(key) && src && tgt) {
|
|
3739
|
+
seen.add(key);
|
|
3740
|
+
out += ` ${src} -->|${verb}| ${tgt}
|
|
3473
3741
|
`;
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
const legacyEdges = dag.e || dag.edges || [];
|
|
3746
|
+
for (const e of legacyEdges) {
|
|
3747
|
+
const src = slug(e.s || e.source || "");
|
|
3748
|
+
const tgt = slug(e.t || e.target || "");
|
|
3749
|
+
const verb = e.v || e.verb || "";
|
|
3750
|
+
const key = `${src}→${tgt}:${verb}`;
|
|
3751
|
+
if (!seen.has(key) && src && tgt) {
|
|
3752
|
+
seen.add(key);
|
|
3753
|
+
out += ` ${src} -->|${verb}| ${tgt}
|
|
3754
|
+
`;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3474
3757
|
out += "```\n";
|
|
3475
3758
|
if (opts.save) {
|
|
3476
|
-
const outFile =
|
|
3477
|
-
|
|
3759
|
+
const outFile = join3(dd, p, `${p}.md`);
|
|
3760
|
+
writeFileSync2(outFile, `# ${p} : Spec Graph
|
|
3478
3761
|
|
|
3479
3762
|
${out}`);
|
|
3480
3763
|
console.log(source_default.green(` ✓ ${outFile}`));
|
|
@@ -3486,24 +3769,24 @@ ${out}`);
|
|
|
3486
3769
|
program2.command("serve [dir]").description("MCP server : expose .dag graph to AI agents over stdio").action((d = ".") => serve(resolvePath2(d)));
|
|
3487
3770
|
program2.command("analyze [dir]").description("Analyze a spec project : score, gaps, suggestions").option("-p, --project <name>").action((d = ".", opts) => {
|
|
3488
3771
|
const dir = resolvePath2(d);
|
|
3489
|
-
const dirs = [
|
|
3772
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3490
3773
|
console.log(source_default.bold(`
|
|
3491
3774
|
Spec Analysis
|
|
3492
3775
|
`));
|
|
3493
3776
|
let found = false, hasGaps = false;
|
|
3494
3777
|
for (const dd of dirs) {
|
|
3495
|
-
if (!
|
|
3778
|
+
if (!existsSync3(dd))
|
|
3496
3779
|
continue;
|
|
3497
|
-
const projects =
|
|
3780
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3498
3781
|
for (const p of projects) {
|
|
3499
3782
|
if (opts.project && p !== opts.project)
|
|
3500
3783
|
continue;
|
|
3501
|
-
const pd =
|
|
3502
|
-
if (!
|
|
3784
|
+
const pd = join3(dd, p);
|
|
3785
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3503
3786
|
continue;
|
|
3504
|
-
if (!
|
|
3787
|
+
if (!existsSync3(pd))
|
|
3505
3788
|
continue;
|
|
3506
|
-
const files =
|
|
3789
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
3507
3790
|
if (!files.length)
|
|
3508
3791
|
continue;
|
|
3509
3792
|
found = true;
|
|
@@ -3514,7 +3797,7 @@ Spec Analysis
|
|
|
3514
3797
|
const allRelationships = [];
|
|
3515
3798
|
const analyses = [];
|
|
3516
3799
|
for (const f of files) {
|
|
3517
|
-
const content =
|
|
3800
|
+
const content = readFileSync3(join3(pd, f), "utf-8");
|
|
3518
3801
|
const ast = parse(content);
|
|
3519
3802
|
const entities = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "entity"));
|
|
3520
3803
|
const rels = ast.sections.flatMap((s) => s.blocks.filter((b) => b.kind === "relationship"));
|
|
@@ -3556,6 +3839,39 @@ Spec Analysis
|
|
|
3556
3839
|
if (r.target && !entityNames.has(r.target))
|
|
3557
3840
|
gaps.push(`\uD83D\uDFE1 Relationship: unknown target "${r.target}"`);
|
|
3558
3841
|
}
|
|
3842
|
+
const contradictions = [];
|
|
3843
|
+
const relMap = new Map;
|
|
3844
|
+
for (const r of allRelationships) {
|
|
3845
|
+
const key = `${r.source}→${r.target}`;
|
|
3846
|
+
if (!relMap.has(key))
|
|
3847
|
+
relMap.set(key, []);
|
|
3848
|
+
relMap.get(key).push(r);
|
|
3849
|
+
}
|
|
3850
|
+
for (const [key, rels] of relMap) {
|
|
3851
|
+
if (rels.length > 1) {
|
|
3852
|
+
const verbs = [...new Set(rels.map((r) => r.verb))];
|
|
3853
|
+
if (verbs.length > 1)
|
|
3854
|
+
contradictions.push(`\uD83D\uDD34 Contradiction: "${key}" has conflicting verbs: ${verbs.join(", ")}`);
|
|
3855
|
+
else
|
|
3856
|
+
contradictions.push(`\uD83D\uDFE1 Duplicate: "${key}" appears ${rels.length} times with same verb "${verbs[0]}"`);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
for (const r of allRelationships) {
|
|
3860
|
+
const reverse = allRelationships.find((r2) => r2.source === r.target && r2.target === r.source);
|
|
3861
|
+
if (reverse && r.verb !== reverse.verb && r.source < r.target) {
|
|
3862
|
+
contradictions.push(`\uD83D\uDFE1 Bidirectional: "${r.source}→${r.target}" verb "${r.verb}" vs "${reverse.source}→${reverse.target}" verb "${reverse.verb}"`);
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
for (const c of contradictions) {
|
|
3866
|
+
if (c.startsWith("\uD83D\uDD34"))
|
|
3867
|
+
hasGaps = true;
|
|
3868
|
+
}
|
|
3869
|
+
if (contradictions.length > 0) {
|
|
3870
|
+
console.log(source_default.bold(`
|
|
3871
|
+
Contradictions (${contradictions.length})`));
|
|
3872
|
+
for (const c of contradictions)
|
|
3873
|
+
console.log(` ${c}`);
|
|
3874
|
+
}
|
|
3559
3875
|
if (gaps.length > 0) {
|
|
3560
3876
|
console.log(source_default.bold(`
|
|
3561
3877
|
Gaps (${gaps.length})`));
|
|
@@ -3575,19 +3891,19 @@ Spec Analysis
|
|
|
3575
3891
|
});
|
|
3576
3892
|
program2.command("generate [dir]").description("Generate missing spec files from SPEC.dog").option("-p, --project <name>").action((d = ".", opts) => {
|
|
3577
3893
|
const dir = resolvePath2(d);
|
|
3578
|
-
const dirs = [
|
|
3894
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3579
3895
|
let specContent = "", specDir = "";
|
|
3580
3896
|
for (const dd of dirs) {
|
|
3581
|
-
if (!
|
|
3897
|
+
if (!existsSync3(dd))
|
|
3582
3898
|
continue;
|
|
3583
|
-
const projects =
|
|
3899
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3584
3900
|
for (const p of projects) {
|
|
3585
3901
|
if (opts.project && p !== opts.project)
|
|
3586
3902
|
continue;
|
|
3587
|
-
const pd =
|
|
3588
|
-
const sp =
|
|
3589
|
-
if (
|
|
3590
|
-
specContent =
|
|
3903
|
+
const pd = join3(dd, p);
|
|
3904
|
+
const sp = join3(pd, "SPEC.dog");
|
|
3905
|
+
if (existsSync3(sp)) {
|
|
3906
|
+
specContent = readFileSync3(sp, "utf-8");
|
|
3591
3907
|
specDir = pd;
|
|
3592
3908
|
break;
|
|
3593
3909
|
}
|
|
@@ -3618,7 +3934,7 @@ Spec Generator
|
|
|
3618
3934
|
uiStrings.push({ screen: section.heading, element: "label", text: m });
|
|
3619
3935
|
}
|
|
3620
3936
|
}
|
|
3621
|
-
if (!
|
|
3937
|
+
if (!existsSync3(join3(specDir, "data-model.dog")) && entities.length > 0) {
|
|
3622
3938
|
let dm = `# Data Model
|
|
3623
3939
|
|
|
3624
3940
|
## Core Entities
|
|
@@ -3651,10 +3967,16 @@ ${e.description || "No description."}
|
|
|
3651
3967
|
`;
|
|
3652
3968
|
dm += "```\n\n";
|
|
3653
3969
|
}
|
|
3654
|
-
|
|
3970
|
+
writeFileSync2(join3(specDir, "data-model.dog"), dm);
|
|
3971
|
+
try {
|
|
3972
|
+
parse(dm);
|
|
3973
|
+
} catch (_) {
|
|
3974
|
+
console.log(source_default.red(" ✗ Generated data-model.dog is invalid"));
|
|
3975
|
+
process.exit(1);
|
|
3976
|
+
}
|
|
3655
3977
|
console.log(source_default.green(` ✓ data-model.dog (${entities.length} entities)`));
|
|
3656
3978
|
}
|
|
3657
|
-
if (!
|
|
3979
|
+
if (!existsSync3(join3(specDir, "COPY.dog")) && uiStrings.length > 0) {
|
|
3658
3980
|
let copy = `# App Copy
|
|
3659
3981
|
|
|
3660
3982
|
| Screen | Element | Copy |
|
|
@@ -3663,10 +3985,16 @@ ${e.description || "No description."}
|
|
|
3663
3985
|
for (const s of uiStrings)
|
|
3664
3986
|
copy += `| ${s.screen} | ${s.element} | ${s.text} |
|
|
3665
3987
|
`;
|
|
3666
|
-
|
|
3988
|
+
writeFileSync2(join3(specDir, "COPY.dog"), copy);
|
|
3989
|
+
try {
|
|
3990
|
+
parse(copy);
|
|
3991
|
+
} catch (_) {
|
|
3992
|
+
console.log(source_default.red(" ✗ Generated COPY.dog is invalid"));
|
|
3993
|
+
process.exit(1);
|
|
3994
|
+
}
|
|
3667
3995
|
console.log(source_default.green(` ✓ COPY.dog (${uiStrings.length} strings)`));
|
|
3668
3996
|
}
|
|
3669
|
-
if (!
|
|
3997
|
+
if (!existsSync3(join3(specDir, "INDEX.dog"))) {
|
|
3670
3998
|
let idx = `# INDEX
|
|
3671
3999
|
|
|
3672
4000
|
| You are... | Start here | Then... |
|
|
@@ -3678,41 +4006,149 @@ ${e.description || "No description."}
|
|
|
3678
4006
|
`;
|
|
3679
4007
|
idx += `| Designer | SPEC.dog | COPY.dog |
|
|
3680
4008
|
`;
|
|
3681
|
-
|
|
4009
|
+
writeFileSync2(join3(specDir, "INDEX.dog"), idx);
|
|
4010
|
+
try {
|
|
4011
|
+
parse(idx);
|
|
4012
|
+
} catch (_) {
|
|
4013
|
+
console.log(source_default.red(" ✗ Generated INDEX.dog is invalid"));
|
|
4014
|
+
process.exit(1);
|
|
4015
|
+
}
|
|
3682
4016
|
console.log(source_default.green(" ✓ INDEX.dog"));
|
|
3683
4017
|
}
|
|
3684
4018
|
console.log(source_default.bold(`
|
|
3685
4019
|
Run dotdog validate to verify.
|
|
3686
4020
|
`));
|
|
3687
4021
|
});
|
|
3688
|
-
program2.command("simulate <scenario>").description("
|
|
4022
|
+
program2.command("simulate <scenario>").description("Walk through a scenario, check pre/postconditions").option("-p, --project <name>").action((scenario, opts) => {
|
|
4023
|
+
const dir = resolvePath2(".");
|
|
4024
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4025
|
+
let projectDir = "";
|
|
4026
|
+
for (const dd of dirs) {
|
|
4027
|
+
if (!existsSync3(dd))
|
|
4028
|
+
continue;
|
|
4029
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4030
|
+
for (const p of projects) {
|
|
4031
|
+
if (opts.project && p !== opts.project)
|
|
4032
|
+
continue;
|
|
4033
|
+
const pd = join3(dd, p);
|
|
4034
|
+
if (existsSync3(join3(pd, "SPEC.dog"))) {
|
|
4035
|
+
projectDir = pd;
|
|
4036
|
+
break;
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
if (projectDir)
|
|
4040
|
+
break;
|
|
4041
|
+
}
|
|
4042
|
+
if (!projectDir) {
|
|
4043
|
+
console.log(source_default.red("Project not found."));
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
const dagFile = join3(projectDir, `${opts.project || projectDir.split("/").pop()}.dag`);
|
|
4047
|
+
let entities = [], relationships = [];
|
|
4048
|
+
if (existsSync3(dagFile)) {
|
|
4049
|
+
const dag = JSON.parse(readFileSync3(dagFile, "utf-8"));
|
|
4050
|
+
const simNodes = dag.n || dag.nodes || [];
|
|
4051
|
+
const isV22 = (n) => Array.isArray(n) && typeof n[0] === "number";
|
|
4052
|
+
entities = simNodes.map((n) => (isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "").toLowerCase());
|
|
4053
|
+
if (dag.e || dag.edges) {
|
|
4054
|
+
relationships = dag.e || dag.edges || [];
|
|
4055
|
+
} else {
|
|
4056
|
+
const seen = new Set;
|
|
4057
|
+
for (const n of simNodes) {
|
|
4058
|
+
const edges = isV22(n) ? n[6] || [] : n.es || [];
|
|
4059
|
+
for (const e of edges) {
|
|
4060
|
+
const srcName = isV22(n) ? n[1] || String(n[0]) : n.i || n.id || "";
|
|
4061
|
+
const tgtName = isV22(n) ? (simNodes[e[0]] ? simNodes[e[0]][2] : "") || String(e[0]) : e.t || "";
|
|
4062
|
+
const verb = isV22(n) ? e[1] || "" : e.v || "";
|
|
4063
|
+
const key = `${srcName}→${tgtName}:${verb}`;
|
|
4064
|
+
if (!seen.has(key)) {
|
|
4065
|
+
seen.add(key);
|
|
4066
|
+
relationships.push({ s: srcName, t: tgtName, v: verb });
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
const specFile = join3(projectDir, "SPEC.dog");
|
|
4073
|
+
const specContent = existsSync3(specFile) ? readFileSync3(specFile, "utf-8") : "";
|
|
3689
4074
|
console.log(source_default.bold(`
|
|
3690
|
-
Simulation: ${scenario}
|
|
3691
|
-
`));
|
|
3692
|
-
|
|
3693
|
-
|
|
4075
|
+
Simulation: ${scenario}`));
|
|
4076
|
+
console.log(source_default.gray(`Project: ${projectDir.split("/").pop()} | Entities: ${entities.length} | Relationships: ${relationships.length}`));
|
|
4077
|
+
const steps = [];
|
|
4078
|
+
const stepMatches = specContent.match(/\[(\d+)\/\d+\]\s*(.+)/g);
|
|
4079
|
+
if (stepMatches) {
|
|
4080
|
+
for (const m of stepMatches) {
|
|
4081
|
+
const step = m.replace(/\[\d+\/\d+\]\s*/, "");
|
|
4082
|
+
if (step)
|
|
4083
|
+
steps.push(step);
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
if (steps.length === 0) {
|
|
4087
|
+
console.log(source_default.yellow(`
|
|
4088
|
+
No scenario steps found in SPEC.dog.`));
|
|
4089
|
+
console.log(source_default.gray(" Add steps like: [1/3] User taps button"));
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
let passed = 0, failed = 0;
|
|
4093
|
+
for (let i = 0;i < steps.length; i++) {
|
|
4094
|
+
const step = steps[i];
|
|
4095
|
+
console.log(source_default.cyan(`
|
|
4096
|
+
[${i + 1}/${steps.length}] ${step}`));
|
|
4097
|
+
let foundRef = false;
|
|
4098
|
+
for (const e of entities) {
|
|
4099
|
+
if (step.toLowerCase().includes(e)) {
|
|
4100
|
+
console.log(source_default.green(` ✓ References entity: ${e}`));
|
|
4101
|
+
foundRef = true;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
for (const r of relationships) {
|
|
4105
|
+
const src = (r.s || r.source || "").toLowerCase();
|
|
4106
|
+
const tgt = (r.t || r.target || "").toLowerCase();
|
|
4107
|
+
const verb = (r.v || r.verb || "").toLowerCase();
|
|
4108
|
+
if (step.toLowerCase().includes(src) && step.toLowerCase().includes(tgt)) {
|
|
4109
|
+
console.log(source_default.green(` ✓ References relationship: ${src} → ${tgt}`));
|
|
4110
|
+
foundRef = true;
|
|
4111
|
+
}
|
|
4112
|
+
if (verb && step.toLowerCase().includes(verb)) {
|
|
4113
|
+
console.log(source_default.green(` ✓ References verb: ${verb}`));
|
|
4114
|
+
foundRef = true;
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
if (!foundRef) {
|
|
4118
|
+
console.log(source_default.yellow(` ⚠ No entity or relationship references found`));
|
|
4119
|
+
failed++;
|
|
4120
|
+
} else {
|
|
4121
|
+
passed++;
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
console.log(source_default.bold(`
|
|
4125
|
+
RESULT: ${failed === 0 ? source_default.green("Success") : source_default.yellow("Partial")} (${passed}/${steps.length} steps passed)`));
|
|
4126
|
+
if (failed > 0)
|
|
4127
|
+
console.log(source_default.red(` ${failed} steps have no spec references — entities or relationships may be missing.`));
|
|
4128
|
+
if (failed === 0)
|
|
4129
|
+
console.log(source_default.green(" All steps reference known entities and relationships."));
|
|
3694
4130
|
});
|
|
3695
4131
|
program2.command("staleness [dir]").action((d = ".") => {
|
|
3696
4132
|
const dir = resolvePath2(d);
|
|
3697
|
-
const dirs = [
|
|
4133
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
3698
4134
|
console.log(source_default.bold(`Staleness Audit
|
|
3699
4135
|
`));
|
|
3700
4136
|
for (const dd of dirs) {
|
|
3701
|
-
if (!
|
|
4137
|
+
if (!existsSync3(dd))
|
|
3702
4138
|
continue;
|
|
3703
|
-
const projects =
|
|
4139
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3704
4140
|
for (const p of projects) {
|
|
3705
|
-
const pd =
|
|
3706
|
-
if (!
|
|
4141
|
+
const pd = join3(dd, p);
|
|
4142
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
3707
4143
|
continue;
|
|
3708
|
-
if (!
|
|
4144
|
+
if (!existsSync3(pd))
|
|
3709
4145
|
continue;
|
|
3710
|
-
const planFile =
|
|
3711
|
-
if (!
|
|
4146
|
+
const planFile = join3(pd, "plan.dog");
|
|
4147
|
+
if (!existsSync3(planFile)) {
|
|
3712
4148
|
console.log(source_default.yellow(` ${p}: No plan.dog`));
|
|
3713
4149
|
continue;
|
|
3714
4150
|
}
|
|
3715
|
-
const plan =
|
|
4151
|
+
const plan = readFileSync3(planFile, "utf-8");
|
|
3716
4152
|
const tasks = [...plan.matchAll(/^\s*- \[([ x])\]\s+(.+)/gm)];
|
|
3717
4153
|
let issues = 0;
|
|
3718
4154
|
for (const m of tasks) {
|
|
@@ -3723,14 +4159,15 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
3723
4159
|
const phase = phaseMatch ? parseInt(phaseMatch[1]) : 99;
|
|
3724
4160
|
if (phase > 3)
|
|
3725
4161
|
continue;
|
|
3726
|
-
if (text.includes("npm publish") || text.includes("npm install")) {
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
4162
|
+
if ((text.includes("npm publish") || text.includes("npm install")) && !done) {
|
|
4163
|
+
const pkgPath = join3(resolvePath2("."), "packages/dotdog/package.json");
|
|
4164
|
+
if (existsSync3(pkgPath)) {
|
|
4165
|
+
const pkg2 = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
4166
|
+
if (pkg2.version) {
|
|
3730
4167
|
console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
|
|
3731
4168
|
issues++;
|
|
3732
4169
|
}
|
|
3733
|
-
}
|
|
4170
|
+
}
|
|
3734
4171
|
}
|
|
3735
4172
|
if (text.includes("compile")) {
|
|
3736
4173
|
if (!done) {
|
|
@@ -3739,7 +4176,7 @@ program2.command("staleness [dir]").action((d = ".") => {
|
|
|
3739
4176
|
}
|
|
3740
4177
|
}
|
|
3741
4178
|
if (text.includes("generate") && !done) {
|
|
3742
|
-
if (
|
|
4179
|
+
if (existsSync3(join3(dir, "packages/dotdog/src/cli.ts")) || existsSync3(join3(dir, "packages/spec-cli/src/generate.ts"))) {
|
|
3743
4180
|
console.log(source_default.yellow(` ⚠ Should be [x]: ${m[2].trim()}`));
|
|
3744
4181
|
issues++;
|
|
3745
4182
|
}
|
|
@@ -3759,6 +4196,214 @@ program2.command("woof").action(() => {
|
|
|
3759
4196
|
console.log(" / (_____/");
|
|
3760
4197
|
console.log("/_____/ U");
|
|
3761
4198
|
});
|
|
4199
|
+
program2.command("index [dir]").description("Build search index for semantic queries").action((d = ".") => {
|
|
4200
|
+
const dir = resolvePath2(d);
|
|
4201
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4202
|
+
let built = 0;
|
|
4203
|
+
for (const dd of dirs) {
|
|
4204
|
+
if (!existsSync3(dd))
|
|
4205
|
+
continue;
|
|
4206
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4207
|
+
for (const p of projects) {
|
|
4208
|
+
const pd = join3(dd, p);
|
|
4209
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
4210
|
+
continue;
|
|
4211
|
+
const idx = buildIndex(pd, p);
|
|
4212
|
+
writeFileSync2(join3(pd, `${p}.idx`), JSON.stringify(idx));
|
|
4213
|
+
console.log(source_default.green(` ✓ ${p} : ${idx.entries.length} sections indexed (${idx.vocabulary.length} terms)`));
|
|
4214
|
+
built++;
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
if (!built)
|
|
4218
|
+
console.log(source_default.yellow("No projects found. Run dotdog init first."));
|
|
4219
|
+
});
|
|
4220
|
+
program2.command("search <query>").description("Semantic search across compiled specs").option("-p, --project <name>").action((query, opts) => {
|
|
4221
|
+
console.log(source_default.bold(`
|
|
4222
|
+
Search: "${query}"
|
|
4223
|
+
`));
|
|
4224
|
+
const dir = resolvePath2(".");
|
|
4225
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4226
|
+
let found = false;
|
|
4227
|
+
for (const dd of dirs) {
|
|
4228
|
+
if (!existsSync3(dd))
|
|
4229
|
+
continue;
|
|
4230
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4231
|
+
for (const p of projects) {
|
|
4232
|
+
if (opts.project && p !== opts.project)
|
|
4233
|
+
continue;
|
|
4234
|
+
const pd = join3(dd, p);
|
|
4235
|
+
const idxFile = join3(pd, `${p}.idx`);
|
|
4236
|
+
if (!existsSync3(idxFile))
|
|
4237
|
+
continue;
|
|
4238
|
+
found = true;
|
|
4239
|
+
const idx = JSON.parse(readFileSync3(idxFile, "utf-8"));
|
|
4240
|
+
const results = searchIndex(idx, query, 8);
|
|
4241
|
+
if (results.length === 0) {
|
|
4242
|
+
console.log(source_default.gray(` ${p}: No matches`));
|
|
4243
|
+
continue;
|
|
4244
|
+
}
|
|
4245
|
+
console.log(source_default.green(` ${p} — ${results.length} results:`));
|
|
4246
|
+
for (const r of results) {
|
|
4247
|
+
const preview = r.entry.content.replace(/\n/g, " ").slice(0, 100);
|
|
4248
|
+
console.log(source_default.gray(` ${Math.round(r.score * 100)}% [${r.entry.file}] ${r.entry.heading}`));
|
|
4249
|
+
console.log(source_default.gray(` ${preview}...`));
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
if (!found)
|
|
4254
|
+
console.log(source_default.yellow("No index found. Run dotdog index first."));
|
|
4255
|
+
});
|
|
4256
|
+
program2.command("predictions [dir]").description("List all predictions with status").option("-p, --project <name>").action((d = ".", opts) => {
|
|
4257
|
+
const dir = resolvePath2(d);
|
|
4258
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4259
|
+
console.log(source_default.bold(`
|
|
4260
|
+
Predictions
|
|
4261
|
+
`));
|
|
4262
|
+
let found = false;
|
|
4263
|
+
for (const dd of dirs) {
|
|
4264
|
+
if (!existsSync3(dd))
|
|
4265
|
+
continue;
|
|
4266
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4267
|
+
for (const p of projects) {
|
|
4268
|
+
if (opts.project && p !== opts.project)
|
|
4269
|
+
continue;
|
|
4270
|
+
const pd = join3(dd, p);
|
|
4271
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
4272
|
+
continue;
|
|
4273
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
4274
|
+
for (const f of files) {
|
|
4275
|
+
const content = readFileSync3(join3(pd, f), "utf-8");
|
|
4276
|
+
const ast = parse(content);
|
|
4277
|
+
for (const section of ast.sections) {
|
|
4278
|
+
for (const block of section.blocks) {
|
|
4279
|
+
if (block.kind === "prediction") {
|
|
4280
|
+
found = true;
|
|
4281
|
+
const b = block;
|
|
4282
|
+
const status = b.status || "pending";
|
|
4283
|
+
const icon = status === "correct" ? "✅" : status === "wrong" ? "❌" : status === "partial" ? "⚠️" : "⏳";
|
|
4284
|
+
console.log(` ${icon} ${b.statement || b.name} (${(b.confidence * 100).toFixed(0)}% confidence, ${status})`);
|
|
4285
|
+
if (b.measurement)
|
|
4286
|
+
console.log(source_default.gray(` Measurement: ${b.measurement}`));
|
|
4287
|
+
if (b.timeframe)
|
|
4288
|
+
console.log(source_default.gray(` Timeframe: ${b.timeframe}`));
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
if (!found)
|
|
4296
|
+
console.log(source_default.yellow("No predictions found."));
|
|
4297
|
+
});
|
|
4298
|
+
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) => {
|
|
4299
|
+
const dir = resolvePath2(".");
|
|
4300
|
+
const dirs = [join3(dir, "projects"), join3(dir, "specs"), dir];
|
|
4301
|
+
const status = opts.correct ? "correct" : opts.wrong ? "wrong" : opts.partial ? "partial" : null;
|
|
4302
|
+
if (!status) {
|
|
4303
|
+
console.log(source_default.red("Specify --correct, --wrong, or --partial"));
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
for (const dd of dirs) {
|
|
4307
|
+
if (!existsSync3(dd))
|
|
4308
|
+
continue;
|
|
4309
|
+
const projects = readdirSync3(dd, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4310
|
+
for (const p of projects) {
|
|
4311
|
+
if (opts.project && p !== opts.project)
|
|
4312
|
+
continue;
|
|
4313
|
+
const pd = join3(dd, p);
|
|
4314
|
+
if (!existsSync3(join3(pd, "SPEC.dog")))
|
|
4315
|
+
continue;
|
|
4316
|
+
const files = readdirSync3(pd).filter((f) => f.endsWith(".dog"));
|
|
4317
|
+
for (const f of files) {
|
|
4318
|
+
const fp = join3(pd, f);
|
|
4319
|
+
let content = readFileSync3(fp, "utf-8");
|
|
4320
|
+
const originalContent = readFileSync3(fp, "utf-8");
|
|
4321
|
+
const ast = parse(content);
|
|
4322
|
+
for (const section of ast.sections) {
|
|
4323
|
+
for (const block of section.blocks) {
|
|
4324
|
+
if (block.kind === "prediction") {
|
|
4325
|
+
const b = block;
|
|
4326
|
+
if ((b.statement || b.name || "").toLowerCase().includes(name.toLowerCase())) {
|
|
4327
|
+
const searchFor = `### Prediction: ${b.statement || b.name}`;
|
|
4328
|
+
const headingIdx = content.indexOf(searchFor);
|
|
4329
|
+
if (headingIdx >= 0) {
|
|
4330
|
+
const blockStart = content.indexOf("```yaml", headingIdx);
|
|
4331
|
+
const blockEnd = content.indexOf("```", blockStart + 7);
|
|
4332
|
+
if (blockStart >= 0 && blockEnd >= 0) {
|
|
4333
|
+
const yamlBlock = content.slice(blockStart, blockEnd + 3);
|
|
4334
|
+
let newYaml = yamlBlock;
|
|
4335
|
+
if (yamlBlock.includes("status:")) {
|
|
4336
|
+
newYaml = yamlBlock.replace(/status:\s*\w+/, `status: ${status}`);
|
|
4337
|
+
} else {
|
|
4338
|
+
newYaml = yamlBlock.replace(/\n\s*(trigger|timeframe|confidence|measurement):/, `
|
|
4339
|
+
status: ${status}
|
|
4340
|
+
$&`);
|
|
4341
|
+
}
|
|
4342
|
+
content = content.replace(yamlBlock, newYaml);
|
|
4343
|
+
try {
|
|
4344
|
+
parse(content);
|
|
4345
|
+
writeFileSync2(fp, content, "utf-8");
|
|
4346
|
+
console.log(source_default.green(` ✓ ${b.statement || b.name}: ${status}`));
|
|
4347
|
+
} catch (_) {
|
|
4348
|
+
writeFileSync2(fp, originalContent, "utf-8");
|
|
4349
|
+
console.log(source_default.red(` ✗ resolve produced invalid output for "${name}" — reverted`));
|
|
4350
|
+
process.exit(1);
|
|
4351
|
+
}
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
console.log(source_default.yellow(`Prediction "${name}" not found.`));
|
|
4363
|
+
});
|
|
4364
|
+
var kitDir = join3(resolve2(import.meta.dirname || "."), "..", "kits");
|
|
4365
|
+
var builtInKits = existsSync3(kitDir) ? readdirSync3(kitDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : [];
|
|
4366
|
+
var kitCmd = program2.command("kit").description("Manage spec kits");
|
|
4367
|
+
kitCmd.command("list").description("List available kits").action(() => {
|
|
4368
|
+
console.log(source_default.bold(`
|
|
4369
|
+
Available kits
|
|
4370
|
+
`));
|
|
4371
|
+
if (builtInKits.length === 0) {
|
|
4372
|
+
console.log(source_default.gray(" No built-in kits found."));
|
|
4373
|
+
} else {
|
|
4374
|
+
for (const k of builtInKits) {
|
|
4375
|
+
const specFile = join3(kitDir, k, "SPEC.dog");
|
|
4376
|
+
const desc = existsSync3(specFile) ? readFileSync3(specFile, "utf-8").split(`
|
|
4377
|
+
`)[1]?.replace(/^>\s*/, "") || "" : "";
|
|
4378
|
+
console.log(` ${source_default.green(k)} ${source_default.gray(desc)}`);
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
console.log(source_default.gray(`
|
|
4382
|
+
Community kits: npm install @scope/kit-<name> then dotdog kit install <name>`));
|
|
4383
|
+
});
|
|
4384
|
+
kitCmd.command("init <kit>").description("Init a project from a kit").option("-p, --project <name>").action((kit, opts) => {
|
|
4385
|
+
const src = join3(kitDir, kit);
|
|
4386
|
+
if (!existsSync3(src)) {
|
|
4387
|
+
console.log(source_default.red(`Kit "${kit}" not found. Available: ${builtInKits.join(", ")}`));
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
const projectName = opts.project || kit;
|
|
4391
|
+
const dir = resolvePath2(".");
|
|
4392
|
+
const dest = join3(dir, "specs", projectName);
|
|
4393
|
+
if (existsSync3(dest)) {
|
|
4394
|
+
console.log(source_default.yellow(`Project "${projectName}" already exists.`));
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
mkdirSync(dest, { recursive: true });
|
|
4398
|
+
const files = readdirSync3(src).filter((f) => f.endsWith(".dog"));
|
|
4399
|
+
for (const f of files) {
|
|
4400
|
+
writeFileSync2(join3(dest, f), readFileSync3(join3(src, f), "utf-8"));
|
|
4401
|
+
console.log(source_default.green(` ✓ ${f}`));
|
|
4402
|
+
}
|
|
4403
|
+
console.log(source_default.gray(`
|
|
4404
|
+
Kit "${kit}" initialized in specs/${projectName}/`));
|
|
4405
|
+
console.log(source_default.gray(` Run: dotdog validate`));
|
|
4406
|
+
});
|
|
3762
4407
|
program2.parse();
|
|
3763
4408
|
export {
|
|
3764
4409
|
parseToJSON,
|