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 CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import chalk4 from 'chalk';
3
+ import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
4
4
  import { dirname, join, resolve } from 'path';
5
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
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 registerModelsFromConfig(models) {
138
- for (const model of models) {
139
- if (model && typeof model === "object" && "id" in model && "schema" in model) {
140
- registerModel(model);
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 getSpecs(modelId) {
145
- const map = specStore.get(modelId);
146
- if (!map) return [];
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 registerSpecsFromConfig(specs) {
156
- if (!specs) return;
148
+ function findModelTypeFromConfig(specs, specId) {
149
+ if (!specs) return null;
157
150
  for (const entry of specs) {
158
- entry.model.register(entry.data);
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
- console.log(chalk4.blue(" Loading models..."));
174
- registerModelsFromConfig(config.models || []);
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 specs = getSpecs(model.id);
195
- if (specs.length === 0) {
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 specs) {
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(specs);
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
- console.log(chalk4.blue(" Loading models..."));
252
- registerModelsFromConfig(config.models || []);
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 specs = getSpecs(model.id);
274
- if (specs.length === 0) continue;
275
- const lintResults = model.lintAll(specs);
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
- console.log(chalk4.blue(" Loading models..."));
335
- registerModelsFromConfig(config.models || []);
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 specs = getSpecs(model.id);
341
- if (specs.length === 0) continue;
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 specs) {
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(specs);
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
- console.log(chalk4.blue(" Loading models..."));
427
- registerModelsFromConfig(config.models || []);
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 specs = getSpecs(model.id);
436
- if (specs.length === 0) continue;
437
- for (const spec of specs) {
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 = Object.fromEntries(getSpecStore());
530
+ const registry = buildRegistryFromConfig(specs);
545
531
  for (const model of models) {
546
532
  const checker = model.getCoverageChecker();
547
533
  if (checker) {
548
- const specs = getSpecs(model.id);
549
- const result = model.checkCoverage(specs, registry);
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
- console.log(chalk4.blue(" Loading models..."));
666
- registerModelsFromConfig(config.models || []);
667
- registerSpecsFromConfig(config.specs);
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(store, targetId, targetType, maxDepth);
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(store, targetId, targetType, maxDepth) {
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 store) {
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("0.1.0");
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);