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 +70 -15
- package/dist/cli.js +53 -36
- package/dist/cli.js.map +1 -1
- package/dist/config-api-U2pt1aHJ.d.ts +970 -0
- package/dist/dsl/index.d.ts +33 -2
- package/dist/dsl/index.js +197 -3
- package/dist/dsl/index.js.map +1 -1
- package/dist/index.d.ts +3 -504
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/model-BPZMYasZ.d.ts +0 -451
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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 →
|
|
220
|
-
Coverage: 100% (7/7
|
|
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`, `
|
|
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
|
-
|
|
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
|
-
|
|
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`;
|