speckeeper 0.4.2 → 0.6.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/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
- // A. lint targets (speckeeper ↔ speckeeper reference integrity)
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: "implements",
1028
+ label: "dependsOn",
981
1029
  expectedDirection: "forward",
982
- relationType: "implements",
983
- category: "check",
984
- description: "Realize speckeeper spec as external artifact, interface, or test"
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: "coverage",
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: "coverage",
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: "coverage",
1006
- description: "Test verifies target"
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 === "includes" || norm.entry.label === "traces") {
1153
- if (!isSpk(edge.sourceId) || !isSpk(edge.targetId)) {
1197
+ if (norm.entry.label === "verifiedBy") {
1198
+ if (isSpk(edge.sourceId) && isSpk(edge.targetId)) {
1154
1199
  diagnostics.push({
1155
1200
  severity: "warning",
1156
- message: `"${norm.entry.label}" should connect two speckeeper-managed nodes`,
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 NODE_ALIAS = {
1170
- // L0
1171
- TERM: "term",
1172
- CDM: "entity",
1173
- // L1
1174
- SR: "requirement",
1175
- FR: "requirement",
1176
- NFR: "requirement",
1177
- UC: "usecase",
1178
- // L2
1179
- LDM: "logical-entity",
1180
- AT: "acceptance-test",
1181
- DT: "data-test",
1182
- VC: "validation-constraint"
1183
- };
1184
- var TEMPLATE_META = {
1185
- term: { level: "L0", fileName: "term", primaryTypeName: "Term" },
1186
- entity: { level: "L0", fileName: "entity", primaryTypeName: "Entity" },
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: meta.level,
1245
+ templateName: "base",
1246
+ defaultLevel: level,
1201
1247
  defaultIdPrefix: nodeId,
1202
- modelName: meta.primaryTypeName,
1203
- fileName: meta.fileName
1248
+ modelName: toPascalCase(nodeId),
1249
+ fileName: toKebabCase(nodeId)
1204
1250
  };
1205
1251
  }
1206
1252
  return {
1207
1253
  templateName: "base",
1208
- defaultLevel: "L1",
1254
+ defaultLevel: level,
1209
1255
  defaultIdPrefix: nodeId,
1210
- modelName: toPascalCase(nodeId),
1211
- fileName: toKebabCase(nodeId)
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 generateModelFile(node, _incomingEdges, _outgoingEdges) {
2231
- const templateInfo = resolveModelTemplate(node.id);
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
- if (!templateFn) {
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 = toCamelCase3(templateInfo.modelName) + "s";
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 toCamelCase3(pascalCase) {
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 key = templateInfo.templateName === "base" ? `base:${node.id}` : templateInfo.templateName;
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: toCamelCase3(templateInfo.modelName),
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 key = templateInfo.templateName === "base" ? `base:${node.id}` : templateInfo.templateName;
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
- resolvedEdges.filter((e) => e.targetId === node.id);
2326
- resolvedEdges.filter((e) => e.sourceId === node.id);
2327
- files.push(generateModelFile(node));
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 SPECKEEPER_CLASS = "speckeeper";
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(SPECKEEPER_CLASS)) {
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 "${SPECKEEPER_CLASS}" found`));
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
- SPECKEEPER_CLASS
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, ...checkerFiles];
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) {
@@ -2544,7 +1636,7 @@ program.command("check").description("Check external SSOT conformance (including
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/ and _checkers/ 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);
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