speckeeper 0.7.1 → 0.8.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 CHANGED
@@ -152,7 +152,7 @@ npx speckeeper impact FR-001
152
152
  | `speckeeper check` | Verify consistency with external SSOT |
153
153
  | `speckeeper check test --coverage` | Verify test coverage for requirements |
154
154
  | `speckeeper scaffold` | Generate model skeletons from a mermaid flowchart |
155
- | `speckeeper drift` | Detect manual edits to generated `docs/` files |
155
+ | `speckeeper drift` | Detect manual edits to generated `specs/` files |
156
156
  | `speckeeper impact <id>` | Analyze change impact for a specific element |
157
157
 
158
158
  **Note**: `speckeeper build` generates machine-readable `specs/` output. For human-readable docs (`docs/`), use [embedoc](https://www.npmjs.com/package/embedoc) or similar tools with the model rendering API.
package/dist/cli.js CHANGED
@@ -196,9 +196,7 @@ async function buildCommand(options) {
196
196
  }
197
197
  for (const exporter of exporters) {
198
198
  const outputDir = exporter.outputDir ? join(cwd, config.docsDir, exporter.outputDir) : join(cwd, config.docsDir);
199
- if (exporter.single) {
200
- ensureDir(outputDir);
201
- }
199
+ ensureDir(outputDir);
202
200
  if (exporter.single) {
203
201
  for (const spec of modelSpecs) {
204
202
  const content = exporter.single(spec);
@@ -211,12 +209,8 @@ async function buildCommand(options) {
211
209
  }
212
210
  if (exporter.index) {
213
211
  const indexContent = exporter.index(modelSpecs);
214
- const indexPath = exporter.outputFile ? join(cwd, config.docsDir, exporter.outputFile) : join(outputDir, "index.md");
215
- if (!exporter.outputFile) {
216
- ensureDir(outputDir);
217
- }
218
212
  files.push({
219
- path: indexPath,
213
+ path: join(outputDir, "index.md"),
220
214
  content: indexContent
221
215
  });
222
216
  }
@@ -357,7 +351,7 @@ async function driftCommand(options) {
357
351
  }
358
352
  }
359
353
  if (exporter.index) {
360
- const indexPath = exporter.outputFile ? join(cwd, config.docsDir, exporter.outputFile) : join(outputDir, "index.md");
354
+ const indexPath = join(outputDir, "index.md");
361
355
  if (!existsSync(indexPath)) {
362
356
  results.push({ file: indexPath, status: "missing" });
363
357
  } else {
@@ -1270,10 +1264,56 @@ function toKebabCase(s) {
1270
1264
  return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
1271
1265
  }
1272
1266
 
1267
+ // src/scaffold/artifact-defaults.ts
1268
+ var SPECIALIZED_CHECKER_MAP = {
1269
+ openapi: "externalOpenAPIChecker",
1270
+ sqlschema: "externalSqlSchemaChecker"
1271
+ };
1272
+ function generateCheckerCode(bindings, modelName) {
1273
+ const dslImports = /* @__PURE__ */ new Set(["annotationChecker"]);
1274
+ for (const b of bindings) {
1275
+ const specialized = SPECIALIZED_CHECKER_MAP[b.targetClass];
1276
+ if (specialized) dslImports.add(specialized);
1277
+ }
1278
+ const checks = bindings.map((b) => {
1279
+ const specialized = SPECIALIZED_CHECKER_MAP[b.targetClass];
1280
+ const artifact = `'${b.targetClass}'`;
1281
+ const relationType = `'${b.edgeType}'`;
1282
+ if (specialized) {
1283
+ return `{ artifact: ${artifact}, relationType: ${relationType}, checker: ${specialized}() }`;
1284
+ }
1285
+ return `{ artifact: ${artifact}, relationType: ${relationType} }`;
1286
+ });
1287
+ let config;
1288
+ if (checks.length === 1) {
1289
+ config = checks[0];
1290
+ } else {
1291
+ config = `{
1292
+ checks: [
1293
+ ${checks.join(",\n ")}
1294
+ ],
1295
+ }`;
1296
+ }
1297
+ const code = `protected externalChecker = annotationChecker<${modelName}>(${config});`;
1298
+ return {
1299
+ code,
1300
+ dslImports: Array.from(dslImports)
1301
+ };
1302
+ }
1303
+
1273
1304
  // src/scaffold/templates/base.ts
1274
1305
  function generateBaseModel(params) {
1275
1306
  const schemaName = `${params.modelName}Schema`;
1276
1307
  const className = `${params.modelName}Model`;
1308
+ const hasCheckerBindings = params.checkerBindings && params.checkerBindings.length > 0;
1309
+ const checkerGen = hasCheckerBindings ? generateCheckerCode(params.checkerBindings, params.modelName) : null;
1310
+ const dslImportItems = ["requireField"];
1311
+ if (checkerGen) {
1312
+ dslImportItems.push(...checkerGen.dslImports);
1313
+ }
1314
+ const dslImportLine = `import { ${dslImportItems.join(", ")} } from 'speckeeper/dsl';`;
1315
+ const externalCheckerLine = checkerGen ? `
1316
+ ${checkerGen.code}` : "";
1277
1317
  return `/**
1278
1318
  * ${params.modelName} Model Definition
1279
1319
  *
@@ -1282,7 +1322,7 @@ function generateBaseModel(params) {
1282
1322
  import { z } from 'zod';
1283
1323
  import { Model, RelationSchema } from 'speckeeper';
1284
1324
  import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
1285
- import { requireField } from 'speckeeper/dsl';
1325
+ ${dslImportLine}
1286
1326
 
1287
1327
  // =============================================================================
1288
1328
  // Schema Definition
@@ -1318,7 +1358,7 @@ class ${className} extends Model<typeof ${schemaName}> {
1318
1358
  requireField<${params.modelName}>('description'),
1319
1359
  ];
1320
1360
 
1321
- protected exporters: Exporter<${params.modelName}>[] = [];
1361
+ protected exporters: Exporter<${params.modelName}>[] = [];${externalCheckerLine}
1322
1362
  }
1323
1363
 
1324
1364
  export { ${className} };
@@ -1350,51 +1390,22 @@ function resolveCheckerBindings(_nodeId, outgoingEdges, nodes, speckeeperClassNa
1350
1390
  function generateModelFile(node, _incomingEdges, outgoingEdges, allNodes) {
1351
1391
  const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
1352
1392
  const templateFn = MODEL_TEMPLATE_FUNCTIONS[templateInfo.templateName];
1393
+ const bindings = allNodes ? resolveCheckerBindings(node.id, outgoingEdges, allNodes, "speckeeper") : [];
1394
+ const checkerBindings = bindings.length > 0 ? bindings.map((b) => ({ edgeType: b.edgeType, targetClass: b.targetClass })) : void 0;
1353
1395
  const params = {
1354
1396
  modelId: templateInfo.fileName,
1355
1397
  modelName: templateInfo.modelName,
1356
1398
  idPrefix: templateInfo.defaultIdPrefix,
1357
1399
  level: templateInfo.defaultLevel,
1358
- description: node.label ?? node.id
1400
+ description: node.label ?? node.id,
1401
+ checkerBindings
1359
1402
  };
1360
- let content;
1361
- if (!templateFn) {
1362
- content = MODEL_TEMPLATE_FUNCTIONS["base"](params);
1363
- } else {
1364
- content = templateFn(params);
1365
- }
1366
- const bindings = allNodes ? resolveCheckerBindings(node.id, outgoingEdges, allNodes, "speckeeper") : [];
1367
- if (bindings.length > 0) {
1368
- content += generateCheckerBindingComment(bindings);
1369
- }
1403
+ const content = !templateFn ? MODEL_TEMPLATE_FUNCTIONS["base"](params) : templateFn(params);
1370
1404
  return {
1371
1405
  relativePath: `_models/${templateInfo.fileName}.ts`,
1372
1406
  content
1373
1407
  };
1374
1408
  }
1375
- var CHECKER_FACTORY_MAP = {
1376
- openapi: "externalOpenAPIChecker",
1377
- sqlschema: "externalSqlSchemaChecker",
1378
- test: "testChecker"
1379
- };
1380
- function generateCheckerBindingComment(bindings) {
1381
- const lines = [
1382
- "",
1383
- "// =============================================================================",
1384
- "// Checker Bindings (auto-detected from flowchart edges)",
1385
- "// =============================================================================",
1386
- "//",
1387
- "// Import and assign to externalChecker in the Model class:",
1388
- "// import { " + bindings.map((b) => CHECKER_FACTORY_MAP[b.targetClass] ?? "externalSsotChecker").join(", ") + " } from 'speckeeper/dsl';",
1389
- "//"
1390
- ];
1391
- for (const b of bindings) {
1392
- const factory = CHECKER_FACTORY_MAP[b.targetClass] ?? `/* custom checker for '${b.targetClass}' */`;
1393
- lines.push(`// ${b.edgeType} \u2192 ${b.targetNodeId} (class: ${b.targetClass}): ${factory}`);
1394
- }
1395
- lines.push("");
1396
- return lines.join("\n");
1397
- }
1398
1409
  function generateSpecDataFile(node) {
1399
1410
  const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
1400
1411
  const className = `${templateInfo.modelName}Model`;
@@ -1637,7 +1648,7 @@ var program = new Command();
1637
1648
  program.name("speckeeper").description("Requirements and design management framework with TypeScript DSL").version(getVersion());
1638
1649
  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);
1639
1650
  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);
1640
- 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").option("--fail-on-drift", "Exit with code 1 if drift is detected (for CI)").action(driftCommand);
1651
+ 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);
1641
1652
  program.command("check").description("Check external SSOT conformance (including custom models)").argument("[type]", "Type of check: external-ssot, openapi, ddl, iac, custom, all, test").option("-c, --config <path>", "Path to config file").option("--strict", "Treat warnings as errors").option("-v, --verbose", "Show detailed output").option("--coverage", "Check if all testable acceptance criteria are covered by TestRefs").action(checkCommand);
1642
1653
  program.command("new").description("Create a new element with auto-generated ID").argument("<type>", "Type: requirement, usecase, entity, component, screen, flow, error-case, term").option("-k, --kind <kind>", "Sub-kind (e.g., functional, non-functional for requirements)").option("-n, --name <name>", "Name of the element").option("-o, --output <path>", "Output directory path").option("-t, --template <path>", "Path to template file").action(newCommand);
1643
1654
  program.command("impact").description("Analyze impact of changes to an ID").argument("<id>", "ID to analyze (e.g., REQ-001, ENT-ORDER)").option("-c, --config <path>", "Path to config file").option("-d, --depth <depth>", "Analysis depth (reference tracking level)", "3").option("--direction <direction>", "Analysis direction: upstream, downstream, both", "both").option("-f, --format <format>", "Output format: text, json, mermaid", "text").action(impactCommand);