speckeeper 0.4.2 → 0.7.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 +99 -61
- package/dist/cli.js +200 -1108
- package/dist/cli.js.map +1 -1
- package/dist/dsl/index.d.ts +252 -1
- package/dist/dsl/index.js +630 -0
- package/dist/dsl/index.js.map +1 -1
- package/dist/index.d.ts +18 -459
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/model-2gVdKS3_.d.ts +448 -0
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -108,6 +108,7 @@ var RELATION_TYPES = [
|
|
|
108
108
|
"implements",
|
|
109
109
|
"refines",
|
|
110
110
|
"verifies",
|
|
111
|
+
"verifiedBy",
|
|
111
112
|
"satisfies",
|
|
112
113
|
"traces",
|
|
113
114
|
"relatedTo"
|
|
@@ -825,7 +826,9 @@ function parseFlowchart(mermaidSource) {
|
|
|
825
826
|
const edges = [];
|
|
826
827
|
const classDefs = [];
|
|
827
828
|
const classAssignments = [];
|
|
829
|
+
const subgraphs = [];
|
|
828
830
|
let direction = "TB";
|
|
831
|
+
const subgraphStack = [];
|
|
829
832
|
for (const rawLine of lines) {
|
|
830
833
|
const line = rawLine.trim();
|
|
831
834
|
if (!line || line.startsWith("%%")) continue;
|
|
@@ -839,6 +842,27 @@ function parseFlowchart(mermaidSource) {
|
|
|
839
842
|
if (gMatch) direction = gMatch[1];
|
|
840
843
|
continue;
|
|
841
844
|
}
|
|
845
|
+
const subgraphMatch = line.match(/^subgraph\s+(\S+)(?:\s*\[([^\]]*)\])?/);
|
|
846
|
+
if (subgraphMatch) {
|
|
847
|
+
const sg = {
|
|
848
|
+
id: subgraphMatch[1],
|
|
849
|
+
label: subgraphMatch[2] || void 0,
|
|
850
|
+
nodeIds: []
|
|
851
|
+
};
|
|
852
|
+
subgraphStack.push(sg);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (line === "end" && subgraphStack.length > 0) {
|
|
856
|
+
const completed = subgraphStack.pop();
|
|
857
|
+
subgraphs.push(completed);
|
|
858
|
+
for (const nodeId of completed.nodeIds) {
|
|
859
|
+
const node = nodes.get(nodeId);
|
|
860
|
+
if (node) {
|
|
861
|
+
node.subgraph = completed.id;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
842
866
|
const classDefMatch = line.match(/^classDef\s+(\S+)\s+(.*)/);
|
|
843
867
|
if (classDefMatch) {
|
|
844
868
|
classDefs.push({ name: classDefMatch[1], styles: classDefMatch[2] });
|
|
@@ -868,15 +892,24 @@ function parseFlowchart(mermaidSource) {
|
|
|
868
892
|
rawLabel: label,
|
|
869
893
|
direction: dir
|
|
870
894
|
});
|
|
895
|
+
if (subgraphStack.length > 0) {
|
|
896
|
+
const current = subgraphStack[subgraphStack.length - 1];
|
|
897
|
+
if (!current.nodeIds.includes(sourceId)) current.nodeIds.push(sourceId);
|
|
898
|
+
if (!current.nodeIds.includes(targetId)) current.nodeIds.push(targetId);
|
|
899
|
+
}
|
|
871
900
|
}
|
|
872
901
|
continue;
|
|
873
902
|
}
|
|
874
903
|
const standaloneNode = tryParseStandaloneNode(line);
|
|
875
904
|
if (standaloneNode) {
|
|
876
905
|
ensureNode(nodes, standaloneNode.id, standaloneNode.label);
|
|
906
|
+
if (subgraphStack.length > 0) {
|
|
907
|
+
const current = subgraphStack[subgraphStack.length - 1];
|
|
908
|
+
if (!current.nodeIds.includes(standaloneNode.id)) current.nodeIds.push(standaloneNode.id);
|
|
909
|
+
}
|
|
877
910
|
}
|
|
878
911
|
}
|
|
879
|
-
return { direction, nodes, edges, classDefs, classAssignments };
|
|
912
|
+
return { direction, nodes, edges, classDefs, classAssignments, subgraphs };
|
|
880
913
|
}
|
|
881
914
|
function parseMarkdownFlowchart(markdown) {
|
|
882
915
|
const blocks = extractMermaidBlocks(markdown);
|
|
@@ -953,7 +986,22 @@ function tryParseStandaloneNode(line) {
|
|
|
953
986
|
|
|
954
987
|
// src/scaffold/edge-vocabulary.ts
|
|
955
988
|
var EDGE_VOCABULARY = [
|
|
956
|
-
//
|
|
989
|
+
// check: speckeeper → external SSOT
|
|
990
|
+
{
|
|
991
|
+
label: "implements",
|
|
992
|
+
expectedDirection: "forward",
|
|
993
|
+
relationType: "implements",
|
|
994
|
+
category: "check",
|
|
995
|
+
description: "Spec is implemented as external artifact (OpenAPI, DDL, etc.)"
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
label: "verifiedBy",
|
|
999
|
+
expectedDirection: "forward",
|
|
1000
|
+
relationType: "verifiedBy",
|
|
1001
|
+
category: "check",
|
|
1002
|
+
description: "Spec is verified by external test code"
|
|
1003
|
+
},
|
|
1004
|
+
// lint: speckeeper ↔ speckeeper with level constraint
|
|
957
1005
|
{
|
|
958
1006
|
label: "refines",
|
|
959
1007
|
expectedDirection: "forward",
|
|
@@ -961,6 +1009,7 @@ var EDGE_VOCABULARY = [
|
|
|
961
1009
|
category: "lint",
|
|
962
1010
|
description: "Decompose higher-level item into lower-level detail"
|
|
963
1011
|
},
|
|
1012
|
+
// lint: speckeeper ↔ speckeeper generic reference integrity
|
|
964
1013
|
{
|
|
965
1014
|
label: "relatedTo",
|
|
966
1015
|
expectedDirection: "bidirectional",
|
|
@@ -975,50 +1024,41 @@ var EDGE_VOCABULARY = [
|
|
|
975
1024
|
category: "lint",
|
|
976
1025
|
description: "Reference / dependency"
|
|
977
1026
|
},
|
|
978
|
-
// B. check targets (speckeeper → external SSOT)
|
|
979
1027
|
{
|
|
980
|
-
label: "
|
|
1028
|
+
label: "dependsOn",
|
|
981
1029
|
expectedDirection: "forward",
|
|
982
|
-
relationType: "
|
|
983
|
-
category: "
|
|
984
|
-
description: "
|
|
1030
|
+
relationType: "dependsOn",
|
|
1031
|
+
category: "lint",
|
|
1032
|
+
description: "Dependency relationship"
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
label: "satisfies",
|
|
1036
|
+
expectedDirection: "forward",
|
|
1037
|
+
relationType: "satisfies",
|
|
1038
|
+
category: "lint",
|
|
1039
|
+
description: "Satisfies business/requirements"
|
|
985
1040
|
},
|
|
986
|
-
// C. coverage targets
|
|
987
1041
|
{
|
|
988
1042
|
label: "includes",
|
|
989
1043
|
expectedDirection: "forward",
|
|
990
1044
|
relationType: "includes",
|
|
991
|
-
category: "
|
|
1045
|
+
category: "lint",
|
|
992
1046
|
description: "Parent contains child items"
|
|
993
1047
|
},
|
|
994
1048
|
{
|
|
995
1049
|
label: "traces",
|
|
996
1050
|
expectedDirection: "forward",
|
|
997
1051
|
relationType: "traces",
|
|
998
|
-
category: "
|
|
1052
|
+
category: "lint",
|
|
999
1053
|
description: "Derive target from source"
|
|
1000
1054
|
},
|
|
1055
|
+
// external: test code → implementation code (no checker generated)
|
|
1001
1056
|
{
|
|
1002
1057
|
label: "verifies",
|
|
1003
1058
|
expectedDirection: "forward",
|
|
1004
1059
|
relationType: "verifies",
|
|
1005
|
-
category: "
|
|
1006
|
-
description: "Test
|
|
1007
|
-
},
|
|
1008
|
-
// Additional RELATION_TYPES (usable but no special scaffold behaviour)
|
|
1009
|
-
{
|
|
1010
|
-
label: "dependsOn",
|
|
1011
|
-
expectedDirection: "forward",
|
|
1012
|
-
relationType: "dependsOn",
|
|
1013
|
-
category: "lint",
|
|
1014
|
-
description: "Dependency relationship"
|
|
1015
|
-
},
|
|
1016
|
-
{
|
|
1017
|
-
label: "satisfies",
|
|
1018
|
-
expectedDirection: "forward",
|
|
1019
|
-
relationType: "satisfies",
|
|
1020
|
-
category: "lint",
|
|
1021
|
-
description: "Satisfies business/requirements"
|
|
1060
|
+
category: "external",
|
|
1061
|
+
description: "Test code tests implementation code (external \u2192 external, no checker)"
|
|
1022
1062
|
}
|
|
1023
1063
|
];
|
|
1024
1064
|
var EXTERNAL_LABELS = [
|
|
@@ -1078,6 +1118,11 @@ function normalizeLabel(rawLabel, vocabulary = SORTED_VOCABULARY) {
|
|
|
1078
1118
|
}
|
|
1079
1119
|
return null;
|
|
1080
1120
|
}
|
|
1121
|
+
var TEST_CLASSES = /* @__PURE__ */ new Set(["test", "e2e-test", "unit-test", "integration-test"]);
|
|
1122
|
+
function isTestLikeByClass(node) {
|
|
1123
|
+
if (!node) return false;
|
|
1124
|
+
return node.classes.some((c) => TEST_CLASSES.has(c) || c.includes("test"));
|
|
1125
|
+
}
|
|
1081
1126
|
function resolveEdges(edges, nodes, speckeeperClassName) {
|
|
1082
1127
|
const diagnostics = [];
|
|
1083
1128
|
const resolved = [];
|
|
@@ -1149,14 +1194,26 @@ function resolveEdges(edges, nodes, speckeeperClassName) {
|
|
|
1149
1194
|
});
|
|
1150
1195
|
}
|
|
1151
1196
|
}
|
|
1152
|
-
if (norm.entry.label === "
|
|
1153
|
-
if (
|
|
1197
|
+
if (norm.entry.label === "verifiedBy") {
|
|
1198
|
+
if (isSpk(edge.sourceId) && isSpk(edge.targetId)) {
|
|
1154
1199
|
diagnostics.push({
|
|
1155
1200
|
severity: "warning",
|
|
1156
|
-
message:
|
|
1201
|
+
message: '"verifiedBy" should be speckeeper \u2192 external; it represents verification by external test code',
|
|
1157
1202
|
context: `${edge.sourceId} \u2192 ${edge.targetId}`
|
|
1158
1203
|
});
|
|
1159
1204
|
}
|
|
1205
|
+
const targetNode = nodes.get(edge.targetId);
|
|
1206
|
+
if (targetNode && !isTestLikeByClass(targetNode)) {
|
|
1207
|
+
const targetId = edge.targetId.toUpperCase();
|
|
1208
|
+
const looksLikeTest = ["UT", "IT", "DUT", "E2ET"].includes(targetId) || targetId.includes("TEST") || (targetNode.label ?? "").toLowerCase().includes("test");
|
|
1209
|
+
if (!looksLikeTest) {
|
|
1210
|
+
diagnostics.push({
|
|
1211
|
+
severity: "warning",
|
|
1212
|
+
message: `"verifiedBy" target "${edge.targetId}" does not appear to be a test node; verifiedBy is intended for test targets`,
|
|
1213
|
+
context: `${edge.sourceId} \u2192 ${edge.targetId}`
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1160
1217
|
}
|
|
1161
1218
|
}
|
|
1162
1219
|
return { resolved, diagnostics };
|
|
@@ -1166,86 +1223,40 @@ function isCheckEdge(entry) {
|
|
|
1166
1223
|
}
|
|
1167
1224
|
|
|
1168
1225
|
// src/scaffold/template-registry.ts
|
|
1169
|
-
var
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
requirement: { level: "L1", fileName: "requirement", primaryTypeName: "Requirement" },
|
|
1188
|
-
usecase: { level: "L1", fileName: "usecase", primaryTypeName: "UseCase" },
|
|
1189
|
-
"logical-entity": { level: "L2", fileName: "logical-entity", primaryTypeName: "LogicalEntity" },
|
|
1190
|
-
"acceptance-test": { level: "L2", fileName: "acceptance-test", primaryTypeName: "AcceptanceTest" },
|
|
1191
|
-
"data-test": { level: "L2", fileName: "data-test", primaryTypeName: "DataTest" },
|
|
1192
|
-
"validation-constraint": { level: "L2", fileName: "validation-constraint", primaryTypeName: "ValidationConstraint" }
|
|
1193
|
-
};
|
|
1194
|
-
function resolveModelTemplate(nodeId) {
|
|
1195
|
-
const templateName = NODE_ALIAS[nodeId];
|
|
1196
|
-
if (templateName) {
|
|
1197
|
-
const meta = TEMPLATE_META[templateName];
|
|
1226
|
+
var LEVEL_PATTERNS = [
|
|
1227
|
+
{ pattern: /^L0$|business|domain/i, level: "L0" },
|
|
1228
|
+
{ pattern: /^L1$|requirement/i, level: "L1" },
|
|
1229
|
+
{ pattern: /^L2$|design|architecture/i, level: "L2" },
|
|
1230
|
+
{ pattern: /^L3$|implementation|external/i, level: "L3" }
|
|
1231
|
+
];
|
|
1232
|
+
function inferLevelFromSubgraph(subgraphId) {
|
|
1233
|
+
if (!subgraphId) return "L0";
|
|
1234
|
+
for (const { pattern, level } of LEVEL_PATTERNS) {
|
|
1235
|
+
if (pattern.test(subgraphId)) return level;
|
|
1236
|
+
}
|
|
1237
|
+
return "L0";
|
|
1238
|
+
}
|
|
1239
|
+
var SPECKEEPER_CLASS = "speckeeper";
|
|
1240
|
+
function resolveModelTemplate(nodeId, nodeClasses, subgraphId) {
|
|
1241
|
+
const level = inferLevelFromSubgraph(subgraphId);
|
|
1242
|
+
const artifactClass = nodeClasses?.find((c) => c !== SPECKEEPER_CLASS);
|
|
1243
|
+
if (!artifactClass) {
|
|
1198
1244
|
return {
|
|
1199
|
-
templateName,
|
|
1200
|
-
defaultLevel:
|
|
1245
|
+
templateName: "base",
|
|
1246
|
+
defaultLevel: level,
|
|
1201
1247
|
defaultIdPrefix: nodeId,
|
|
1202
|
-
modelName:
|
|
1203
|
-
fileName:
|
|
1248
|
+
modelName: toPascalCase(nodeId),
|
|
1249
|
+
fileName: toKebabCase(nodeId)
|
|
1204
1250
|
};
|
|
1205
1251
|
}
|
|
1206
1252
|
return {
|
|
1207
1253
|
templateName: "base",
|
|
1208
|
-
defaultLevel:
|
|
1254
|
+
defaultLevel: level,
|
|
1209
1255
|
defaultIdPrefix: nodeId,
|
|
1210
|
-
modelName: toPascalCase(
|
|
1211
|
-
fileName: toKebabCase(
|
|
1256
|
+
modelName: toPascalCase(artifactClass),
|
|
1257
|
+
fileName: toKebabCase(artifactClass)
|
|
1212
1258
|
};
|
|
1213
1259
|
}
|
|
1214
|
-
var CHECKER_ALIAS = {
|
|
1215
|
-
DDL: {
|
|
1216
|
-
templateName: "ddl-checker",
|
|
1217
|
-
targetType: "ddl",
|
|
1218
|
-
fileName: "ddl-checker"
|
|
1219
|
-
},
|
|
1220
|
-
API: {
|
|
1221
|
-
templateName: "openapi-checker",
|
|
1222
|
-
targetType: "openapi",
|
|
1223
|
-
fileName: "openapi-checker"
|
|
1224
|
-
},
|
|
1225
|
-
E2ET: {
|
|
1226
|
-
templateName: "test-checker",
|
|
1227
|
-
targetType: "test",
|
|
1228
|
-
fileName: "e2e-test-checker"
|
|
1229
|
-
},
|
|
1230
|
-
UT: {
|
|
1231
|
-
templateName: "test-checker",
|
|
1232
|
-
targetType: "test",
|
|
1233
|
-
fileName: "unit-test-checker"
|
|
1234
|
-
},
|
|
1235
|
-
DUT: {
|
|
1236
|
-
templateName: "test-checker",
|
|
1237
|
-
targetType: "test",
|
|
1238
|
-
fileName: "data-unit-test-checker"
|
|
1239
|
-
},
|
|
1240
|
-
IT: {
|
|
1241
|
-
templateName: "test-checker",
|
|
1242
|
-
targetType: "test",
|
|
1243
|
-
fileName: "integration-test-checker"
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
|
-
function resolveCheckerTemplate(targetNodeId) {
|
|
1247
|
-
return CHECKER_ALIAS[targetNodeId];
|
|
1248
|
-
}
|
|
1249
1260
|
function toPascalCase(s) {
|
|
1250
1261
|
return s.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
|
|
1251
1262
|
}
|
|
@@ -1253,682 +1264,6 @@ function toKebabCase(s) {
|
|
|
1253
1264
|
return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
|
|
1254
1265
|
}
|
|
1255
1266
|
|
|
1256
|
-
// src/scaffold/templates/requirement.ts
|
|
1257
|
-
function generateRequirementModel(_params) {
|
|
1258
|
-
return `/**
|
|
1259
|
-
* Requirement Model Definitions
|
|
1260
|
-
*
|
|
1261
|
-
* Contains SystemRequirement (SR), FunctionalRequirement (FR),
|
|
1262
|
-
* and NonFunctionalRequirement (NFR) models sharing the same schema.
|
|
1263
|
-
*/
|
|
1264
|
-
import { z } from 'zod';
|
|
1265
|
-
import { Model, RelationSchema } from 'speckeeper';
|
|
1266
|
-
import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
|
|
1267
|
-
|
|
1268
|
-
// =============================================================================
|
|
1269
|
-
// Schema Definition
|
|
1270
|
-
// =============================================================================
|
|
1271
|
-
|
|
1272
|
-
export const AcceptanceCriteriaSchema = z.object({
|
|
1273
|
-
id: z.string(),
|
|
1274
|
-
description: z.string(),
|
|
1275
|
-
verificationMethod: z.enum(['test', 'review', 'demo', 'inspection']).optional(),
|
|
1276
|
-
});
|
|
1277
|
-
|
|
1278
|
-
export const RequirementSchema = z.object({
|
|
1279
|
-
id: z.string(),
|
|
1280
|
-
name: z.string().min(1),
|
|
1281
|
-
description: z.string(),
|
|
1282
|
-
type: z.enum(['functional', 'non-functional', 'constraint']),
|
|
1283
|
-
priority: z.enum(['must', 'should', 'could']),
|
|
1284
|
-
rationale: z.string().optional(),
|
|
1285
|
-
acceptanceCriteria: z.array(AcceptanceCriteriaSchema).min(1),
|
|
1286
|
-
category: z.string().optional(),
|
|
1287
|
-
relations: z.array(RelationSchema).optional(),
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
// =============================================================================
|
|
1291
|
-
// Type Export
|
|
1292
|
-
// =============================================================================
|
|
1293
|
-
|
|
1294
|
-
export type AcceptanceCriteria = z.infer<typeof AcceptanceCriteriaSchema>;
|
|
1295
|
-
export type Requirement = z.input<typeof RequirementSchema>;
|
|
1296
|
-
|
|
1297
|
-
// =============================================================================
|
|
1298
|
-
// Shared lint rules and exporters
|
|
1299
|
-
// =============================================================================
|
|
1300
|
-
|
|
1301
|
-
const requirementLintRules: LintRule<Requirement>[] = [
|
|
1302
|
-
{
|
|
1303
|
-
id: 'req-acceptance-not-empty',
|
|
1304
|
-
severity: 'error',
|
|
1305
|
-
message: 'Requirement must have at least one acceptance criteria',
|
|
1306
|
-
check: (spec) => !spec.acceptanceCriteria || spec.acceptanceCriteria.length === 0,
|
|
1307
|
-
},
|
|
1308
|
-
{
|
|
1309
|
-
id: 'req-acceptance-id-format',
|
|
1310
|
-
severity: 'warning',
|
|
1311
|
-
message: 'Acceptance criteria ID should follow parent requirement ID (e.g., FR-001-01)',
|
|
1312
|
-
check: (spec) => {
|
|
1313
|
-
if (!spec.acceptanceCriteria) return false;
|
|
1314
|
-
return spec.acceptanceCriteria.some(ac => !ac.id.startsWith(spec.id + '-'));
|
|
1315
|
-
},
|
|
1316
|
-
},
|
|
1317
|
-
];
|
|
1318
|
-
|
|
1319
|
-
const requirementExporters: Exporter<Requirement>[] = [
|
|
1320
|
-
{
|
|
1321
|
-
format: 'markdown',
|
|
1322
|
-
single: (spec) => {
|
|
1323
|
-
const lines: string[] = [];
|
|
1324
|
-
lines.push(\`# \${spec.name}\`);
|
|
1325
|
-
lines.push('');
|
|
1326
|
-
lines.push(\`**ID**: \${spec.id}\`);
|
|
1327
|
-
lines.push(\`**Type**: \${spec.type}\`);
|
|
1328
|
-
lines.push(\`**Priority**: \${spec.priority}\`);
|
|
1329
|
-
lines.push('');
|
|
1330
|
-
lines.push('## Description');
|
|
1331
|
-
lines.push('');
|
|
1332
|
-
lines.push(spec.description);
|
|
1333
|
-
lines.push('');
|
|
1334
|
-
lines.push('## Acceptance Criteria');
|
|
1335
|
-
lines.push('');
|
|
1336
|
-
for (const ac of spec.acceptanceCriteria) {
|
|
1337
|
-
lines.push(\`- **\${ac.id}**: \${ac.description}\`);
|
|
1338
|
-
}
|
|
1339
|
-
return lines.join('\\n');
|
|
1340
|
-
},
|
|
1341
|
-
outputDir: 'requirements',
|
|
1342
|
-
filename: (spec) => spec.id,
|
|
1343
|
-
},
|
|
1344
|
-
];
|
|
1345
|
-
|
|
1346
|
-
// =============================================================================
|
|
1347
|
-
// Model Classes
|
|
1348
|
-
// =============================================================================
|
|
1349
|
-
|
|
1350
|
-
class SystemRequirementModel extends Model<typeof RequirementSchema> {
|
|
1351
|
-
readonly id = 'system-requirement';
|
|
1352
|
-
readonly name = 'SystemRequirement';
|
|
1353
|
-
readonly idPrefix = 'SR';
|
|
1354
|
-
readonly schema = RequirementSchema;
|
|
1355
|
-
readonly description = 'Defines system-level requirements';
|
|
1356
|
-
protected modelLevel: ModelLevel = 'L1';
|
|
1357
|
-
protected lintRules = requirementLintRules;
|
|
1358
|
-
protected exporters = requirementExporters;
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
class FunctionalRequirementModel extends Model<typeof RequirementSchema> {
|
|
1362
|
-
readonly id = 'functional-requirement';
|
|
1363
|
-
readonly name = 'FunctionalRequirement';
|
|
1364
|
-
readonly idPrefix = 'FR';
|
|
1365
|
-
readonly schema = RequirementSchema;
|
|
1366
|
-
readonly description = 'Defines functional requirements';
|
|
1367
|
-
protected modelLevel: ModelLevel = 'L1';
|
|
1368
|
-
protected lintRules = requirementLintRules;
|
|
1369
|
-
protected exporters = requirementExporters;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
class NonFunctionalRequirementModel extends Model<typeof RequirementSchema> {
|
|
1373
|
-
readonly id = 'non-functional-requirement';
|
|
1374
|
-
readonly name = 'NonFunctionalRequirement';
|
|
1375
|
-
readonly idPrefix = 'NFR';
|
|
1376
|
-
readonly schema = RequirementSchema;
|
|
1377
|
-
readonly description = 'Defines non-functional requirements and constraints';
|
|
1378
|
-
protected modelLevel: ModelLevel = 'L1';
|
|
1379
|
-
protected lintRules = requirementLintRules;
|
|
1380
|
-
protected exporters = requirementExporters;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
export { SystemRequirementModel, FunctionalRequirementModel, NonFunctionalRequirementModel };
|
|
1384
|
-
`;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
// src/scaffold/templates/usecase.ts
|
|
1388
|
-
function generateUseCaseModel(_params) {
|
|
1389
|
-
return `/**
|
|
1390
|
-
* UseCase Model Definition
|
|
1391
|
-
*/
|
|
1392
|
-
import { z } from 'zod';
|
|
1393
|
-
import { Model, RelationSchema } from 'speckeeper';
|
|
1394
|
-
import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
|
|
1395
|
-
|
|
1396
|
-
// =============================================================================
|
|
1397
|
-
// Schema Definition
|
|
1398
|
-
// =============================================================================
|
|
1399
|
-
|
|
1400
|
-
export const ActorSchema = z.object({
|
|
1401
|
-
id: z.string(),
|
|
1402
|
-
name: z.string(),
|
|
1403
|
-
description: z.string().optional(),
|
|
1404
|
-
type: z.enum(['human', 'system', 'external']),
|
|
1405
|
-
relations: z.array(RelationSchema).optional(),
|
|
1406
|
-
});
|
|
1407
|
-
|
|
1408
|
-
export const UseCaseStepSchema = z.object({
|
|
1409
|
-
stepNumber: z.number(),
|
|
1410
|
-
type: z.enum(['user_action', 'system_response', 'alternative']),
|
|
1411
|
-
description: z.string(),
|
|
1412
|
-
});
|
|
1413
|
-
|
|
1414
|
-
export const UseCaseSchema = z.object({
|
|
1415
|
-
id: z.string(),
|
|
1416
|
-
name: z.string(),
|
|
1417
|
-
description: z.string(),
|
|
1418
|
-
actor: z.string(),
|
|
1419
|
-
preconditions: z.array(z.string()).optional().default([]),
|
|
1420
|
-
postconditions: z.array(z.string()).optional().default([]),
|
|
1421
|
-
mainFlow: z.array(UseCaseStepSchema),
|
|
1422
|
-
relations: z.array(RelationSchema).optional(),
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
// =============================================================================
|
|
1426
|
-
// Type Export
|
|
1427
|
-
// =============================================================================
|
|
1428
|
-
|
|
1429
|
-
export type Actor = z.input<typeof ActorSchema>;
|
|
1430
|
-
export type UseCaseStep = z.infer<typeof UseCaseStepSchema>;
|
|
1431
|
-
export type UseCase = z.input<typeof UseCaseSchema>;
|
|
1432
|
-
|
|
1433
|
-
// =============================================================================
|
|
1434
|
-
// Actor Model Class
|
|
1435
|
-
// =============================================================================
|
|
1436
|
-
|
|
1437
|
-
class ActorModel extends Model<typeof ActorSchema> {
|
|
1438
|
-
readonly id = 'actor';
|
|
1439
|
-
readonly name = 'Actor';
|
|
1440
|
-
readonly idPrefix = 'ACT';
|
|
1441
|
-
readonly schema = ActorSchema;
|
|
1442
|
-
readonly description = 'Defines actors';
|
|
1443
|
-
protected modelLevel: ModelLevel = 'L0';
|
|
1444
|
-
|
|
1445
|
-
protected lintRules: LintRule<Actor>[] = [
|
|
1446
|
-
{
|
|
1447
|
-
id: 'actor-has-description',
|
|
1448
|
-
severity: 'warning',
|
|
1449
|
-
message: 'Actor should have a description',
|
|
1450
|
-
check: (spec) => !spec.description || spec.description.trim() === '',
|
|
1451
|
-
},
|
|
1452
|
-
];
|
|
1453
|
-
|
|
1454
|
-
protected exporters: Exporter<Actor>[] = [];
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
// =============================================================================
|
|
1458
|
-
// UseCase Model Class
|
|
1459
|
-
// =============================================================================
|
|
1460
|
-
|
|
1461
|
-
class UseCaseModel extends Model<typeof UseCaseSchema> {
|
|
1462
|
-
readonly id = 'usecase';
|
|
1463
|
-
readonly name = 'UseCase';
|
|
1464
|
-
readonly idPrefix = 'UC';
|
|
1465
|
-
readonly schema = UseCaseSchema;
|
|
1466
|
-
readonly description = 'Defines use cases (business flows)';
|
|
1467
|
-
protected modelLevel: ModelLevel = 'L1';
|
|
1468
|
-
|
|
1469
|
-
protected lintRules: LintRule<UseCase>[] = [
|
|
1470
|
-
{
|
|
1471
|
-
id: 'usecase-has-main-flow',
|
|
1472
|
-
severity: 'error',
|
|
1473
|
-
message: 'UseCase must have a main flow',
|
|
1474
|
-
check: (spec) => !spec.mainFlow || spec.mainFlow.length === 0,
|
|
1475
|
-
},
|
|
1476
|
-
{
|
|
1477
|
-
id: 'usecase-has-actor',
|
|
1478
|
-
severity: 'error',
|
|
1479
|
-
message: 'UseCase must have an actor',
|
|
1480
|
-
check: (spec) => !spec.actor || spec.actor.trim() === '',
|
|
1481
|
-
},
|
|
1482
|
-
];
|
|
1483
|
-
|
|
1484
|
-
protected exporters: Exporter<UseCase>[] = [];
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
export { ActorModel, UseCaseModel };
|
|
1488
|
-
`;
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
// src/scaffold/templates/term.ts
|
|
1492
|
-
function generateTermModel(_params) {
|
|
1493
|
-
return `/**
|
|
1494
|
-
* Term Model Definition
|
|
1495
|
-
*/
|
|
1496
|
-
import { z } from 'zod';
|
|
1497
|
-
import { Model, RelationSchema } from 'speckeeper';
|
|
1498
|
-
import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
|
|
1499
|
-
|
|
1500
|
-
// =============================================================================
|
|
1501
|
-
// Schema Definition
|
|
1502
|
-
// =============================================================================
|
|
1503
|
-
|
|
1504
|
-
export const TermSchema = z.object({
|
|
1505
|
-
id: z.string(),
|
|
1506
|
-
term: z.string(),
|
|
1507
|
-
definition: z.string(),
|
|
1508
|
-
category: z.enum(['business', 'technical', 'acronym', 'process', 'role']),
|
|
1509
|
-
abbreviation: z.string().optional(),
|
|
1510
|
-
expandedForm: z.string().optional(),
|
|
1511
|
-
relations: z.array(RelationSchema).optional(),
|
|
1512
|
-
});
|
|
1513
|
-
|
|
1514
|
-
// =============================================================================
|
|
1515
|
-
// Type Export
|
|
1516
|
-
// =============================================================================
|
|
1517
|
-
|
|
1518
|
-
export type Term = z.input<typeof TermSchema>;
|
|
1519
|
-
|
|
1520
|
-
// =============================================================================
|
|
1521
|
-
// Model Class
|
|
1522
|
-
// =============================================================================
|
|
1523
|
-
|
|
1524
|
-
class TermModel extends Model<typeof TermSchema> {
|
|
1525
|
-
readonly id = 'term';
|
|
1526
|
-
readonly name = 'Term';
|
|
1527
|
-
readonly idPrefix = 'TERM';
|
|
1528
|
-
readonly schema = TermSchema;
|
|
1529
|
-
readonly description = 'Defines terms (glossary)';
|
|
1530
|
-
protected modelLevel: ModelLevel = 'L0';
|
|
1531
|
-
|
|
1532
|
-
protected lintRules: LintRule<Term>[] = [
|
|
1533
|
-
{
|
|
1534
|
-
id: 'term-has-definition',
|
|
1535
|
-
severity: 'error',
|
|
1536
|
-
message: 'Term must have a definition',
|
|
1537
|
-
check: (spec) => !spec.definition || spec.definition.trim() === '',
|
|
1538
|
-
},
|
|
1539
|
-
{
|
|
1540
|
-
id: 'term-acronym-has-expanded-form',
|
|
1541
|
-
severity: 'warning',
|
|
1542
|
-
message: 'Acronym terms should have an expanded form',
|
|
1543
|
-
check: (spec) => spec.category === 'acronym' && !spec.expandedForm,
|
|
1544
|
-
},
|
|
1545
|
-
];
|
|
1546
|
-
|
|
1547
|
-
protected exporters: Exporter<Term>[] = [];
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
export { TermModel };
|
|
1551
|
-
`;
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
// src/scaffold/templates/entity.ts
|
|
1555
|
-
function generateEntityModel(_params) {
|
|
1556
|
-
return `/**
|
|
1557
|
-
* Entity Model Definition (Conceptual Data Model)
|
|
1558
|
-
*/
|
|
1559
|
-
import { z } from 'zod';
|
|
1560
|
-
import { Model, RelationSchema } from 'speckeeper';
|
|
1561
|
-
import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
|
|
1562
|
-
|
|
1563
|
-
// =============================================================================
|
|
1564
|
-
// Schema Definition
|
|
1565
|
-
// =============================================================================
|
|
1566
|
-
|
|
1567
|
-
export const AttributeSchema = z.object({
|
|
1568
|
-
name: z.string(),
|
|
1569
|
-
type: z.enum(['string', 'number', 'boolean', 'date', 'datetime', 'uuid', 'enum', 'reference']),
|
|
1570
|
-
required: z.boolean().optional().default(true),
|
|
1571
|
-
description: z.string().optional(),
|
|
1572
|
-
});
|
|
1573
|
-
|
|
1574
|
-
export const EntitySchema = z.object({
|
|
1575
|
-
id: z.string(),
|
|
1576
|
-
name: z.string(),
|
|
1577
|
-
description: z.string(),
|
|
1578
|
-
attributes: z.array(AttributeSchema),
|
|
1579
|
-
isAggregateRoot: z.boolean().optional().default(false),
|
|
1580
|
-
relations: z.array(RelationSchema).optional(),
|
|
1581
|
-
});
|
|
1582
|
-
|
|
1583
|
-
// =============================================================================
|
|
1584
|
-
// Type Export
|
|
1585
|
-
// =============================================================================
|
|
1586
|
-
|
|
1587
|
-
export type Attribute = z.infer<typeof AttributeSchema>;
|
|
1588
|
-
export type Entity = z.input<typeof EntitySchema>;
|
|
1589
|
-
|
|
1590
|
-
// =============================================================================
|
|
1591
|
-
// Model Class
|
|
1592
|
-
// =============================================================================
|
|
1593
|
-
|
|
1594
|
-
class ConceptualDataModel extends Model<typeof EntitySchema> {
|
|
1595
|
-
readonly id = 'conceptual-data-model';
|
|
1596
|
-
readonly name = 'ConceptualDataModel';
|
|
1597
|
-
readonly idPrefix = 'CDM';
|
|
1598
|
-
readonly schema = EntitySchema;
|
|
1599
|
-
readonly description = 'Defines conceptual entities (domain model)';
|
|
1600
|
-
protected modelLevel: ModelLevel = 'L0';
|
|
1601
|
-
|
|
1602
|
-
protected lintRules: LintRule<Entity>[] = [
|
|
1603
|
-
{
|
|
1604
|
-
id: 'entity-has-attributes',
|
|
1605
|
-
severity: 'warning',
|
|
1606
|
-
message: 'Entity should have at least one attribute',
|
|
1607
|
-
check: (spec) => !spec.attributes || spec.attributes.length === 0,
|
|
1608
|
-
},
|
|
1609
|
-
];
|
|
1610
|
-
|
|
1611
|
-
protected exporters: Exporter<Entity>[] = [
|
|
1612
|
-
{
|
|
1613
|
-
format: 'markdown',
|
|
1614
|
-
single: (spec) => {
|
|
1615
|
-
const lines: string[] = [];
|
|
1616
|
-
lines.push(\`# \${spec.name}\`);
|
|
1617
|
-
lines.push('');
|
|
1618
|
-
lines.push(\`**ID**: \${spec.id}\`);
|
|
1619
|
-
lines.push('');
|
|
1620
|
-
lines.push(spec.description);
|
|
1621
|
-
lines.push('');
|
|
1622
|
-
lines.push('## Attributes');
|
|
1623
|
-
lines.push('');
|
|
1624
|
-
lines.push('| Name | Type | Required | Description |');
|
|
1625
|
-
lines.push('|------|------|----------|-------------|');
|
|
1626
|
-
for (const attr of spec.attributes) {
|
|
1627
|
-
lines.push(\`| \${attr.name} | \${attr.type} | \${attr.required ? 'Yes' : 'No'} | \${attr.description || ''} |\`);
|
|
1628
|
-
}
|
|
1629
|
-
return lines.join('\\n');
|
|
1630
|
-
},
|
|
1631
|
-
outputDir: 'data-model',
|
|
1632
|
-
filename: (spec) => spec.id,
|
|
1633
|
-
},
|
|
1634
|
-
];
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
export { ConceptualDataModel };
|
|
1638
|
-
`;
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
// src/scaffold/templates/logical-entity.ts
|
|
1642
|
-
function generateLogicalEntityModel(_params) {
|
|
1643
|
-
return `/**
|
|
1644
|
-
* LogicalEntity Model Definition (Logical Data Model)
|
|
1645
|
-
*/
|
|
1646
|
-
import { z } from 'zod';
|
|
1647
|
-
import { Model, RelationSchema } from 'speckeeper';
|
|
1648
|
-
import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
|
|
1649
|
-
|
|
1650
|
-
// =============================================================================
|
|
1651
|
-
// Schema Definition
|
|
1652
|
-
// =============================================================================
|
|
1653
|
-
|
|
1654
|
-
export const ColumnSchema = z.object({
|
|
1655
|
-
name: z.string(),
|
|
1656
|
-
dataType: z.string(),
|
|
1657
|
-
nullable: z.boolean().optional().default(false),
|
|
1658
|
-
primaryKey: z.boolean().optional().default(false),
|
|
1659
|
-
foreignKey: z.object({
|
|
1660
|
-
table: z.string(),
|
|
1661
|
-
column: z.string(),
|
|
1662
|
-
}).optional(),
|
|
1663
|
-
description: z.string().optional(),
|
|
1664
|
-
});
|
|
1665
|
-
|
|
1666
|
-
export const ConstraintSchema = z.object({
|
|
1667
|
-
name: z.string(),
|
|
1668
|
-
type: z.enum(['unique', 'check', 'foreign_key', 'index']),
|
|
1669
|
-
columns: z.array(z.string()),
|
|
1670
|
-
expression: z.string().optional(),
|
|
1671
|
-
});
|
|
1672
|
-
|
|
1673
|
-
export const LogicalEntitySchema = z.object({
|
|
1674
|
-
id: z.string(),
|
|
1675
|
-
name: z.string(),
|
|
1676
|
-
description: z.string(),
|
|
1677
|
-
tableName: z.string(),
|
|
1678
|
-
columns: z.array(ColumnSchema).min(1),
|
|
1679
|
-
constraints: z.array(ConstraintSchema).optional(),
|
|
1680
|
-
relations: z.array(RelationSchema).optional(),
|
|
1681
|
-
});
|
|
1682
|
-
|
|
1683
|
-
// =============================================================================
|
|
1684
|
-
// Type Export
|
|
1685
|
-
// =============================================================================
|
|
1686
|
-
|
|
1687
|
-
export type Column = z.infer<typeof ColumnSchema>;
|
|
1688
|
-
export type Constraint = z.infer<typeof ConstraintSchema>;
|
|
1689
|
-
export type LogicalEntity = z.input<typeof LogicalEntitySchema>;
|
|
1690
|
-
|
|
1691
|
-
// =============================================================================
|
|
1692
|
-
// Model Class
|
|
1693
|
-
// =============================================================================
|
|
1694
|
-
|
|
1695
|
-
class LogicalDataModel extends Model<typeof LogicalEntitySchema> {
|
|
1696
|
-
readonly id = 'logical-data-model';
|
|
1697
|
-
readonly name = 'LogicalDataModel';
|
|
1698
|
-
readonly idPrefix = 'LDM';
|
|
1699
|
-
readonly schema = LogicalEntitySchema;
|
|
1700
|
-
readonly description = 'Defines logical data model entities (tables, columns, constraints)';
|
|
1701
|
-
protected modelLevel: ModelLevel = 'L2';
|
|
1702
|
-
|
|
1703
|
-
protected lintRules: LintRule<LogicalEntity>[] = [
|
|
1704
|
-
{
|
|
1705
|
-
id: 'lent-has-columns',
|
|
1706
|
-
severity: 'error',
|
|
1707
|
-
message: 'LogicalEntity must have at least one column',
|
|
1708
|
-
check: (spec) => !spec.columns || spec.columns.length === 0,
|
|
1709
|
-
},
|
|
1710
|
-
{
|
|
1711
|
-
id: 'lent-has-primary-key',
|
|
1712
|
-
severity: 'warning',
|
|
1713
|
-
message: 'LogicalEntity should have at least one primary key column',
|
|
1714
|
-
check: (spec) => {
|
|
1715
|
-
if (!spec.columns) return true;
|
|
1716
|
-
return !spec.columns.some(c => c.primaryKey);
|
|
1717
|
-
},
|
|
1718
|
-
},
|
|
1719
|
-
];
|
|
1720
|
-
|
|
1721
|
-
protected exporters: Exporter<LogicalEntity>[] = [
|
|
1722
|
-
{
|
|
1723
|
-
format: 'markdown',
|
|
1724
|
-
single: (spec) => {
|
|
1725
|
-
const lines: string[] = [];
|
|
1726
|
-
lines.push(\`# \${spec.name}\`);
|
|
1727
|
-
lines.push('');
|
|
1728
|
-
lines.push(\`**ID**: \${spec.id} \`);
|
|
1729
|
-
lines.push(\`**Table**: \${spec.tableName}\`);
|
|
1730
|
-
lines.push('');
|
|
1731
|
-
lines.push(spec.description);
|
|
1732
|
-
lines.push('');
|
|
1733
|
-
lines.push('## Columns');
|
|
1734
|
-
lines.push('');
|
|
1735
|
-
lines.push('| Name | Type | Nullable | PK | Description |');
|
|
1736
|
-
lines.push('|------|------|----------|----|-------------|');
|
|
1737
|
-
for (const col of spec.columns) {
|
|
1738
|
-
lines.push(\`| \${col.name} | \${col.dataType} | \${col.nullable ? 'Yes' : 'No'} | \${col.primaryKey ? 'Yes' : ''} | \${col.description || ''} |\`);
|
|
1739
|
-
}
|
|
1740
|
-
return lines.join('\\n');
|
|
1741
|
-
},
|
|
1742
|
-
outputDir: 'data-model',
|
|
1743
|
-
filename: (spec) => spec.id,
|
|
1744
|
-
},
|
|
1745
|
-
];
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
export { LogicalDataModel };
|
|
1749
|
-
`;
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
// src/scaffold/templates/acceptance-test.ts
|
|
1753
|
-
function generateAcceptanceTestModel(_params) {
|
|
1754
|
-
return `/**
|
|
1755
|
-
* AcceptanceTest Model Definition
|
|
1756
|
-
*/
|
|
1757
|
-
import { z } from 'zod';
|
|
1758
|
-
import { Model, RelationSchema } from 'speckeeper';
|
|
1759
|
-
import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
|
|
1760
|
-
|
|
1761
|
-
// =============================================================================
|
|
1762
|
-
// Schema Definition
|
|
1763
|
-
// =============================================================================
|
|
1764
|
-
|
|
1765
|
-
export const TestStepSchema = z.object({
|
|
1766
|
-
stepNumber: z.number(),
|
|
1767
|
-
action: z.string(),
|
|
1768
|
-
expectedResult: z.string(),
|
|
1769
|
-
});
|
|
1770
|
-
|
|
1771
|
-
export const AcceptanceTestSchema = z.object({
|
|
1772
|
-
id: z.string(),
|
|
1773
|
-
name: z.string(),
|
|
1774
|
-
description: z.string(),
|
|
1775
|
-
targetId: z.string(),
|
|
1776
|
-
testType: z.enum(['e2e', 'manual', 'review']),
|
|
1777
|
-
steps: z.array(TestStepSchema).optional(),
|
|
1778
|
-
expectedResult: z.string(),
|
|
1779
|
-
relations: z.array(RelationSchema).optional(),
|
|
1780
|
-
});
|
|
1781
|
-
|
|
1782
|
-
// =============================================================================
|
|
1783
|
-
// Type Export
|
|
1784
|
-
// =============================================================================
|
|
1785
|
-
|
|
1786
|
-
export type TestStep = z.infer<typeof TestStepSchema>;
|
|
1787
|
-
export type AcceptanceTest = z.input<typeof AcceptanceTestSchema>;
|
|
1788
|
-
|
|
1789
|
-
// =============================================================================
|
|
1790
|
-
// Model Class
|
|
1791
|
-
// =============================================================================
|
|
1792
|
-
|
|
1793
|
-
class AcceptanceTestModel extends Model<typeof AcceptanceTestSchema> {
|
|
1794
|
-
readonly id = 'acceptance-test';
|
|
1795
|
-
readonly name = 'AcceptanceTest';
|
|
1796
|
-
readonly idPrefix = 'AT';
|
|
1797
|
-
readonly schema = AcceptanceTestSchema;
|
|
1798
|
-
readonly description = 'Defines acceptance test specifications';
|
|
1799
|
-
protected modelLevel: ModelLevel = 'L2';
|
|
1800
|
-
|
|
1801
|
-
protected lintRules: LintRule<AcceptanceTest>[] = [
|
|
1802
|
-
{
|
|
1803
|
-
id: 'at-has-target',
|
|
1804
|
-
severity: 'error',
|
|
1805
|
-
message: 'AcceptanceTest must reference a target requirement or use case',
|
|
1806
|
-
check: (spec) => !spec.targetId || spec.targetId.trim() === '',
|
|
1807
|
-
},
|
|
1808
|
-
];
|
|
1809
|
-
|
|
1810
|
-
protected exporters: Exporter<AcceptanceTest>[] = [];
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
export { AcceptanceTestModel };
|
|
1814
|
-
`;
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
// src/scaffold/templates/data-test.ts
|
|
1818
|
-
function generateDataTestModel(_params) {
|
|
1819
|
-
return `/**
|
|
1820
|
-
* DataTest Model Definition
|
|
1821
|
-
*/
|
|
1822
|
-
import { z } from 'zod';
|
|
1823
|
-
import { Model, RelationSchema } from 'speckeeper';
|
|
1824
|
-
import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
|
|
1825
|
-
|
|
1826
|
-
// =============================================================================
|
|
1827
|
-
// Schema Definition
|
|
1828
|
-
// =============================================================================
|
|
1829
|
-
|
|
1830
|
-
export const DataTestSchema = z.object({
|
|
1831
|
-
id: z.string(),
|
|
1832
|
-
name: z.string(),
|
|
1833
|
-
description: z.string(),
|
|
1834
|
-
targetEntityId: z.string(),
|
|
1835
|
-
ruleType: z.enum(['fk', 'unique', 'type', 'not-null']),
|
|
1836
|
-
condition: z.string(),
|
|
1837
|
-
relations: z.array(RelationSchema).optional(),
|
|
1838
|
-
});
|
|
1839
|
-
|
|
1840
|
-
// =============================================================================
|
|
1841
|
-
// Type Export
|
|
1842
|
-
// =============================================================================
|
|
1843
|
-
|
|
1844
|
-
export type DataTest = z.input<typeof DataTestSchema>;
|
|
1845
|
-
|
|
1846
|
-
// =============================================================================
|
|
1847
|
-
// Model Class
|
|
1848
|
-
// =============================================================================
|
|
1849
|
-
|
|
1850
|
-
class DataTestModel extends Model<typeof DataTestSchema> {
|
|
1851
|
-
readonly id = 'data-test';
|
|
1852
|
-
readonly name = 'DataTest';
|
|
1853
|
-
readonly idPrefix = 'DT';
|
|
1854
|
-
readonly schema = DataTestSchema;
|
|
1855
|
-
readonly description = 'Defines data integrity test specifications';
|
|
1856
|
-
protected modelLevel: ModelLevel = 'L2';
|
|
1857
|
-
|
|
1858
|
-
protected lintRules: LintRule<DataTest>[] = [
|
|
1859
|
-
{
|
|
1860
|
-
id: 'dt-has-target-entity',
|
|
1861
|
-
severity: 'error',
|
|
1862
|
-
message: 'DataTest must reference a target entity',
|
|
1863
|
-
check: (spec) => !spec.targetEntityId || spec.targetEntityId.trim() === '',
|
|
1864
|
-
},
|
|
1865
|
-
];
|
|
1866
|
-
|
|
1867
|
-
protected exporters: Exporter<DataTest>[] = [];
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
export { DataTestModel };
|
|
1871
|
-
`;
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
// src/scaffold/templates/validation-constraint.ts
|
|
1875
|
-
function generateValidationConstraintModel(_params) {
|
|
1876
|
-
return `/**
|
|
1877
|
-
* ValidationConstraint Model Definition
|
|
1878
|
-
*/
|
|
1879
|
-
import { z } from 'zod';
|
|
1880
|
-
import { Model, RelationSchema } from 'speckeeper';
|
|
1881
|
-
import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
|
|
1882
|
-
|
|
1883
|
-
// =============================================================================
|
|
1884
|
-
// Schema Definition
|
|
1885
|
-
// =============================================================================
|
|
1886
|
-
|
|
1887
|
-
export const ValidationConstraintSchema = z.object({
|
|
1888
|
-
id: z.string(),
|
|
1889
|
-
name: z.string(),
|
|
1890
|
-
description: z.string(),
|
|
1891
|
-
targetField: z.string(),
|
|
1892
|
-
constraintType: z.enum(['range', 'format', 'required', 'enum', 'custom']),
|
|
1893
|
-
rule: z.string(),
|
|
1894
|
-
errorMessage: z.string().optional(),
|
|
1895
|
-
relations: z.array(RelationSchema).optional(),
|
|
1896
|
-
});
|
|
1897
|
-
|
|
1898
|
-
// =============================================================================
|
|
1899
|
-
// Type Export
|
|
1900
|
-
// =============================================================================
|
|
1901
|
-
|
|
1902
|
-
export type ValidationConstraint = z.input<typeof ValidationConstraintSchema>;
|
|
1903
|
-
|
|
1904
|
-
// =============================================================================
|
|
1905
|
-
// Model Class
|
|
1906
|
-
// =============================================================================
|
|
1907
|
-
|
|
1908
|
-
class ValidationConstraintModel extends Model<typeof ValidationConstraintSchema> {
|
|
1909
|
-
readonly id = 'validation-constraint';
|
|
1910
|
-
readonly name = 'ValidationConstraint';
|
|
1911
|
-
readonly idPrefix = 'VC';
|
|
1912
|
-
readonly schema = ValidationConstraintSchema;
|
|
1913
|
-
readonly description = 'Defines validation constraints (input rules, boundary values, formats)';
|
|
1914
|
-
protected modelLevel: ModelLevel = 'L2';
|
|
1915
|
-
|
|
1916
|
-
protected lintRules: LintRule<ValidationConstraint>[] = [
|
|
1917
|
-
{
|
|
1918
|
-
id: 'vc-has-rule',
|
|
1919
|
-
severity: 'error',
|
|
1920
|
-
message: 'ValidationConstraint must have a rule definition',
|
|
1921
|
-
check: (spec) => !spec.rule || spec.rule.trim() === '',
|
|
1922
|
-
},
|
|
1923
|
-
];
|
|
1924
|
-
|
|
1925
|
-
protected exporters: Exporter<ValidationConstraint>[] = [];
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
export { ValidationConstraintModel };
|
|
1929
|
-
`;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
1267
|
// src/scaffold/templates/base.ts
|
|
1933
1268
|
function generateBaseModel(params) {
|
|
1934
1269
|
const schemaName = `${params.modelName}Schema`;
|
|
@@ -1941,6 +1276,7 @@ function generateBaseModel(params) {
|
|
|
1941
1276
|
import { z } from 'zod';
|
|
1942
1277
|
import { Model, RelationSchema } from 'speckeeper';
|
|
1943
1278
|
import type { LintRule, Exporter, ModelLevel } from 'speckeeper';
|
|
1279
|
+
import { requireField } from 'speckeeper/dsl';
|
|
1944
1280
|
|
|
1945
1281
|
// =============================================================================
|
|
1946
1282
|
// Schema Definition
|
|
@@ -1973,12 +1309,7 @@ class ${className} extends Model<typeof ${schemaName}> {
|
|
|
1973
1309
|
protected modelLevel: ModelLevel = '${params.level}';
|
|
1974
1310
|
|
|
1975
1311
|
protected lintRules: LintRule<${params.modelName}>[] = [
|
|
1976
|
-
{
|
|
1977
|
-
id: '${params.modelId}-has-description',
|
|
1978
|
-
severity: 'warning',
|
|
1979
|
-
message: '${params.modelName} should have a description',
|
|
1980
|
-
check: (spec) => !spec.description || spec.description.trim() === '',
|
|
1981
|
-
},
|
|
1312
|
+
requireField<${params.modelName}>('description'),
|
|
1982
1313
|
];
|
|
1983
1314
|
|
|
1984
1315
|
protected exporters: Exporter<${params.modelName}>[] = [];
|
|
@@ -1988,278 +1319,81 @@ export { ${className} };
|
|
|
1988
1319
|
`;
|
|
1989
1320
|
}
|
|
1990
1321
|
|
|
1991
|
-
// src/scaffold/templates/checkers/ddl-checker.ts
|
|
1992
|
-
function generateDdlChecker(_params) {
|
|
1993
|
-
return `import { readFileSync, existsSync } from 'node:fs';
|
|
1994
|
-
import { resolve } from 'node:path';
|
|
1995
|
-
import type { ExternalChecker, CheckResult } from 'speckeeper';
|
|
1996
|
-
import type { LogicalEntity } from '../_models/logical-entity.ts';
|
|
1997
|
-
|
|
1998
|
-
/**
|
|
1999
|
-
* DDL Checker: verifies that logical entity definitions
|
|
2000
|
-
* have corresponding tables/columns in schema.sql.
|
|
2001
|
-
*/
|
|
2002
|
-
export const ddlChecker: ExternalChecker<LogicalEntity> = {
|
|
2003
|
-
targetType: 'ddl',
|
|
2004
|
-
|
|
2005
|
-
sourcePath: () => resolve('..', 'db', 'schema.sql'),
|
|
2006
|
-
|
|
2007
|
-
check: (spec, _externalData): CheckResult => {
|
|
2008
|
-
const errors: CheckResult['errors'] = [];
|
|
2009
|
-
const warnings: CheckResult['warnings'] = [];
|
|
2010
|
-
|
|
2011
|
-
const schemaPath = resolve('..', 'db', 'schema.sql');
|
|
2012
|
-
if (!existsSync(schemaPath)) {
|
|
2013
|
-
warnings.push({
|
|
2014
|
-
message: \`schema.sql not found: \${schemaPath}\`,
|
|
2015
|
-
specId: spec.id,
|
|
2016
|
-
});
|
|
2017
|
-
return { success: true, errors, warnings };
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
const _schemaSql = readFileSync(schemaPath, 'utf-8');
|
|
2021
|
-
|
|
2022
|
-
// TODO: implement table/column existence check
|
|
2023
|
-
warnings.push({
|
|
2024
|
-
message: \`DDL checker not fully implemented for \${spec.id} \u2014 add table/column verification logic\`,
|
|
2025
|
-
specId: spec.id,
|
|
2026
|
-
});
|
|
2027
|
-
|
|
2028
|
-
return {
|
|
2029
|
-
success: errors.length === 0,
|
|
2030
|
-
errors,
|
|
2031
|
-
warnings,
|
|
2032
|
-
};
|
|
2033
|
-
},
|
|
2034
|
-
};
|
|
2035
|
-
`;
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
// src/scaffold/templates/checkers/openapi-checker.ts
|
|
2039
|
-
function generateOpenapiChecker(_params) {
|
|
2040
|
-
return `import { existsSync } from 'node:fs';
|
|
2041
|
-
import { resolve } from 'node:path';
|
|
2042
|
-
import type { ExternalChecker, CheckResult } from 'speckeeper';
|
|
2043
|
-
import type { UseCase } from '../_models/usecase.ts';
|
|
2044
|
-
|
|
2045
|
-
/**
|
|
2046
|
-
* OpenAPI Checker: verifies that use cases have corresponding
|
|
2047
|
-
* endpoint definitions in the OpenAPI spec.
|
|
2048
|
-
*/
|
|
2049
|
-
export const openapiChecker: ExternalChecker<UseCase> = {
|
|
2050
|
-
targetType: 'openapi',
|
|
2051
|
-
|
|
2052
|
-
sourcePath: () => resolve('..', 'api', 'spec'),
|
|
2053
|
-
|
|
2054
|
-
check: (spec, _externalData): CheckResult => {
|
|
2055
|
-
const errors: CheckResult['errors'] = [];
|
|
2056
|
-
const warnings: CheckResult['warnings'] = [];
|
|
2057
|
-
|
|
2058
|
-
const specDir = resolve('..', 'api', 'spec');
|
|
2059
|
-
if (!existsSync(specDir)) {
|
|
2060
|
-
warnings.push({
|
|
2061
|
-
message: \`OpenAPI spec directory not found: \${specDir}\`,
|
|
2062
|
-
specId: spec.id,
|
|
2063
|
-
});
|
|
2064
|
-
return { success: true, errors, warnings };
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
// TODO: implement endpoint existence check
|
|
2068
|
-
warnings.push({
|
|
2069
|
-
message: \`OpenAPI checker not fully implemented for \${spec.id} \u2014 add endpoint verification logic\`,
|
|
2070
|
-
specId: spec.id,
|
|
2071
|
-
});
|
|
2072
|
-
|
|
2073
|
-
return {
|
|
2074
|
-
success: errors.length === 0,
|
|
2075
|
-
errors,
|
|
2076
|
-
warnings,
|
|
2077
|
-
};
|
|
2078
|
-
},
|
|
2079
|
-
};
|
|
2080
|
-
`;
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
// src/scaffold/templates/checkers/test-checker.ts
|
|
2084
|
-
function generateTestChecker(params) {
|
|
2085
|
-
return `import { existsSync, readFileSync } from 'node:fs';
|
|
2086
|
-
import { join } from 'node:path';
|
|
2087
|
-
import { glob } from 'glob';
|
|
2088
|
-
import type { ExternalChecker, CheckResult } from 'speckeeper';
|
|
2089
|
-
import type { ${params.sourceModelName} } from '../_models/${params.sourceModelFile}.ts';
|
|
2090
|
-
|
|
2091
|
-
/**
|
|
2092
|
-
* Test Checker: verifies that ${params.sourceModelName} items have
|
|
2093
|
-
* corresponding test code that exists and references the spec ID.
|
|
2094
|
-
*
|
|
2095
|
-
* Checks:
|
|
2096
|
-
* 1. Test file(s) exist at the expected path
|
|
2097
|
-
* 2. Test file content references the spec ID (in describe/it/test blocks or embedoc markers)
|
|
2098
|
-
*/
|
|
2099
|
-
export const ${toCamelCase(params.checkerName)}: ExternalChecker<${params.sourceModelName}> = {
|
|
2100
|
-
targetType: '${params.targetType}',
|
|
2101
|
-
|
|
2102
|
-
sourcePath: () => '.',
|
|
2103
|
-
|
|
2104
|
-
check: (spec, _externalData): CheckResult => {
|
|
2105
|
-
const errors: CheckResult['errors'] = [];
|
|
2106
|
-
const warnings: CheckResult['warnings'] = [];
|
|
2107
|
-
const basePath = process.cwd();
|
|
2108
|
-
|
|
2109
|
-
// TODO: configure test file path pattern per spec
|
|
2110
|
-
// For now, search common test directories
|
|
2111
|
-
const testPatterns = [
|
|
2112
|
-
'test/**/*.test.ts',
|
|
2113
|
-
'test/**/*.spec.ts',
|
|
2114
|
-
'tests/**/*.test.ts',
|
|
2115
|
-
'tests/**/*.spec.ts',
|
|
2116
|
-
];
|
|
2117
|
-
|
|
2118
|
-
let testFiles: string[] = [];
|
|
2119
|
-
for (const pattern of testPatterns) {
|
|
2120
|
-
testFiles = testFiles.concat(glob.sync(pattern, { cwd: basePath }));
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
if (testFiles.length === 0) {
|
|
2124
|
-
warnings.push({
|
|
2125
|
-
message: \`No test files found for \${spec.id}\`,
|
|
2126
|
-
specId: spec.id,
|
|
2127
|
-
});
|
|
2128
|
-
return { success: true, errors, warnings };
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
// Check if spec ID is referenced in any test file
|
|
2132
|
-
let specIdFound = false;
|
|
2133
|
-
for (const testFile of testFiles) {
|
|
2134
|
-
const fullPath = join(basePath, testFile);
|
|
2135
|
-
try {
|
|
2136
|
-
const content = readFileSync(fullPath, 'utf-8');
|
|
2137
|
-
const patterns = [
|
|
2138
|
-
new RegExp(\`describe\\\\s*\\\\(\\\\s*['\\\`"].*\${spec.id}\`, 'm'),
|
|
2139
|
-
new RegExp(\`it\\\\s*\\\\(\\\\s*['\\\`"].*\${spec.id}\`, 'm'),
|
|
2140
|
-
new RegExp(\`test\\\\s*\\\\(\\\\s*['\\\`"].*\${spec.id}\`, 'm'),
|
|
2141
|
-
new RegExp(\`@embedoc.*\${spec.id}\`, 'm'),
|
|
2142
|
-
];
|
|
2143
|
-
if (patterns.some(p => p.test(content))) {
|
|
2144
|
-
specIdFound = true;
|
|
2145
|
-
break;
|
|
2146
|
-
}
|
|
2147
|
-
} catch {
|
|
2148
|
-
// skip unreadable files
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
if (!specIdFound) {
|
|
2153
|
-
warnings.push({
|
|
2154
|
-
message: \`Spec ID "\${spec.id}" not found in any test file \u2014 test should reference the spec ID in describe/it/test or embedoc marker\`,
|
|
2155
|
-
specId: spec.id,
|
|
2156
|
-
});
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
return {
|
|
2160
|
-
success: errors.length === 0,
|
|
2161
|
-
errors,
|
|
2162
|
-
warnings,
|
|
2163
|
-
};
|
|
2164
|
-
},
|
|
2165
|
-
};
|
|
2166
|
-
`;
|
|
2167
|
-
}
|
|
2168
|
-
function toCamelCase(s) {
|
|
2169
|
-
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
// src/scaffold/templates/checkers/base-checker.ts
|
|
2173
|
-
function generateBaseChecker(params) {
|
|
2174
|
-
return `import type { ExternalChecker, CheckResult } from 'speckeeper';
|
|
2175
|
-
import type { ${params.sourceModelName} } from '../_models/${params.sourceModelFile}.ts';
|
|
2176
|
-
|
|
2177
|
-
/**
|
|
2178
|
-
* ${params.checkerName}: verifies ${params.sourceModelName} against external ${params.targetType} artifacts.
|
|
2179
|
-
*
|
|
2180
|
-
* Generated by speckeeper scaffold \u2014 implement check logic.
|
|
2181
|
-
*/
|
|
2182
|
-
export const ${toCamelCase2(params.checkerName)}: ExternalChecker<${params.sourceModelName}> = {
|
|
2183
|
-
targetType: '${params.targetType}',
|
|
2184
|
-
|
|
2185
|
-
sourcePath: () => '.',
|
|
2186
|
-
|
|
2187
|
-
check: (spec, _externalData): CheckResult => {
|
|
2188
|
-
const errors: CheckResult['errors'] = [];
|
|
2189
|
-
const warnings: CheckResult['warnings'] = [];
|
|
2190
|
-
|
|
2191
|
-
// TODO: implement check logic for ${params.targetType}
|
|
2192
|
-
warnings.push({
|
|
2193
|
-
message: \`${params.checkerName} not implemented for \${spec.id}\`,
|
|
2194
|
-
specId: spec.id,
|
|
2195
|
-
});
|
|
2196
|
-
|
|
2197
|
-
return {
|
|
2198
|
-
success: errors.length === 0,
|
|
2199
|
-
errors,
|
|
2200
|
-
warnings,
|
|
2201
|
-
};
|
|
2202
|
-
},
|
|
2203
|
-
};
|
|
2204
|
-
`;
|
|
2205
|
-
}
|
|
2206
|
-
function toCamelCase2(s) {
|
|
2207
|
-
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
1322
|
// src/scaffold/templates/index.ts
|
|
2211
1323
|
var MODEL_TEMPLATE_FUNCTIONS = {
|
|
2212
|
-
requirement: generateRequirementModel,
|
|
2213
|
-
usecase: generateUseCaseModel,
|
|
2214
|
-
term: generateTermModel,
|
|
2215
|
-
entity: generateEntityModel,
|
|
2216
|
-
"logical-entity": generateLogicalEntityModel,
|
|
2217
|
-
"acceptance-test": generateAcceptanceTestModel,
|
|
2218
|
-
"data-test": generateDataTestModel,
|
|
2219
|
-
"validation-constraint": generateValidationConstraintModel,
|
|
2220
1324
|
base: generateBaseModel
|
|
2221
1325
|
};
|
|
2222
|
-
var CHECKER_TEMPLATE_FUNCTIONS = {
|
|
2223
|
-
"ddl-checker": generateDdlChecker,
|
|
2224
|
-
"openapi-checker": generateOpenapiChecker,
|
|
2225
|
-
"test-checker": generateTestChecker,
|
|
2226
|
-
"base-checker": generateBaseChecker
|
|
2227
|
-
};
|
|
2228
1326
|
|
|
2229
1327
|
// src/scaffold/model-generator.ts
|
|
2230
|
-
function
|
|
2231
|
-
const
|
|
1328
|
+
function resolveCheckerBindings(_nodeId, outgoingEdges, nodes, speckeeperClassName) {
|
|
1329
|
+
const isSpk = (id) => nodes.get(id)?.classes.includes(speckeeperClassName) ?? false;
|
|
1330
|
+
const bindings = [];
|
|
1331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1332
|
+
for (const edge of outgoingEdges) {
|
|
1333
|
+
if (!isCheckEdge(edge.vocabulary)) continue;
|
|
1334
|
+
if (isSpk(edge.targetId)) continue;
|
|
1335
|
+
if (seen.has(edge.targetId)) continue;
|
|
1336
|
+
seen.add(edge.targetId);
|
|
1337
|
+
const targetNode = nodes.get(edge.targetId);
|
|
1338
|
+
const targetClass = targetNode?.classes.find((c) => c !== speckeeperClassName) ?? edge.targetId.toLowerCase();
|
|
1339
|
+
const edgeType = edge.normalizedLabel === "verifiedBy" ? "verifiedBy" : "implements";
|
|
1340
|
+
bindings.push({ edgeType, targetNodeId: edge.targetId, targetClass });
|
|
1341
|
+
}
|
|
1342
|
+
return bindings;
|
|
1343
|
+
}
|
|
1344
|
+
function generateModelFile(node, _incomingEdges, outgoingEdges, allNodes) {
|
|
1345
|
+
const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
|
|
2232
1346
|
const templateFn = MODEL_TEMPLATE_FUNCTIONS[templateInfo.templateName];
|
|
2233
|
-
|
|
2234
|
-
const baseFn = MODEL_TEMPLATE_FUNCTIONS["base"];
|
|
2235
|
-
return {
|
|
2236
|
-
relativePath: `_models/${templateInfo.fileName}.ts`,
|
|
2237
|
-
content: baseFn({
|
|
2238
|
-
modelId: templateInfo.fileName,
|
|
2239
|
-
modelName: templateInfo.modelName,
|
|
2240
|
-
idPrefix: templateInfo.defaultIdPrefix,
|
|
2241
|
-
level: templateInfo.defaultLevel,
|
|
2242
|
-
description: node.label ?? node.id
|
|
2243
|
-
})
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
const content = templateFn({
|
|
1347
|
+
const params = {
|
|
2247
1348
|
modelId: templateInfo.fileName,
|
|
2248
1349
|
modelName: templateInfo.modelName,
|
|
2249
1350
|
idPrefix: templateInfo.defaultIdPrefix,
|
|
2250
1351
|
level: templateInfo.defaultLevel,
|
|
2251
1352
|
description: node.label ?? node.id
|
|
2252
|
-
}
|
|
1353
|
+
};
|
|
1354
|
+
let content;
|
|
1355
|
+
if (!templateFn) {
|
|
1356
|
+
content = MODEL_TEMPLATE_FUNCTIONS["base"](params);
|
|
1357
|
+
} else {
|
|
1358
|
+
content = templateFn(params);
|
|
1359
|
+
}
|
|
1360
|
+
const bindings = allNodes ? resolveCheckerBindings(node.id, outgoingEdges, allNodes, "speckeeper") : [];
|
|
1361
|
+
if (bindings.length > 0) {
|
|
1362
|
+
content += generateCheckerBindingComment(bindings);
|
|
1363
|
+
}
|
|
2253
1364
|
return {
|
|
2254
1365
|
relativePath: `_models/${templateInfo.fileName}.ts`,
|
|
2255
1366
|
content
|
|
2256
1367
|
};
|
|
2257
1368
|
}
|
|
1369
|
+
var CHECKER_FACTORY_MAP = {
|
|
1370
|
+
openapi: "externalOpenAPIChecker",
|
|
1371
|
+
sqlschema: "externalSqlSchemaChecker",
|
|
1372
|
+
test: "testChecker"
|
|
1373
|
+
};
|
|
1374
|
+
function generateCheckerBindingComment(bindings) {
|
|
1375
|
+
const lines = [
|
|
1376
|
+
"",
|
|
1377
|
+
"// =============================================================================",
|
|
1378
|
+
"// Checker Bindings (auto-detected from flowchart edges)",
|
|
1379
|
+
"// =============================================================================",
|
|
1380
|
+
"//",
|
|
1381
|
+
"// Import and assign to externalChecker in the Model class:",
|
|
1382
|
+
"// import { " + bindings.map((b) => CHECKER_FACTORY_MAP[b.targetClass] ?? "externalSsotChecker").join(", ") + " } from 'speckeeper/dsl';",
|
|
1383
|
+
"//"
|
|
1384
|
+
];
|
|
1385
|
+
for (const b of bindings) {
|
|
1386
|
+
const factory = CHECKER_FACTORY_MAP[b.targetClass] ?? `/* custom checker for '${b.targetClass}' */`;
|
|
1387
|
+
lines.push(`// ${b.edgeType} \u2192 ${b.targetNodeId} (class: ${b.targetClass}): ${factory}`);
|
|
1388
|
+
}
|
|
1389
|
+
lines.push("");
|
|
1390
|
+
return lines.join("\n");
|
|
1391
|
+
}
|
|
2258
1392
|
function generateSpecDataFile(node) {
|
|
2259
|
-
const templateInfo = resolveModelTemplate(node.id);
|
|
1393
|
+
const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
|
|
2260
1394
|
const className = `${templateInfo.modelName}Model`;
|
|
2261
1395
|
const typeName = templateInfo.modelName;
|
|
2262
|
-
const varName =
|
|
1396
|
+
const varName = toCamelCase(templateInfo.modelName) + "s";
|
|
2263
1397
|
const fileName = templateInfo.fileName;
|
|
2264
1398
|
const content = `/**
|
|
2265
1399
|
* ${templateInfo.modelName} Specifications
|
|
@@ -2283,19 +1417,20 @@ export default defineSpecs(
|
|
|
2283
1417
|
content
|
|
2284
1418
|
};
|
|
2285
1419
|
}
|
|
2286
|
-
function
|
|
1420
|
+
function toCamelCase(pascalCase) {
|
|
2287
1421
|
return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
|
|
2288
1422
|
}
|
|
2289
1423
|
function generateDesignIndex(speckeeperNodes) {
|
|
2290
1424
|
const generated = /* @__PURE__ */ new Set();
|
|
2291
1425
|
const specFiles = [];
|
|
2292
1426
|
for (const node of speckeeperNodes) {
|
|
2293
|
-
const templateInfo = resolveModelTemplate(node.id);
|
|
2294
|
-
const
|
|
1427
|
+
const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
|
|
1428
|
+
const hasArtifactClass = node.classes.some((c) => c !== "speckeeper");
|
|
1429
|
+
const key = hasArtifactClass ? templateInfo.fileName : `base:${node.id}`;
|
|
2295
1430
|
if (generated.has(key)) continue;
|
|
2296
1431
|
generated.add(key);
|
|
2297
1432
|
specFiles.push({
|
|
2298
|
-
varName:
|
|
1433
|
+
varName: toCamelCase(templateInfo.modelName),
|
|
2299
1434
|
fileName: templateInfo.fileName
|
|
2300
1435
|
});
|
|
2301
1436
|
}
|
|
@@ -2314,77 +1449,35 @@ export default mergeSpecs(${args});
|
|
|
2314
1449
|
content
|
|
2315
1450
|
};
|
|
2316
1451
|
}
|
|
2317
|
-
function generateAllModelFiles(speckeeperNodes, resolvedEdges) {
|
|
1452
|
+
function generateAllModelFiles(speckeeperNodes, resolvedEdges, allNodes) {
|
|
2318
1453
|
const files = [];
|
|
2319
1454
|
const generated = /* @__PURE__ */ new Set();
|
|
1455
|
+
const templateNodeGroups = /* @__PURE__ */ new Map();
|
|
2320
1456
|
for (const node of speckeeperNodes) {
|
|
2321
|
-
const templateInfo = resolveModelTemplate(node.id);
|
|
2322
|
-
const
|
|
1457
|
+
const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
|
|
1458
|
+
const hasArtifactClass = node.classes.some((c) => c !== "speckeeper");
|
|
1459
|
+
const key = hasArtifactClass ? templateInfo.fileName : `base:${node.id}`;
|
|
1460
|
+
const group = templateNodeGroups.get(key) ?? [];
|
|
1461
|
+
group.push(node);
|
|
1462
|
+
templateNodeGroups.set(key, group);
|
|
1463
|
+
}
|
|
1464
|
+
for (const node of speckeeperNodes) {
|
|
1465
|
+
const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
|
|
1466
|
+
const hasArtifactClass = node.classes.some((c) => c !== "speckeeper");
|
|
1467
|
+
const key = hasArtifactClass ? templateInfo.fileName : `base:${node.id}`;
|
|
2323
1468
|
if (generated.has(key)) continue;
|
|
2324
1469
|
generated.add(key);
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
1470
|
+
const groupNodes = templateNodeGroups.get(key) ?? [node];
|
|
1471
|
+
const groupNodeIds = new Set(groupNodes.map((n) => n.id));
|
|
1472
|
+
const incoming = resolvedEdges.filter((e) => groupNodeIds.has(e.targetId));
|
|
1473
|
+
const outgoing = resolvedEdges.filter((e) => groupNodeIds.has(e.sourceId));
|
|
1474
|
+
files.push(generateModelFile(node, incoming, outgoing, allNodes));
|
|
2328
1475
|
files.push(generateSpecDataFile(node));
|
|
2329
1476
|
}
|
|
2330
1477
|
files.push(generateDesignIndex(speckeeperNodes));
|
|
2331
1478
|
return files;
|
|
2332
1479
|
}
|
|
2333
1480
|
|
|
2334
|
-
// src/scaffold/checker-generator.ts
|
|
2335
|
-
function findCheckerEdges(resolvedEdges, nodes, speckeeperClassName) {
|
|
2336
|
-
const isSpk = (id) => nodes.get(id)?.classes.includes(speckeeperClassName) ?? false;
|
|
2337
|
-
return resolvedEdges.filter((edge) => {
|
|
2338
|
-
if (!isCheckEdge(edge.vocabulary)) return false;
|
|
2339
|
-
if (!isSpk(edge.sourceId)) return false;
|
|
2340
|
-
if (isSpk(edge.targetId)) return false;
|
|
2341
|
-
return true;
|
|
2342
|
-
});
|
|
2343
|
-
}
|
|
2344
|
-
function generateCheckerFile(edge, _nodes) {
|
|
2345
|
-
const checkerInfo = resolveCheckerTemplate(edge.targetId);
|
|
2346
|
-
const sourceTemplateInfo = resolveModelTemplate(edge.sourceId);
|
|
2347
|
-
if (checkerInfo) {
|
|
2348
|
-
const templateFn = CHECKER_TEMPLATE_FUNCTIONS[checkerInfo.templateName];
|
|
2349
|
-
if (templateFn) {
|
|
2350
|
-
return {
|
|
2351
|
-
relativePath: `_checkers/${checkerInfo.fileName}.ts`,
|
|
2352
|
-
content: templateFn({
|
|
2353
|
-
checkerName: checkerInfo.fileName,
|
|
2354
|
-
targetType: checkerInfo.targetType,
|
|
2355
|
-
sourceModelName: sourceTemplateInfo.modelName,
|
|
2356
|
-
sourceModelFile: sourceTemplateInfo.fileName,
|
|
2357
|
-
description: `Checks ${sourceTemplateInfo.modelName} against ${edge.targetId}`
|
|
2358
|
-
})
|
|
2359
|
-
};
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
const baseFn = CHECKER_TEMPLATE_FUNCTIONS["base-checker"];
|
|
2363
|
-
const targetId = edge.targetId.toLowerCase();
|
|
2364
|
-
const fileName = `${targetId}-checker`;
|
|
2365
|
-
return {
|
|
2366
|
-
relativePath: `_checkers/${fileName}.ts`,
|
|
2367
|
-
content: baseFn({
|
|
2368
|
-
checkerName: fileName,
|
|
2369
|
-
targetType: targetId,
|
|
2370
|
-
sourceModelName: sourceTemplateInfo.modelName,
|
|
2371
|
-
sourceModelFile: sourceTemplateInfo.fileName,
|
|
2372
|
-
description: `Checks ${sourceTemplateInfo.modelName} against ${edge.targetId}`
|
|
2373
|
-
})
|
|
2374
|
-
};
|
|
2375
|
-
}
|
|
2376
|
-
function generateAllCheckerFiles(resolvedEdges, nodes, speckeeperClassName) {
|
|
2377
|
-
const checkerEdges = findCheckerEdges(resolvedEdges, nodes, speckeeperClassName);
|
|
2378
|
-
const files = [];
|
|
2379
|
-
const generated = /* @__PURE__ */ new Set();
|
|
2380
|
-
for (const edge of checkerEdges) {
|
|
2381
|
-
if (generated.has(edge.targetId)) continue;
|
|
2382
|
-
generated.add(edge.targetId);
|
|
2383
|
-
files.push(generateCheckerFile(edge));
|
|
2384
|
-
}
|
|
2385
|
-
return files;
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
1481
|
// src/scaffold/index-generator.ts
|
|
2389
1482
|
function extractExportedClassNames(content) {
|
|
2390
1483
|
const match = content.match(/export\s*\{([^}]+)\}\s*;?\s*$/m);
|
|
@@ -2439,7 +1532,7 @@ function generateModelsIndex(modelFiles) {
|
|
|
2439
1532
|
}
|
|
2440
1533
|
|
|
2441
1534
|
// src/cli/scaffold.ts
|
|
2442
|
-
var
|
|
1535
|
+
var SPECKEEPER_CLASS2 = "speckeeper";
|
|
2443
1536
|
async function scaffoldCommand(options) {
|
|
2444
1537
|
console.log(chalk4.blue("speckeeper scaffold"));
|
|
2445
1538
|
console.log("");
|
|
@@ -2457,12 +1550,12 @@ async function scaffoldCommand(options) {
|
|
|
2457
1550
|
console.log(chalk4.gray(` Parsed: ${flowchart.nodes.size} nodes, ${flowchart.edges.length} edges`));
|
|
2458
1551
|
const speckeeperNodes = [];
|
|
2459
1552
|
for (const node of flowchart.nodes.values()) {
|
|
2460
|
-
if (node.classes.includes(
|
|
1553
|
+
if (node.classes.includes(SPECKEEPER_CLASS2)) {
|
|
2461
1554
|
speckeeperNodes.push(node);
|
|
2462
1555
|
}
|
|
2463
1556
|
}
|
|
2464
1557
|
if (speckeeperNodes.length === 0) {
|
|
2465
|
-
console.error(chalk4.red(` Error: No nodes with class "${
|
|
1558
|
+
console.error(chalk4.red(` Error: No nodes with class "${SPECKEEPER_CLASS2}" found`));
|
|
2466
1559
|
console.log(chalk4.gray(" Add `classDef speckeeper ...` and `class NODE1,NODE2 speckeeper` to your flowchart"));
|
|
2467
1560
|
process.exit(1);
|
|
2468
1561
|
}
|
|
@@ -2470,14 +1563,13 @@ async function scaffoldCommand(options) {
|
|
|
2470
1563
|
const { resolved, diagnostics } = resolveEdges(
|
|
2471
1564
|
flowchart.edges,
|
|
2472
1565
|
flowchart.nodes,
|
|
2473
|
-
|
|
1566
|
+
SPECKEEPER_CLASS2
|
|
2474
1567
|
);
|
|
2475
1568
|
printDiagnostics(diagnostics);
|
|
2476
1569
|
const outputDir = options.output ? resolve(options.output) : join(process.cwd(), "design");
|
|
2477
|
-
const modelFiles = generateAllModelFiles(speckeeperNodes, resolved);
|
|
2478
|
-
const checkerFiles = generateAllCheckerFiles(resolved, flowchart.nodes, SPECKEEPER_CLASS);
|
|
1570
|
+
const modelFiles = generateAllModelFiles(speckeeperNodes, resolved, flowchart.nodes);
|
|
2479
1571
|
const indexFile = generateModelsIndex(modelFiles);
|
|
2480
|
-
const allFiles = [...modelFiles, indexFile
|
|
1572
|
+
const allFiles = [...modelFiles, indexFile];
|
|
2481
1573
|
console.log("");
|
|
2482
1574
|
console.log(chalk4.cyan(` Files to generate (${allFiles.length}):`));
|
|
2483
1575
|
for (const file of allFiles) {
|
|
@@ -2539,12 +1631,12 @@ var program = new Command();
|
|
|
2539
1631
|
program.name("speckeeper").description("Requirements and design management framework with TypeScript DSL").version(getVersion());
|
|
2540
1632
|
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);
|
|
2541
1633
|
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);
|
|
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);
|
|
1634
|
+
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);
|
|
2543
1635
|
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);
|
|
2544
1636
|
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);
|
|
2545
1637
|
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);
|
|
2546
1638
|
program.command("init").description("Initialize a new speckeeper project with starter templates").option("-f, --force", "Overwrite existing files").action(runInit);
|
|
2547
|
-
program.command("scaffold").description("Generate _models/
|
|
1639
|
+
program.command("scaffold").description("Generate _models/ from a mermaid flowchart definition").requiredOption("-s, --source <path>", "Path to Markdown file containing mermaid flowchart").option("-o, --output <path>", "Output directory (default: design/)").option("-f, --force", "Overwrite existing files").option("--dry-run", "Preview generated files without writing").action(scaffoldCommand);
|
|
2548
1640
|
program.parse();
|
|
2549
1641
|
//# sourceMappingURL=cli.js.map
|
|
2550
1642
|
//# sourceMappingURL=cli.js.map
|