speckeeper 0.3.0 → 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/dist/cli.js +71 -78
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +14 -52
- package/dist/index.js +27 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import
|
|
3
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
4
4
|
import { dirname, join, resolve } from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import chalk4 from 'chalk';
|
|
6
7
|
import { parse } from 'yaml';
|
|
7
8
|
import 'crypto';
|
|
8
9
|
import 'glob';
|
|
9
10
|
import { z } from 'zod';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
11
|
|
|
12
12
|
var defaultConfig = {
|
|
13
13
|
srcDir: "src",
|
|
@@ -123,40 +123,38 @@ var RelationSchema = z.object({
|
|
|
123
123
|
z.array(RelationSchema).optional();
|
|
124
124
|
|
|
125
125
|
// src/core/model.ts
|
|
126
|
-
var specStore = /* @__PURE__ */ new Map();
|
|
127
|
-
function getSpecStore() {
|
|
128
|
-
return specStore;
|
|
129
|
-
}
|
|
130
126
|
var modelRegistry = /* @__PURE__ */ new Map();
|
|
131
|
-
function registerModel(model) {
|
|
132
|
-
modelRegistry.set(model.id, model);
|
|
133
|
-
}
|
|
134
127
|
function getAllModels() {
|
|
135
128
|
return Array.from(modelRegistry.values());
|
|
136
129
|
}
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
130
|
+
function buildRegistryFromConfig(specs) {
|
|
131
|
+
const registry = {};
|
|
132
|
+
if (!specs) return registry;
|
|
133
|
+
for (const entry of specs) {
|
|
134
|
+
if (!registry[entry.model.id]) {
|
|
135
|
+
registry[entry.model.id] = /* @__PURE__ */ new Map();
|
|
136
|
+
}
|
|
137
|
+
const map = registry[entry.model.id];
|
|
138
|
+
for (const spec of entry.data) {
|
|
139
|
+
map.set(spec.id, spec);
|
|
141
140
|
}
|
|
142
141
|
}
|
|
142
|
+
return registry;
|
|
143
143
|
}
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return Array.from(map.values());
|
|
148
|
-
}
|
|
149
|
-
function findModelTypeBySpecId(specId) {
|
|
150
|
-
for (const [modelId, map] of specStore) {
|
|
151
|
-
if (map.has(specId)) return modelId;
|
|
152
|
-
}
|
|
153
|
-
return null;
|
|
144
|
+
function getSpecsFromConfig(specs, modelId) {
|
|
145
|
+
if (!specs) return [];
|
|
146
|
+
return specs.filter((e) => e.model.id === modelId).flatMap((e) => e.data);
|
|
154
147
|
}
|
|
155
|
-
function
|
|
156
|
-
if (!specs) return;
|
|
148
|
+
function findModelTypeFromConfig(specs, specId) {
|
|
149
|
+
if (!specs) return null;
|
|
157
150
|
for (const entry of specs) {
|
|
158
|
-
|
|
151
|
+
for (const spec of entry.data) {
|
|
152
|
+
if (spec.id === specId) {
|
|
153
|
+
return entry.model.id;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
159
156
|
}
|
|
157
|
+
return null;
|
|
160
158
|
}
|
|
161
159
|
|
|
162
160
|
// src/cli/build.ts
|
|
@@ -170,14 +168,11 @@ async function buildCommand(options) {
|
|
|
170
168
|
console.log(chalk4.gray(` Specs: ${config.specsDir}/`));
|
|
171
169
|
console.log("");
|
|
172
170
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
registerSpecsFromConfig(config.specs);
|
|
176
|
-
console.log("");
|
|
171
|
+
const models = config.models || [];
|
|
172
|
+
const specs = config.specs;
|
|
177
173
|
ensureDir(join(cwd, config.docsDir));
|
|
178
174
|
ensureDir(join(cwd, config.specsDir));
|
|
179
175
|
const files = [];
|
|
180
|
-
const models = getAllModels();
|
|
181
176
|
if (models.length === 0) {
|
|
182
177
|
console.log(chalk4.yellow(" No models registered. Add models to speckeeper.config.ts."));
|
|
183
178
|
return;
|
|
@@ -191,8 +186,8 @@ async function buildCommand(options) {
|
|
|
191
186
|
}
|
|
192
187
|
continue;
|
|
193
188
|
}
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
189
|
+
const modelSpecs = getSpecsFromConfig(specs, model.id);
|
|
190
|
+
if (modelSpecs.length === 0) {
|
|
196
191
|
if (options.verbose) {
|
|
197
192
|
console.log(chalk4.gray(` ${model.name}: no specs found`));
|
|
198
193
|
}
|
|
@@ -202,7 +197,7 @@ async function buildCommand(options) {
|
|
|
202
197
|
const outputDir = exporter.outputDir ? join(cwd, config.docsDir, exporter.outputDir) : join(cwd, config.docsDir);
|
|
203
198
|
ensureDir(outputDir);
|
|
204
199
|
if (exporter.single) {
|
|
205
|
-
for (const spec of
|
|
200
|
+
for (const spec of modelSpecs) {
|
|
206
201
|
const content = exporter.single(spec);
|
|
207
202
|
const filename = model.getFilename(spec, exporter.format) || spec.id;
|
|
208
203
|
files.push({
|
|
@@ -212,7 +207,7 @@ async function buildCommand(options) {
|
|
|
212
207
|
}
|
|
213
208
|
}
|
|
214
209
|
if (exporter.index) {
|
|
215
|
-
const indexContent = exporter.index(
|
|
210
|
+
const indexContent = exporter.index(modelSpecs);
|
|
216
211
|
files.push({
|
|
217
212
|
path: join(outputDir, "index.md"),
|
|
218
213
|
content: indexContent
|
|
@@ -248,14 +243,12 @@ async function lintCommand(options) {
|
|
|
248
243
|
}
|
|
249
244
|
console.log("");
|
|
250
245
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
registerSpecsFromConfig(config.specs);
|
|
254
|
-
const models = getAllModels();
|
|
246
|
+
const models = config.models || [];
|
|
247
|
+
const specs = config.specs;
|
|
255
248
|
console.log(chalk4.gray(` Loaded: ${models.length} models`));
|
|
256
249
|
console.log("");
|
|
257
250
|
console.log(chalk4.blue(" Running lint checks..."));
|
|
258
|
-
const result = runModelLint(options);
|
|
251
|
+
const result = runModelLint(models, specs, options);
|
|
259
252
|
console.log("");
|
|
260
253
|
outputLintResults(result, options);
|
|
261
254
|
if (result.errors > 0) {
|
|
@@ -266,13 +259,12 @@ async function lintCommand(options) {
|
|
|
266
259
|
process.exit(1);
|
|
267
260
|
}
|
|
268
261
|
}
|
|
269
|
-
function runModelLint(options) {
|
|
262
|
+
function runModelLint(models, specs, options) {
|
|
270
263
|
const issues = [];
|
|
271
|
-
const models = getAllModels();
|
|
272
264
|
for (const model of models) {
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
const lintResults = model.lintAll(
|
|
265
|
+
const modelSpecs = getSpecsFromConfig(specs, model.id);
|
|
266
|
+
if (modelSpecs.length === 0) continue;
|
|
267
|
+
const lintResults = model.lintAll(modelSpecs);
|
|
276
268
|
for (const result of lintResults) {
|
|
277
269
|
issues.push({
|
|
278
270
|
rule: result.ruleId,
|
|
@@ -331,19 +323,17 @@ async function driftCommand(options) {
|
|
|
331
323
|
console.log(chalk4.gray(` Docs: ${config.docsDir}/`));
|
|
332
324
|
console.log("");
|
|
333
325
|
try {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
registerSpecsFromConfig(config.specs);
|
|
326
|
+
const models = config.models || [];
|
|
327
|
+
const specs = config.specs;
|
|
337
328
|
const results = [];
|
|
338
|
-
const models = getAllModels();
|
|
339
329
|
for (const model of models) {
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
330
|
+
const modelSpecs = getSpecsFromConfig(specs, model.id);
|
|
331
|
+
if (modelSpecs.length === 0) continue;
|
|
342
332
|
for (const exporter of model.getExporters()) {
|
|
343
333
|
if (exporter.format !== "markdown") continue;
|
|
344
334
|
const outputDir = exporter.outputDir ? join(cwd, config.docsDir, exporter.outputDir) : join(cwd, config.docsDir);
|
|
345
335
|
if (exporter.single) {
|
|
346
|
-
for (const spec of
|
|
336
|
+
for (const spec of modelSpecs) {
|
|
347
337
|
const filename = model.getFilename(spec, "markdown") || spec.id;
|
|
348
338
|
const filePath = join(outputDir, `${filename}.md`);
|
|
349
339
|
if (!existsSync(filePath)) {
|
|
@@ -364,7 +354,7 @@ async function driftCommand(options) {
|
|
|
364
354
|
if (!existsSync(indexPath)) {
|
|
365
355
|
results.push({ file: indexPath, status: "missing" });
|
|
366
356
|
} else {
|
|
367
|
-
const expected = exporter.index(
|
|
357
|
+
const expected = exporter.index(modelSpecs);
|
|
368
358
|
const actual = readFileSync(indexPath, "utf-8");
|
|
369
359
|
if (normalizeContent(expected) !== normalizeContent(actual)) {
|
|
370
360
|
results.push({ file: indexPath, status: "drifted" });
|
|
@@ -423,18 +413,16 @@ async function checkCommand(type, options) {
|
|
|
423
413
|
console.log(chalk4.gray(` Type: ${checkType}`));
|
|
424
414
|
console.log("");
|
|
425
415
|
try {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
registerSpecsFromConfig(config.specs);
|
|
429
|
-
const results = [];
|
|
430
|
-
const models = getAllModels();
|
|
416
|
+
const models = config.models || [];
|
|
417
|
+
const specs = config.specs;
|
|
431
418
|
if (options.verbose) {
|
|
432
419
|
console.log(chalk4.gray(` Registered models: ${models.map((m) => m.id).join(", ")}`));
|
|
433
420
|
}
|
|
421
|
+
const results = [];
|
|
434
422
|
for (const model of models) {
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
437
|
-
for (const spec of
|
|
423
|
+
const modelSpecs = getSpecsFromConfig(specs, model.id);
|
|
424
|
+
if (modelSpecs.length === 0) continue;
|
|
425
|
+
for (const spec of modelSpecs) {
|
|
438
426
|
const sourcePath = model.getExternalSourcePath(spec);
|
|
439
427
|
if (!sourcePath) continue;
|
|
440
428
|
const fullPath = join(cwd, sourcePath);
|
|
@@ -468,7 +456,7 @@ async function checkCommand(type, options) {
|
|
|
468
456
|
if (options.coverage) {
|
|
469
457
|
console.log("");
|
|
470
458
|
console.log(chalk4.blue(" Coverage checks..."));
|
|
471
|
-
const coverageResults = runAllCoverageChecks();
|
|
459
|
+
const coverageResults = runAllCoverageChecks(models, specs);
|
|
472
460
|
if (coverageResults.length > 0) {
|
|
473
461
|
outputAllCoverageResults(coverageResults);
|
|
474
462
|
for (const check of coverageResults) {
|
|
@@ -476,7 +464,6 @@ async function checkCommand(type, options) {
|
|
|
476
464
|
results.push({
|
|
477
465
|
type: `coverage-${check.modelId}`,
|
|
478
466
|
success: check.result.coveragePercent >= 80,
|
|
479
|
-
// fail if below 80%
|
|
480
467
|
issues: check.result.uncoveredItems.slice(0, 10).map((item) => ({
|
|
481
468
|
severity: "warning",
|
|
482
469
|
message: `[${check.modelName}\u2192${check.targetModel}] '${item.id}'${item.sourceId ? ` (${item.sourceId})` : ""} not covered`,
|
|
@@ -538,15 +525,14 @@ function outputCheckResults(results) {
|
|
|
538
525
|
console.log("");
|
|
539
526
|
console.log(chalk4.gray(` Summary: ${totalErrors} errors, ${totalWarnings} warnings`));
|
|
540
527
|
}
|
|
541
|
-
function runAllCoverageChecks() {
|
|
542
|
-
const models = getAllModels();
|
|
528
|
+
function runAllCoverageChecks(models, specs) {
|
|
543
529
|
const results = [];
|
|
544
|
-
const registry =
|
|
530
|
+
const registry = buildRegistryFromConfig(specs);
|
|
545
531
|
for (const model of models) {
|
|
546
532
|
const checker = model.getCoverageChecker();
|
|
547
533
|
if (checker) {
|
|
548
|
-
const
|
|
549
|
-
const result = model.checkCoverage(
|
|
534
|
+
const modelSpecs = getSpecsFromConfig(specs, model.id);
|
|
535
|
+
const result = model.checkCoverage(modelSpecs, registry);
|
|
550
536
|
if (result) {
|
|
551
537
|
results.push({
|
|
552
538
|
modelId: model.id,
|
|
@@ -662,29 +648,27 @@ async function impactCommand(targetId, options) {
|
|
|
662
648
|
console.log(chalk4.gray(` Depth: ${maxDepth}`));
|
|
663
649
|
console.log("");
|
|
664
650
|
try {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const store = getSpecStore();
|
|
669
|
-
const targetType = findModelTypeBySpecId(targetId);
|
|
651
|
+
const specs = config.specs;
|
|
652
|
+
const registry = buildRegistryFromConfig(specs);
|
|
653
|
+
const targetType = findModelTypeFromConfig(specs, targetId);
|
|
670
654
|
if (!targetType) {
|
|
671
655
|
console.error(chalk4.red(` Error: Target '${targetId}' not found`));
|
|
672
656
|
process.exit(1);
|
|
673
657
|
}
|
|
674
658
|
console.log(chalk4.blue(" Analyzing impact..."));
|
|
675
|
-
const result = analyzeImpact(
|
|
659
|
+
const result = analyzeImpact(registry, targetId, targetType, maxDepth);
|
|
676
660
|
outputImpactResults(result, options);
|
|
677
661
|
} catch (error) {
|
|
678
662
|
console.error(chalk4.red("Impact analysis failed:"), error);
|
|
679
663
|
process.exit(1);
|
|
680
664
|
}
|
|
681
665
|
}
|
|
682
|
-
function analyzeImpact(
|
|
666
|
+
function analyzeImpact(registry, targetId, targetType, maxDepth) {
|
|
683
667
|
const impactedNodes = [];
|
|
684
668
|
const visited = /* @__PURE__ */ new Set([targetId]);
|
|
685
669
|
function findReferences(id, depth) {
|
|
686
670
|
if (depth > maxDepth) return;
|
|
687
|
-
for (const [type, map] of
|
|
671
|
+
for (const [type, map] of Object.entries(registry)) {
|
|
688
672
|
for (const [itemId, item] of map) {
|
|
689
673
|
if (visited.has(itemId)) continue;
|
|
690
674
|
const itemStr = JSON.stringify(item);
|
|
@@ -2542,8 +2526,17 @@ function printDiagnostics(diagnostics) {
|
|
|
2542
2526
|
}
|
|
2543
2527
|
|
|
2544
2528
|
// src/cli/index.ts
|
|
2529
|
+
function getVersion() {
|
|
2530
|
+
try {
|
|
2531
|
+
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
2532
|
+
const pkg = JSON.parse(readFileSync(join(__dirname2, "..", "package.json"), "utf-8"));
|
|
2533
|
+
return pkg.version;
|
|
2534
|
+
} catch {
|
|
2535
|
+
return "0.0.0";
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2545
2538
|
var program = new Command();
|
|
2546
|
-
program.name("speckeeper").description("Requirements and design management framework with TypeScript DSL").version(
|
|
2539
|
+
program.name("speckeeper").description("Requirements and design management framework with TypeScript DSL").version(getVersion());
|
|
2547
2540
|
program.command("build").description("Generate docs/ and specs/ from TypeScript models").option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Base output directory path", ".").option("-f, --format <format>", "Output format: markdown, json, both", "both").option("-w, --watch", "Watch for changes and auto-regenerate").option("-v, --verbose", "Show detailed output").action(buildCommand);
|
|
2548
2541
|
program.command("lint").description("Check design integrity (ID duplicates, references, layer violations, etc.)").option("-c, --config <path>", "Path to config file").option("-p, --phase <phase>", "Phase gate to check against (REQ, HLD, LLD, OPS)").option("-s, --strict", "Treat warnings as errors").option("--fix", "Attempt to fix auto-fixable issues").option("-f, --format <format>", "Output format: text, json, github", "text").action(lintCommand);
|
|
2549
2542
|
program.command("drift").description("Check if generated files have been manually edited").option("-c, --config <path>", "Path to config file").option("-u, --update", "Auto-update if differences are found").option("-f, --format <format>", "Output format: text, json, diff", "text").action(driftCommand);
|