speckeeper 0.7.2 → 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 +1 -1
- package/dist/cli.js +57 -46
- package/dist/cli.js.map +1 -1
- package/dist/config-api-Bh0zX8W7.d.ts +967 -0
- package/dist/dsl/index.d.ts +33 -2
- package/dist/dsl/index.js +190 -1
- 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
|
@@ -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 `
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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").
|
|
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);
|