speckeeper 0.7.2 → 0.8.1

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
@@ -90,10 +90,9 @@ npx speckeeper scaffold --source requirements.md
90
90
  ```
91
91
 
92
92
  This generates:
93
- - `design/_models/` — Model classes with base schema (id, name, description, relations) and core factory imports. Customise after generation.
93
+ - `design/_models/` — Model classes with base schema, lint rules, and `annotationChecker` bindings derived from `implements`/`verifiedBy` edges
94
94
  - `design/*.ts` — Spec data files using `defineSpecs()`
95
95
  - `design/index.ts` — Entry point via `mergeSpecs()`
96
- - Checker bindings from `implements`/`verifiedBy` edges are added as guidance comments
97
96
 
98
97
  See [Scaffold Mermaid Specification](./docs/scaffold-mermaid-spec.md) for the full input format.
99
98
 
@@ -184,25 +183,77 @@ Checks include:
184
183
 
185
184
  ### External SSOT Validation (check)
186
185
 
187
- Validate your specifications against actual implementation artifacts. speckeeper provides built-in checker factories via `speckeeper/dsl`:
186
+ Validate your specifications against actual implementation artifacts. speckeeper scans source and test files for **annotation comments** (`@verifies`, `@implements`, `@traces`) to automatically detect which specs are covered — no manual relation wiring needed.
188
187
 
189
- | Factory | Target | Validates |
190
- |---------|--------|-----------|
191
- | `testChecker()` | Test code | Test file existence + spec ID references in describe/it/test blocks |
192
- | `externalOpenAPIChecker()` | OpenAPI spec | Spec ID existence (operationId, path, schema, x-spec-id), HTTP method, parameter/response property names and types |
193
- | `externalSqlSchemaChecker()` | SQL schema | Table existence, column existence, type containment (DDL type must be equal or wider than spec type) |
194
- | `relationCoverage()` | Cross-model | Coverage of a target model via relations |
188
+ #### Annotation-based auto-detection
195
189
 
196
- Assign a checker to a model's `externalChecker` property. Scaffold emits guidance comments showing which factory to use based on your `implements` / `verifiedBy` edges.
190
+ Add annotations to your source and test files:
191
+
192
+ ```typescript
193
+ // tests/unit/auth.test.ts
194
+ // @verifies FR-001, FR-001-01
195
+ describe('User Authentication', () => { ... });
196
+ ```
197
+
198
+ ```typescript
199
+ // src/auth/handler.ts
200
+ // @implements FR-001
201
+ export class AuthHandler { ... }
202
+ ```
203
+
204
+ speckeeper scans for these annotations and automatically links specs to their implementation and tests. Annotations work in any comment style (`//`, `#`, `--`, `/* */`, `<!-- -->`).
205
+
206
+ #### Artifact configuration
207
+
208
+ Define scan targets per artifact class in your config:
209
+
210
+ ```typescript
211
+ // speckeeper.config.ts
212
+ export default defineConfig({
213
+ // ...
214
+ artifacts: {
215
+ test: {
216
+ globs: ['test/**/*.test.ts', 'tests/**/*.test.ts'],
217
+ contentPatterns: [/@verifies\s+([\w-]+(?:[,\s]+[\w-]+)*)/],
218
+ },
219
+ typescript: {
220
+ globs: ['src/**/*.ts'],
221
+ exclude: ['src/**/*.test.ts', 'src/**/*.d.ts'],
222
+ contentPatterns: [/@implements\s+([\w-]+(?:[,\s]+[\w-]+)*)/],
223
+ },
224
+ openapi: {
225
+ globs: ['api/openapi.yaml'],
226
+ },
227
+ },
228
+ });
229
+ ```
230
+
231
+ Scaffold auto-generates this config based on your Mermaid flowchart's external nodes and their artifact classes.
232
+
233
+ #### Checker factories
234
+
235
+ | Factory | Type | Validates |
236
+ |---------|------|-----------|
237
+ | `annotationChecker()` | Generic | Scans files for `@verifies`/`@implements`/`@traces` annotations matching spec IDs |
238
+ | `annotationCoverage()` | Coverage | Computes coverage from annotation scan results (no manual relations needed) |
239
+ | `externalOpenAPIChecker()` | Specialized | OpenAPI spec: operationId, path, schema, x-spec-id, HTTP method, parameter/response types |
240
+ | `externalSqlSchemaChecker()` | Specialized | SQL schema: table existence, column existence, type containment |
241
+
242
+ `annotationChecker()` is the generic checker. Specialized checkers (`externalOpenAPIChecker`, `externalSqlSchemaChecker`) extend it with source-level parsing for deeper validation. Scaffold generates the appropriate checker binding based on your artifact classes.
197
243
 
198
244
  ```typescript
199
245
  // design/_models/requirement.ts
200
- import { testChecker } from 'speckeeper/dsl';
246
+ import { annotationChecker } from 'speckeeper/dsl';
201
247
 
202
248
  class RequirementModel extends Model<typeof RequirementSchema> {
203
249
  // ... schema, lintRules, etc.
204
250
 
205
- protected externalChecker = testChecker<Requirement>();
251
+ protected externalChecker = annotationChecker<Requirement>({
252
+ checks: [
253
+ { artifact: 'test', relationType: 'verifiedBy' },
254
+ { artifact: 'typescript', relationType: 'implements' },
255
+ ],
256
+ });
206
257
  }
207
258
  ```
208
259
 
@@ -214,10 +265,14 @@ speckeeper check
214
265
  Design: design/
215
266
  Type: test
216
267
 
268
+ Scanning artifacts...
269
+ test: 12 @verifies annotations in 8 files
270
+ typescript: 5 @implements annotations in 4 files
271
+
217
272
  ✓ All checks passed
218
273
 
219
- Coverage: Requirement → UseCase
220
- Coverage: 100% (7/7 use cases covered)
274
+ Coverage: Requirement (verifiedBy → test)
275
+ Coverage: 100% (7/7 requirements verified)
221
276
  ```
222
277
 
223
278
  You can also implement custom checkers for any external source by defining an `ExternalChecker<T>` directly. See [Model Definition Guide](./docs/model-guide.md) for details.
@@ -299,7 +354,7 @@ class RunbookModel extends Model<typeof RunbookSchema> {
299
354
  }
300
355
  ```
301
356
 
302
- Core DSL factories (`speckeeper/dsl`) include `requireField`, `arrayMinLength`, `idFormat`, `childIdFormat`, `markdownExporter`, `testChecker`, `externalOpenAPIChecker`, `externalSqlSchemaChecker`, `relationCoverage`, and `baseSpecSchema`.
357
+ Core DSL factories (`speckeeper/dsl`) include `requireField`, `arrayMinLength`, `idFormat`, `childIdFormat`, `markdownExporter`, `annotationChecker`, `annotationCoverage`, `externalOpenAPIChecker`, `externalSqlSchemaChecker`, `relationCoverage`, and `baseSpecSchema`.
303
358
 
304
359
  ## Documentation
305
360
 
package/dist/cli.js CHANGED
@@ -1270,10 +1270,56 @@ function toKebabCase(s) {
1270
1270
  return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
1271
1271
  }
1272
1272
 
1273
+ // src/scaffold/artifact-defaults.ts
1274
+ var SPECIALIZED_CHECKER_MAP = {
1275
+ openapi: "externalOpenAPIChecker",
1276
+ sqlschema: "externalSqlSchemaChecker"
1277
+ };
1278
+ function generateCheckerCode(bindings, modelName) {
1279
+ const dslImports = /* @__PURE__ */ new Set(["annotationChecker"]);
1280
+ for (const b of bindings) {
1281
+ const specialized = SPECIALIZED_CHECKER_MAP[b.targetClass];
1282
+ if (specialized) dslImports.add(specialized);
1283
+ }
1284
+ const checks = bindings.map((b) => {
1285
+ const specialized = SPECIALIZED_CHECKER_MAP[b.targetClass];
1286
+ const artifact = `'${b.targetClass}'`;
1287
+ const relationType = `'${b.edgeType}'`;
1288
+ if (specialized) {
1289
+ return `{ artifact: ${artifact}, relationType: ${relationType}, checker: ${specialized}() }`;
1290
+ }
1291
+ return `{ artifact: ${artifact}, relationType: ${relationType} }`;
1292
+ });
1293
+ let config;
1294
+ if (checks.length === 1) {
1295
+ config = checks[0];
1296
+ } else {
1297
+ config = `{
1298
+ checks: [
1299
+ ${checks.join(",\n ")}
1300
+ ],
1301
+ }`;
1302
+ }
1303
+ const code = `protected externalChecker = annotationChecker<${modelName}>(${config});`;
1304
+ return {
1305
+ code,
1306
+ dslImports: Array.from(dslImports)
1307
+ };
1308
+ }
1309
+
1273
1310
  // src/scaffold/templates/base.ts
1274
1311
  function generateBaseModel(params) {
1275
1312
  const schemaName = `${params.modelName}Schema`;
1276
1313
  const className = `${params.modelName}Model`;
1314
+ const hasCheckerBindings = params.checkerBindings && params.checkerBindings.length > 0;
1315
+ const checkerGen = hasCheckerBindings ? generateCheckerCode(params.checkerBindings, params.modelName) : null;
1316
+ const dslImportItems = ["requireField"];
1317
+ if (checkerGen) {
1318
+ dslImportItems.push(...checkerGen.dslImports);
1319
+ }
1320
+ const dslImportLine = `import { ${dslImportItems.join(", ")} } from 'speckeeper/dsl';`;
1321
+ const externalCheckerLine = checkerGen ? `
1322
+ ${checkerGen.code}` : "";
1277
1323
  return `/**
1278
1324
  * ${params.modelName} Model Definition
1279
1325
  *
@@ -1282,7 +1328,7 @@ function generateBaseModel(params) {
1282
1328
  import { z } from 'zod';
1283
1329
  import { Model, RelationSchema } from 'speckeeper';
1284
1330
  import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
1285
- import { requireField } from 'speckeeper/dsl';
1331
+ ${dslImportLine}
1286
1332
 
1287
1333
  // =============================================================================
1288
1334
  // Schema Definition
@@ -1318,7 +1364,7 @@ class ${className} extends Model<typeof ${schemaName}> {
1318
1364
  requireField<${params.modelName}>('description'),
1319
1365
  ];
1320
1366
 
1321
- protected exporters: Exporter<${params.modelName}>[] = [];
1367
+ protected exporters: Exporter<${params.modelName}>[] = [];${externalCheckerLine}
1322
1368
  }
1323
1369
 
1324
1370
  export { ${className} };
@@ -1350,51 +1396,22 @@ function resolveCheckerBindings(_nodeId, outgoingEdges, nodes, speckeeperClassNa
1350
1396
  function generateModelFile(node, _incomingEdges, outgoingEdges, allNodes) {
1351
1397
  const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
1352
1398
  const templateFn = MODEL_TEMPLATE_FUNCTIONS[templateInfo.templateName];
1399
+ const bindings = allNodes ? resolveCheckerBindings(node.id, outgoingEdges, allNodes, "speckeeper") : [];
1400
+ const checkerBindings = bindings.length > 0 ? bindings.map((b) => ({ edgeType: b.edgeType, targetClass: b.targetClass })) : void 0;
1353
1401
  const params = {
1354
1402
  modelId: templateInfo.fileName,
1355
1403
  modelName: templateInfo.modelName,
1356
1404
  idPrefix: templateInfo.defaultIdPrefix,
1357
1405
  level: templateInfo.defaultLevel,
1358
- description: node.label ?? node.id
1406
+ description: node.label ?? node.id,
1407
+ checkerBindings
1359
1408
  };
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
- }
1409
+ const content = !templateFn ? MODEL_TEMPLATE_FUNCTIONS["base"](params) : templateFn(params);
1370
1410
  return {
1371
1411
  relativePath: `_models/${templateInfo.fileName}.ts`,
1372
1412
  content
1373
1413
  };
1374
1414
  }
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
1415
  function generateSpecDataFile(node) {
1399
1416
  const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
1400
1417
  const className = `${templateInfo.modelName}Model`;