agent-sdd 1.0.3

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.
Files changed (304) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1028 -0
  3. package/README.ru.md +1046 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.js +867 -0
  6. package/dist/features/approve/adapters/inbound/CliApproveHandler.d.ts +17 -0
  7. package/dist/features/approve/adapters/inbound/CliApproveHandler.js +108 -0
  8. package/dist/features/approve/adapters/outbound/NodeApproveFileSystem.d.ts +8 -0
  9. package/dist/features/approve/adapters/outbound/NodeApproveFileSystem.js +147 -0
  10. package/dist/features/approve/adapters/outbound/NodePlanFileWriter.d.ts +6 -0
  11. package/dist/features/approve/adapters/outbound/NodePlanFileWriter.js +92 -0
  12. package/dist/features/approve/adapters/outbound/SystemApproveClock.d.ts +4 -0
  13. package/dist/features/approve/adapters/outbound/SystemApproveClock.js +5 -0
  14. package/dist/features/approve/application/ApplyApproval.d.ts +19 -0
  15. package/dist/features/approve/application/ApplyApproval.js +30 -0
  16. package/dist/features/approve/application/WriteAttestation.d.ts +19 -0
  17. package/dist/features/approve/application/WriteAttestation.js +23 -0
  18. package/dist/features/approve/domain/ApproveRequest.d.ts +24 -0
  19. package/dist/features/approve/domain/ApproveRequest.js +24 -0
  20. package/dist/features/approve/domain/Rewrite.d.ts +1 -0
  21. package/dist/features/approve/domain/Rewrite.js +6 -0
  22. package/dist/features/approve/ports/inbound/ApproveCommand.d.ts +10 -0
  23. package/dist/features/approve/ports/inbound/ApproveCommand.js +1 -0
  24. package/dist/features/approve/ports/outbound/ApproveClock.d.ts +3 -0
  25. package/dist/features/approve/ports/outbound/ApproveClock.js +1 -0
  26. package/dist/features/approve/ports/outbound/ApproveConfigPort.d.ts +4 -0
  27. package/dist/features/approve/ports/outbound/ApproveConfigPort.js +1 -0
  28. package/dist/features/approve/ports/outbound/ApproveFileSystem.d.ts +8 -0
  29. package/dist/features/approve/ports/outbound/ApproveFileSystem.js +1 -0
  30. package/dist/features/approve/ports/outbound/PlanFileWriter.d.ts +13 -0
  31. package/dist/features/approve/ports/outbound/PlanFileWriter.js +1 -0
  32. package/dist/features/check/adapters/inbound/CliCheckHandler.d.ts +8 -0
  33. package/dist/features/check/adapters/inbound/CliCheckHandler.js +62 -0
  34. package/dist/features/check/adapters/outbound/ChildProcessCheckGit.d.ts +8 -0
  35. package/dist/features/check/adapters/outbound/ChildProcessCheckGit.js +112 -0
  36. package/dist/features/check/adapters/outbound/NodeCheckFileReader.d.ts +7 -0
  37. package/dist/features/check/adapters/outbound/NodeCheckFileReader.js +44 -0
  38. package/dist/features/check/application/CheckBaseline.d.ts +28 -0
  39. package/dist/features/check/application/CheckBaseline.js +54 -0
  40. package/dist/features/check/domain/BaselineComparison.d.ts +1 -0
  41. package/dist/features/check/domain/BaselineComparison.js +2 -0
  42. package/dist/features/check/ports/inbound/CheckCommand.d.ts +4 -0
  43. package/dist/features/check/ports/inbound/CheckCommand.js +1 -0
  44. package/dist/features/check/ports/outbound/CheckConfigPort.d.ts +4 -0
  45. package/dist/features/check/ports/outbound/CheckConfigPort.js +1 -0
  46. package/dist/features/check/ports/outbound/CheckGitPort.d.ts +7 -0
  47. package/dist/features/check/ports/outbound/CheckGitPort.js +1 -0
  48. package/dist/features/check/ports/outbound/CheckSpecPort.d.ts +9 -0
  49. package/dist/features/check/ports/outbound/CheckSpecPort.js +1 -0
  50. package/dist/features/doctor/adapters/inbound/CliDoctorHandler.d.ts +8 -0
  51. package/dist/features/doctor/adapters/inbound/CliDoctorHandler.js +77 -0
  52. package/dist/features/doctor/adapters/outbound/NodeRegistryReader.d.ts +5 -0
  53. package/dist/features/doctor/adapters/outbound/NodeRegistryReader.js +40 -0
  54. package/dist/features/doctor/application/RunDoctor.d.ts +30 -0
  55. package/dist/features/doctor/application/RunDoctor.js +78 -0
  56. package/dist/features/doctor/domain/RegistryRow.d.ts +23 -0
  57. package/dist/features/doctor/domain/RegistryRow.js +114 -0
  58. package/dist/features/doctor/domain/SemverRange.d.ts +7 -0
  59. package/dist/features/doctor/domain/SemverRange.js +82 -0
  60. package/dist/features/doctor/ports/inbound/DoctorCommand.d.ts +8 -0
  61. package/dist/features/doctor/ports/inbound/DoctorCommand.js +1 -0
  62. package/dist/features/doctor/ports/outbound/RegistryReader.d.ts +12 -0
  63. package/dist/features/doctor/ports/outbound/RegistryReader.js +1 -0
  64. package/dist/features/finalize/adapters/inbound/CliFinalizeHandler.d.ts +8 -0
  65. package/dist/features/finalize/adapters/inbound/CliFinalizeHandler.js +80 -0
  66. package/dist/features/finalize/adapters/outbound/NodeFinalizeFileSystem.d.ts +11 -0
  67. package/dist/features/finalize/adapters/outbound/NodeFinalizeFileSystem.js +167 -0
  68. package/dist/features/finalize/adapters/outbound/NodePlanRepo.d.ts +7 -0
  69. package/dist/features/finalize/adapters/outbound/NodePlanRepo.js +82 -0
  70. package/dist/features/finalize/adapters/outbound/SystemFinalizeClock.d.ts +4 -0
  71. package/dist/features/finalize/adapters/outbound/SystemFinalizeClock.js +5 -0
  72. package/dist/features/finalize/application/RunFinalize.d.ts +34 -0
  73. package/dist/features/finalize/application/RunFinalize.js +98 -0
  74. package/dist/features/finalize/domain/ValidateFinalizeGraph.d.ts +9 -0
  75. package/dist/features/finalize/domain/ValidateFinalizeGraph.js +86 -0
  76. package/dist/features/finalize/ports/inbound/FinalizeCommand.d.ts +7 -0
  77. package/dist/features/finalize/ports/inbound/FinalizeCommand.js +1 -0
  78. package/dist/features/finalize/ports/outbound/FinalizeClock.d.ts +3 -0
  79. package/dist/features/finalize/ports/outbound/FinalizeClock.js +1 -0
  80. package/dist/features/finalize/ports/outbound/FinalizeConfigPort.d.ts +4 -0
  81. package/dist/features/finalize/ports/outbound/FinalizeConfigPort.js +1 -0
  82. package/dist/features/finalize/ports/outbound/FinalizeFileSystem.d.ts +14 -0
  83. package/dist/features/finalize/ports/outbound/FinalizeFileSystem.js +1 -0
  84. package/dist/features/finalize/ports/outbound/PlanRepo.d.ts +21 -0
  85. package/dist/features/finalize/ports/outbound/PlanRepo.js +1 -0
  86. package/dist/features/install/adapters/inbound/CliInstallHandler.d.ts +8 -0
  87. package/dist/features/install/adapters/inbound/CliInstallHandler.js +54 -0
  88. package/dist/features/install/adapters/outbound/NodeInstallSource.d.ts +7 -0
  89. package/dist/features/install/adapters/outbound/NodeInstallSource.js +24 -0
  90. package/dist/features/install/adapters/outbound/NodeInstallTargetFs.d.ts +7 -0
  91. package/dist/features/install/adapters/outbound/NodeInstallTargetFs.js +30 -0
  92. package/dist/features/install/application/InstallRules.d.ts +10 -0
  93. package/dist/features/install/application/InstallRules.js +73 -0
  94. package/dist/features/install/domain/InstallPlan.d.ts +27 -0
  95. package/dist/features/install/domain/InstallPlan.js +168 -0
  96. package/dist/features/install/domain/InstallResult.d.ts +23 -0
  97. package/dist/features/install/domain/InstallResult.js +1 -0
  98. package/dist/features/install/domain/InstallTarget.d.ts +6 -0
  99. package/dist/features/install/domain/InstallTarget.js +7 -0
  100. package/dist/features/install/domain/ManagedBlock.d.ts +3 -0
  101. package/dist/features/install/domain/ManagedBlock.js +20 -0
  102. package/dist/features/install/domain/RuleManifest.d.ts +17 -0
  103. package/dist/features/install/domain/RuleManifest.js +69 -0
  104. package/dist/features/install/domain/SettingsMerge.d.ts +5 -0
  105. package/dist/features/install/domain/SettingsMerge.js +43 -0
  106. package/dist/features/install/ports/inbound/InstallCommand.d.ts +10 -0
  107. package/dist/features/install/ports/inbound/InstallCommand.js +1 -0
  108. package/dist/features/install/ports/outbound/InstallSource.d.ts +4 -0
  109. package/dist/features/install/ports/outbound/InstallSource.js +1 -0
  110. package/dist/features/install/ports/outbound/InstallTargetFs.d.ts +6 -0
  111. package/dist/features/install/ports/outbound/InstallTargetFs.js +1 -0
  112. package/dist/features/lint/adapters/inbound/CliLintHandler.d.ts +8 -0
  113. package/dist/features/lint/adapters/inbound/CliLintHandler.js +61 -0
  114. package/dist/features/lint/adapters/outbound/NodeLintFileReader.d.ts +7 -0
  115. package/dist/features/lint/adapters/outbound/NodeLintFileReader.js +165 -0
  116. package/dist/features/lint/application/RunLint.d.ts +10 -0
  117. package/dist/features/lint/application/RunLint.js +100 -0
  118. package/dist/features/lint/domain/Diagnostic.d.ts +1 -0
  119. package/dist/features/lint/domain/Diagnostic.js +2 -0
  120. package/dist/features/lint/domain/Record.d.ts +1 -0
  121. package/dist/features/lint/domain/Record.js +5 -0
  122. package/dist/features/lint/domain/Rules.d.ts +1 -0
  123. package/dist/features/lint/domain/Rules.js +2 -0
  124. package/dist/features/lint/domain/SpecParser.d.ts +1 -0
  125. package/dist/features/lint/domain/SpecParser.js +2 -0
  126. package/dist/features/lint/ports/inbound/LintCommand.d.ts +4 -0
  127. package/dist/features/lint/ports/inbound/LintCommand.js +1 -0
  128. package/dist/features/lint/ports/outbound/LintConfigPort.d.ts +4 -0
  129. package/dist/features/lint/ports/outbound/LintConfigPort.js +1 -0
  130. package/dist/features/lint/ports/outbound/LintFileReader.d.ts +10 -0
  131. package/dist/features/lint/ports/outbound/LintFileReader.js +1 -0
  132. package/dist/features/plan/adapters/inbound/CliPlanShowHandler.d.ts +8 -0
  133. package/dist/features/plan/adapters/inbound/CliPlanShowHandler.js +73 -0
  134. package/dist/features/plan/adapters/outbound/NodePlanReader.d.ts +7 -0
  135. package/dist/features/plan/adapters/outbound/NodePlanReader.js +68 -0
  136. package/dist/features/plan/application/ShowPlan.d.ts +7 -0
  137. package/dist/features/plan/application/ShowPlan.js +4 -0
  138. package/dist/features/plan/ports/inbound/PlanShowCommand.d.ts +7 -0
  139. package/dist/features/plan/ports/inbound/PlanShowCommand.js +1 -0
  140. package/dist/features/plan/ports/outbound/PlanConfigPort.d.ts +4 -0
  141. package/dist/features/plan/ports/outbound/PlanConfigPort.js +1 -0
  142. package/dist/features/plan/ports/outbound/PlanReader.d.ts +19 -0
  143. package/dist/features/plan/ports/outbound/PlanReader.js +1 -0
  144. package/dist/features/ready/adapters/inbound/CliReadyHandler.d.ts +8 -0
  145. package/dist/features/ready/adapters/inbound/CliReadyHandler.js +79 -0
  146. package/dist/features/ready/adapters/outbound/ChildProcessReadyGit.d.ts +9 -0
  147. package/dist/features/ready/adapters/outbound/ChildProcessReadyGit.js +113 -0
  148. package/dist/features/ready/adapters/outbound/NodeReadyFileSystem.d.ts +8 -0
  149. package/dist/features/ready/adapters/outbound/NodeReadyFileSystem.js +159 -0
  150. package/dist/features/ready/application/RunReady.d.ts +16 -0
  151. package/dist/features/ready/application/RunReady.js +572 -0
  152. package/dist/features/ready/domain/AggregatedRules.d.ts +16 -0
  153. package/dist/features/ready/domain/AggregatedRules.js +42 -0
  154. package/dist/features/ready/domain/MarkerParser.d.ts +17 -0
  155. package/dist/features/ready/domain/MarkerParser.js +108 -0
  156. package/dist/features/ready/domain/PartitionResolver.d.ts +1 -0
  157. package/dist/features/ready/domain/PartitionResolver.js +5 -0
  158. package/dist/features/ready/domain/ReadyInput.d.ts +6 -0
  159. package/dist/features/ready/domain/ReadyInput.js +1 -0
  160. package/dist/features/ready/domain/ReadyViolation.d.ts +38 -0
  161. package/dist/features/ready/domain/ReadyViolation.js +19 -0
  162. package/dist/features/ready/domain/Rules.d.ts +22 -0
  163. package/dist/features/ready/domain/Rules.js +243 -0
  164. package/dist/features/ready/domain/SpecDiff.d.ts +33 -0
  165. package/dist/features/ready/domain/SpecDiff.js +321 -0
  166. package/dist/features/ready/ports/inbound/ReadyCommand.d.ts +4 -0
  167. package/dist/features/ready/ports/inbound/ReadyCommand.js +1 -0
  168. package/dist/features/ready/ports/outbound/ReadyConfigPort.d.ts +4 -0
  169. package/dist/features/ready/ports/outbound/ReadyConfigPort.js +1 -0
  170. package/dist/features/ready/ports/outbound/ReadyFileReader.d.ts +12 -0
  171. package/dist/features/ready/ports/outbound/ReadyFileReader.js +1 -0
  172. package/dist/features/ready/ports/outbound/ReadyGitPort.d.ts +10 -0
  173. package/dist/features/ready/ports/outbound/ReadyGitPort.js +5 -0
  174. package/dist/features/record/adapters/inbound/CliRecordHandler.d.ts +10 -0
  175. package/dist/features/record/adapters/inbound/CliRecordHandler.js +111 -0
  176. package/dist/features/record/adapters/outbound/NodeRecordFileSystem.d.ts +9 -0
  177. package/dist/features/record/adapters/outbound/NodeRecordFileSystem.js +152 -0
  178. package/dist/features/record/application/AddRecord.d.ts +11 -0
  179. package/dist/features/record/application/AddRecord.js +84 -0
  180. package/dist/features/record/application/GetRecord.d.ts +8 -0
  181. package/dist/features/record/application/GetRecord.js +22 -0
  182. package/dist/features/record/application/ListRecords.d.ts +9 -0
  183. package/dist/features/record/application/ListRecords.js +24 -0
  184. package/dist/features/record/application/SetRecord.d.ts +11 -0
  185. package/dist/features/record/application/SetRecord.js +68 -0
  186. package/dist/features/record/domain/RecordBody.d.ts +12 -0
  187. package/dist/features/record/domain/RecordBody.js +66 -0
  188. package/dist/features/record/domain/RecordPartition.d.ts +1 -0
  189. package/dist/features/record/domain/RecordPartition.js +7 -0
  190. package/dist/features/record/domain/RecordSlice.d.ts +7 -0
  191. package/dist/features/record/domain/RecordSlice.js +1 -0
  192. package/dist/features/record/domain/RecordSummary.d.ts +11 -0
  193. package/dist/features/record/domain/RecordSummary.js +13 -0
  194. package/dist/features/record/domain/RecordWrite.d.ts +14 -0
  195. package/dist/features/record/domain/RecordWrite.js +8 -0
  196. package/dist/features/record/ports/inbound/RecordCommand.d.ts +19 -0
  197. package/dist/features/record/ports/inbound/RecordCommand.js +1 -0
  198. package/dist/features/record/ports/outbound/RecordConfigPort.d.ts +4 -0
  199. package/dist/features/record/ports/outbound/RecordConfigPort.js +1 -0
  200. package/dist/features/record/ports/outbound/RecordFileReader.d.ts +10 -0
  201. package/dist/features/record/ports/outbound/RecordFileReader.js +1 -0
  202. package/dist/features/record/ports/outbound/RecordFileWriter.d.ts +6 -0
  203. package/dist/features/record/ports/outbound/RecordFileWriter.js +1 -0
  204. package/dist/features/refresh/adapters/inbound/CliRefreshHandler.d.ts +8 -0
  205. package/dist/features/refresh/adapters/inbound/CliRefreshHandler.js +24 -0
  206. package/dist/features/refresh/adapters/outbound/ChildProcessRefreshGit.d.ts +8 -0
  207. package/dist/features/refresh/adapters/outbound/ChildProcessRefreshGit.js +118 -0
  208. package/dist/features/refresh/adapters/outbound/NodeRefreshFileReader.d.ts +7 -0
  209. package/dist/features/refresh/adapters/outbound/NodeRefreshFileReader.js +44 -0
  210. package/dist/features/refresh/adapters/outbound/SystemRefreshClock.d.ts +4 -0
  211. package/dist/features/refresh/adapters/outbound/SystemRefreshClock.js +5 -0
  212. package/dist/features/refresh/application/BuildRefreshStubs.d.ts +25 -0
  213. package/dist/features/refresh/application/BuildRefreshStubs.js +43 -0
  214. package/dist/features/refresh/domain/DiffStubs.d.ts +24 -0
  215. package/dist/features/refresh/domain/DiffStubs.js +81 -0
  216. package/dist/features/refresh/domain/Footprint.d.ts +14 -0
  217. package/dist/features/refresh/domain/Footprint.js +45 -0
  218. package/dist/features/refresh/ports/inbound/RefreshCommand.d.ts +4 -0
  219. package/dist/features/refresh/ports/inbound/RefreshCommand.js +1 -0
  220. package/dist/features/refresh/ports/outbound/RefreshClockPort.d.ts +3 -0
  221. package/dist/features/refresh/ports/outbound/RefreshClockPort.js +1 -0
  222. package/dist/features/refresh/ports/outbound/RefreshConfigPort.d.ts +4 -0
  223. package/dist/features/refresh/ports/outbound/RefreshConfigPort.js +1 -0
  224. package/dist/features/refresh/ports/outbound/RefreshGitPort.d.ts +7 -0
  225. package/dist/features/refresh/ports/outbound/RefreshGitPort.js +1 -0
  226. package/dist/features/refresh/ports/outbound/RefreshSpecPort.d.ts +9 -0
  227. package/dist/features/refresh/ports/outbound/RefreshSpecPort.js +1 -0
  228. package/dist/features/report/adapters/inbound/CliReportHandler.d.ts +8 -0
  229. package/dist/features/report/adapters/inbound/CliReportHandler.js +35 -0
  230. package/dist/features/report/adapters/outbound/NodeReportFileSystem.d.ts +7 -0
  231. package/dist/features/report/adapters/outbound/NodeReportFileSystem.js +128 -0
  232. package/dist/features/report/application/RunReport.d.ts +19 -0
  233. package/dist/features/report/application/RunReport.js +161 -0
  234. package/dist/features/report/ports/inbound/ReportCommand.d.ts +8 -0
  235. package/dist/features/report/ports/inbound/ReportCommand.js +1 -0
  236. package/dist/features/report/ports/outbound/ReportConfigPort.d.ts +4 -0
  237. package/dist/features/report/ports/outbound/ReportConfigPort.js +1 -0
  238. package/dist/features/report/ports/outbound/ReportFileReader.d.ts +7 -0
  239. package/dist/features/report/ports/outbound/ReportFileReader.js +1 -0
  240. package/dist/features/token/adapters/inbound/CliTokenHandler.d.ts +8 -0
  241. package/dist/features/token/adapters/inbound/CliTokenHandler.js +53 -0
  242. package/dist/features/token/adapters/outbound/ChildProcessTokenGit.d.ts +8 -0
  243. package/dist/features/token/adapters/outbound/ChildProcessTokenGit.js +112 -0
  244. package/dist/features/token/adapters/outbound/NodeTokenConfigReader.d.ts +5 -0
  245. package/dist/features/token/adapters/outbound/NodeTokenConfigReader.js +41 -0
  246. package/dist/features/token/application/ComputeToken.d.ts +18 -0
  247. package/dist/features/token/application/ComputeToken.js +27 -0
  248. package/dist/features/token/ports/inbound/TokenCommand.d.ts +4 -0
  249. package/dist/features/token/ports/inbound/TokenCommand.js +1 -0
  250. package/dist/features/token/ports/outbound/TokenConfigPort.d.ts +4 -0
  251. package/dist/features/token/ports/outbound/TokenConfigPort.js +1 -0
  252. package/dist/features/token/ports/outbound/TokenGitPort.d.ts +7 -0
  253. package/dist/features/token/ports/outbound/TokenGitPort.js +1 -0
  254. package/dist/shared/domain/AgentBlocklist.d.ts +5 -0
  255. package/dist/shared/domain/AgentBlocklist.js +28 -0
  256. package/dist/shared/domain/BoundaryReachability.d.ts +5 -0
  257. package/dist/shared/domain/BoundaryReachability.js +71 -0
  258. package/dist/shared/domain/CheckOutcome.d.ts +10 -0
  259. package/dist/shared/domain/CheckOutcome.js +7 -0
  260. package/dist/shared/domain/CliOutput.d.ts +10 -0
  261. package/dist/shared/domain/CliOutput.js +29 -0
  262. package/dist/shared/domain/Config.d.ts +29 -0
  263. package/dist/shared/domain/Config.js +201 -0
  264. package/dist/shared/domain/DiagnosticRegistry.d.ts +8 -0
  265. package/dist/shared/domain/DiagnosticRegistry.js +71 -0
  266. package/dist/shared/domain/Errors.d.ts +12 -0
  267. package/dist/shared/domain/Errors.js +23 -0
  268. package/dist/shared/domain/GlobMatch.d.ts +2 -0
  269. package/dist/shared/domain/GlobMatch.js +58 -0
  270. package/dist/shared/domain/LintReport.d.ts +16 -0
  271. package/dist/shared/domain/LintReport.js +11 -0
  272. package/dist/shared/domain/LintRules.d.ts +67 -0
  273. package/dist/shared/domain/LintRules.js +956 -0
  274. package/dist/shared/domain/PartitionGrammar.d.ts +4 -0
  275. package/dist/shared/domain/PartitionGrammar.js +16 -0
  276. package/dist/shared/domain/PlanFile.d.ts +28 -0
  277. package/dist/shared/domain/PlanFile.js +112 -0
  278. package/dist/shared/domain/Scope.d.ts +1 -0
  279. package/dist/shared/domain/Scope.js +3 -0
  280. package/dist/shared/domain/SpecApprovalRewrite.d.ts +23 -0
  281. package/dist/shared/domain/SpecApprovalRewrite.js +254 -0
  282. package/dist/shared/domain/SpecBlocks.d.ts +12 -0
  283. package/dist/shared/domain/SpecBlocks.js +96 -0
  284. package/dist/shared/domain/SpecRecord.d.ts +17 -0
  285. package/dist/shared/domain/SpecRecord.js +208 -0
  286. package/dist/shared/domain/TemplateFieldMetadata.d.ts +2 -0
  287. package/dist/shared/domain/TemplateFieldMetadata.js +177 -0
  288. package/dist/shared/domain/Token.d.ts +2 -0
  289. package/dist/shared/domain/Token.js +5 -0
  290. package/dist/shared/domain/WeaselWords.d.ts +3 -0
  291. package/dist/shared/domain/WeaselWords.js +32 -0
  292. package/package.json +71 -0
  293. package/rules/enforcement_registry.md +126 -0
  294. package/rules/hooks/sdd-lint-reminder.sh +33 -0
  295. package/rules/hooks/sdd-spec-read-guard.sh +73 -0
  296. package/rules/manifest.json +15 -0
  297. package/rules/review-sdd.md +9 -0
  298. package/rules/sdd-cli-usage.md +91 -0
  299. package/rules/skills/spec-driven-development/SKILL.md +554 -0
  300. package/rules/skills/spec-driven-development/data/weasel-words.json +22 -0
  301. package/rules/spec-driven-development.md +69 -0
  302. package/rules/tdd-sdd.md +127 -0
  303. package/rules/workflow-sdd.md +56 -0
  304. package/schema/sdd.config.schema.json +104 -0
@@ -0,0 +1,956 @@
1
+ import { NORMATIVE_TEMPLATES, VALID_LIFECYCLE_STATUS, } from "./SpecRecord.js";
2
+ import { WEASEL_ABSOLUTE, WEASEL_MODAL_IN_NORMATIVE, WEASEL_WORDS as WEASEL_WORDS_SOT, } from "./WeaselWords.js";
3
+ /*
4
+ * Diagnostic rule-IDs emitted below are members of CTR-016 / SUR-009;
5
+ * DiagnosticRegistry.ts holds the canonical list (INV-010 coverage test).
6
+ */
7
+ /*
8
+ * Pure rule functions. Each returns 0..N diagnostics for a single record.
9
+ * No I/O. No globals. The caller wires Diagnostics together into a LintReport.
10
+ */
11
+ export const REQUIRED_PARTITION_SECTIONS = [
12
+ "1. Context",
13
+ "2. Glossary",
14
+ "3. Partition",
15
+ "4. Brownfield baseline",
16
+ "5. Surfaces",
17
+ "6. Requirements",
18
+ "7. Data contracts",
19
+ "8. Invariants",
20
+ "9. External dependencies",
21
+ "10. Generated artifacts",
22
+ "11. Localization",
23
+ "12. Policies",
24
+ "13. Constraints",
25
+ "14. Migrations",
26
+ "15. Deltas",
27
+ "16. Implementation bindings",
28
+ "17. Open questions",
29
+ "18. Assumptions",
30
+ "19. Out of scope",
31
+ ];
32
+ export const NORMATIVE_SECTIONS = [
33
+ "6. Requirements",
34
+ "7. Data contracts",
35
+ "8. Invariants",
36
+ "9. External dependencies",
37
+ "11. Localization",
38
+ "12. Policies",
39
+ "13. Constraints",
40
+ "14. Migrations",
41
+ "15. Deltas",
42
+ ];
43
+ /*
44
+ * Re-exported from WeaselWords.ts for backward-compatibility with the lint
45
+ * feature's domain shim. Two narrower exports are the new canonical names.
46
+ */
47
+ export const WEASEL_WORDS = WEASEL_WORDS_SOT;
48
+ export { WEASEL_ABSOLUTE, WEASEL_MODAL_IN_NORMATIVE };
49
+ const VALID_EVIDENCE = new Set([
50
+ "public_api",
51
+ "test_probe",
52
+ "db_constraint",
53
+ "operational_signal",
54
+ ]);
55
+ const VALID_STABILITY = new Set([
56
+ "contractual",
57
+ "internal",
58
+ ]);
59
+ const VALID_DATA_SCOPE_PREFIX = [
60
+ "new_writes_only",
61
+ "all_data",
62
+ "post_migration:",
63
+ ];
64
+ const VALID_VERIFICATION_STAGE = new Set([
65
+ "ci_unit",
66
+ "ci_integration",
67
+ "perf_lab",
68
+ "staging_canary",
69
+ "prod_slo",
70
+ ]);
71
+ const VALID_RUNTIME_STATE = new Set([
72
+ "pre_cutover",
73
+ "in_progress",
74
+ "cutover_done",
75
+ "rolled_back",
76
+ ]);
77
+ const VALID_DIRECTION = new Set([
78
+ "forward_only",
79
+ "reversible",
80
+ ]);
81
+ const VALID_MODE = new Set([
82
+ "online",
83
+ "offline",
84
+ "dual_write",
85
+ "backfill",
86
+ "dual_emit_with_legacy_text",
87
+ ]);
88
+ const VALID_BOUNDARY = new Set([
89
+ "api",
90
+ "sdk",
91
+ "event_bus",
92
+ "cli",
93
+ "public_db",
94
+ "public_storage",
95
+ "generated_published_artifact",
96
+ ]);
97
+ const OBLIGATIONLESS_TEMPLATES = new Set(["Surface"]);
98
+ function isMember(set, value) {
99
+ return set.has(value);
100
+ }
101
+ function stringifyValue(value) {
102
+ return typeof value === "string" ? value : JSON.stringify(value);
103
+ }
104
+ function isRecord(value) {
105
+ return typeof value === "object" && value !== null && !Array.isArray(value);
106
+ }
107
+ /* Per-record rules. */
108
+ export function lifecycleStatusRules(rec) {
109
+ const out = [];
110
+ const isNormative = rec.template !== null && isMember(NORMATIVE_TEMPLATES, rec.template);
111
+ if (!isNormative) {
112
+ return out;
113
+ }
114
+ if (rec.lifecycleStatus === null) {
115
+ out.push({
116
+ severity: "error",
117
+ rule: "sdd:lifecycle-status-present",
118
+ file: rec.file,
119
+ line: rec.line,
120
+ message: `ID "${rec.id}" missing lifecycle.status (SDD §1.6 + §14).`,
121
+ });
122
+ return out;
123
+ }
124
+ if (!isMember(VALID_LIFECYCLE_STATUS, rec.lifecycleStatus)) {
125
+ out.push({
126
+ severity: "error",
127
+ rule: "sdd:lifecycle-status-valid",
128
+ file: rec.file,
129
+ line: rec.line,
130
+ message: `ID "${rec.id}" has invalid lifecycle.status="${rec.lifecycleStatus}". Valid: draft|proposed|approved|deprecated|removed.`,
131
+ });
132
+ }
133
+ return out;
134
+ }
135
+ export function approvalRecordRules(rec) {
136
+ const out = [];
137
+ const isNormative = rec.template !== null && isMember(NORMATIVE_TEMPLATES, rec.template);
138
+ if (!isNormative) {
139
+ return out;
140
+ }
141
+ const status = rec.lifecycleStatus;
142
+ const ar = rec.approvalRecord;
143
+ if (status === "approved" ||
144
+ status === "deprecated" ||
145
+ status === "removed") {
146
+ if (ar === null || ar === "" || ar.startsWith("not_applicable")) {
147
+ out.push({
148
+ severity: "error",
149
+ rule: "sdd:approval-record-required",
150
+ file: rec.file,
151
+ line: rec.line,
152
+ message: `ID "${rec.id}" has lifecycle.status=${status} but no real approval_record (SDD §7.5).`,
153
+ });
154
+ }
155
+ }
156
+ if ((status === "draft" || status === "proposed") &&
157
+ ar !== null &&
158
+ ar !== "" &&
159
+ !ar.startsWith("not_applicable")) {
160
+ out.push({
161
+ severity: "error",
162
+ rule: "sdd:approval-record-forbidden",
163
+ file: rec.file,
164
+ line: rec.line,
165
+ message: `ID "${rec.id}" has lifecycle.status=${status} but approval_record is set (SDD §7.3).`,
166
+ });
167
+ }
168
+ return out;
169
+ }
170
+ export function testObligationRules(rec) {
171
+ const out = [];
172
+ const isNormative = rec.template !== null && isMember(NORMATIVE_TEMPLATES, rec.template);
173
+ if (!isNormative) {
174
+ return out;
175
+ }
176
+ if (rec.template !== null && OBLIGATIONLESS_TEMPLATES.has(rec.template)) {
177
+ return out;
178
+ }
179
+ const hasAny = rec.testObligations.length > 0 || rec.hasAliasedObligations;
180
+ if (hasAny) {
181
+ return out;
182
+ }
183
+ out.push({
184
+ severity: rec.template === "Constraint" ? "warn" : "error",
185
+ rule: "sdd:test-obligation-required",
186
+ file: rec.file,
187
+ line: rec.line,
188
+ message: `ID "${rec.id}" (template=${rec.template}) has no test_obligations / aliased obligation entry (SDD §4).`,
189
+ });
190
+ return out;
191
+ }
192
+ export function fieldTypeRules(rec) {
193
+ const out = [];
194
+ const v = rec.parsed;
195
+ if (rec.template !== "Surface") {
196
+ const versionV = v.version;
197
+ if (versionV !== undefined &&
198
+ (typeof versionV !== "number" || !Number.isInteger(versionV))) {
199
+ out.push({
200
+ severity: "error",
201
+ rule: "sdd:type-version-int",
202
+ file: rec.file,
203
+ line: rec.line,
204
+ message: `ID "${rec.id}" version="${stringifyValue(versionV)}" is not an integer (SDD §1.5).`,
205
+ });
206
+ }
207
+ }
208
+ if (rec.template === "Invariant") {
209
+ out.push(...invariantFieldTypeRules(rec));
210
+ }
211
+ const ds = v.data_scope;
212
+ if (typeof ds === "string" && ds !== "not_applicable") {
213
+ const ok = VALID_DATA_SCOPE_PREFIX.some((p) => ds === p || ds.startsWith(p));
214
+ if (!ok) {
215
+ out.push({
216
+ severity: "error",
217
+ rule: "sdd:type-data-scope",
218
+ file: rec.file,
219
+ line: rec.line,
220
+ message: `ID "${rec.id}" data_scope="${ds}" not in {new_writes_only, all_data, post_migration:<MIG-ID>} (SDD §14).`,
221
+ });
222
+ }
223
+ }
224
+ if (rec.template === "NFR") {
225
+ const stage = readVerificationStage(v);
226
+ if (stage !== null && !VALID_VERIFICATION_STAGE.has(stage)) {
227
+ out.push({
228
+ severity: "error",
229
+ rule: "sdd:type-nfr-stage",
230
+ file: rec.file,
231
+ line: rec.line,
232
+ message: `ID "${rec.id}" verification_stage="${stage}" not in {ci_unit, ci_integration, perf_lab, staging_canary, prod_slo} (SDD §9.4).`,
233
+ });
234
+ }
235
+ }
236
+ if (rec.template === "Migration") {
237
+ out.push(...migrationFieldTypeRules(rec));
238
+ }
239
+ if (rec.template === "Surface") {
240
+ const bt = v.boundary_type;
241
+ if (typeof bt === "string" && !VALID_BOUNDARY.has(bt)) {
242
+ out.push({
243
+ severity: "error",
244
+ rule: "sdd:type-surface-boundary-type",
245
+ file: rec.file,
246
+ line: rec.line,
247
+ message: `ID "${rec.id}" boundary_type="${bt}" not in {api, sdk, event_bus, cli, public_db, public_storage, generated_published_artifact} (SDD §1.4).`,
248
+ });
249
+ }
250
+ }
251
+ return out;
252
+ }
253
+ function invariantFieldTypeRules(rec) {
254
+ const out = [];
255
+ const v = rec.parsed;
256
+ const ev = v.evidence;
257
+ if (typeof ev === "string" && !VALID_EVIDENCE.has(ev)) {
258
+ out.push({
259
+ severity: "error",
260
+ rule: "sdd:type-invariant-evidence",
261
+ file: rec.file,
262
+ line: rec.line,
263
+ message: `ID "${rec.id}" evidence="${ev}" not in {public_api, test_probe, db_constraint, operational_signal} (SDD §1.7).`,
264
+ });
265
+ }
266
+ const st = v.stability;
267
+ if (typeof st === "string" && !VALID_STABILITY.has(st)) {
268
+ out.push({
269
+ severity: "error",
270
+ rule: "sdd:type-invariant-stability",
271
+ file: rec.file,
272
+ line: rec.line,
273
+ message: `ID "${rec.id}" stability="${st}" not in {contractual, internal} (SDD §1.7).`,
274
+ });
275
+ }
276
+ return out;
277
+ }
278
+ function migrationFieldTypeRules(rec) {
279
+ const out = [];
280
+ const v = rec.parsed;
281
+ const dir = v.direction;
282
+ if (typeof dir === "string" && !VALID_DIRECTION.has(dir)) {
283
+ out.push({
284
+ severity: "error",
285
+ rule: "sdd:type-migration-direction",
286
+ file: rec.file,
287
+ line: rec.line,
288
+ message: `ID "${rec.id}" direction="${dir}" not in {forward_only, reversible} (SDD §14).`,
289
+ });
290
+ }
291
+ const mode = v.mode;
292
+ if (typeof mode === "string" && !VALID_MODE.has(mode)) {
293
+ out.push({
294
+ severity: "error",
295
+ rule: "sdd:type-migration-mode",
296
+ file: rec.file,
297
+ line: rec.line,
298
+ message: `ID "${rec.id}" mode="${mode}" not in {online, offline, dual_write, backfill, dual_emit_with_legacy_text} (SDD §14).`,
299
+ });
300
+ }
301
+ const rs = v.runtime_state;
302
+ if (typeof rs === "string" && !VALID_RUNTIME_STATE.has(rs)) {
303
+ out.push({
304
+ severity: "error",
305
+ rule: "sdd:type-migration-runtime-state",
306
+ file: rec.file,
307
+ line: rec.line,
308
+ message: `ID "${rec.id}" runtime_state="${rs}" not in {pre_cutover, in_progress, cutover_done, rolled_back} (SDD §11.3-bis).`,
309
+ });
310
+ }
311
+ return out;
312
+ }
313
+ function readVerificationStage(v) {
314
+ const vo = v.verification_obligation;
315
+ if (isRecord(vo)) {
316
+ const stage = vo.verification_stage;
317
+ return typeof stage === "string" ? stage : null;
318
+ }
319
+ return null;
320
+ }
321
+ /*
322
+ * P1 — cheap requiredness rules (ENF-003/009/010/011/012). Each follows the
323
+ * same `(rec: LintRecord) => Diagnostic[]` signature as the §1.4 rules above.
324
+ */
325
+ import { isBlockedApprover } from "./AgentBlocklist.js";
326
+ /** ENF-003: Delta and Migration records MUST pin a baseline_version. */
327
+ export function baselineVersionRequiredRule(rec) {
328
+ if (rec.template !== "Delta" && rec.template !== "Migration") {
329
+ return [];
330
+ }
331
+ const v = rec.parsed.baseline_version;
332
+ if (typeof v === "string" && v.length > 0) {
333
+ return [];
334
+ }
335
+ if (typeof v === "number" && Number.isInteger(v)) {
336
+ return [];
337
+ }
338
+ return [
339
+ {
340
+ severity: "error",
341
+ rule: "sdd:baseline-version-required",
342
+ file: rec.file,
343
+ line: rec.line,
344
+ message: `ID "${rec.id}" (template=${rec.template}) is missing baseline_version (SDD §3 + §11).`,
345
+ },
346
+ ];
347
+ }
348
+ /** ENF-009: records with lifecycle.status=deprecated MUST carry both
349
+ * sunset_version and replacement_id (SDD §1.6). */
350
+ export function deprecatedFieldsRequiredRule(rec) {
351
+ if (rec.lifecycleStatus !== "deprecated") {
352
+ return [];
353
+ }
354
+ const out = [];
355
+ const sunset = rec.parsed.sunset_version;
356
+ if (typeof sunset !== "string" || sunset.length === 0) {
357
+ out.push({
358
+ severity: "error",
359
+ rule: "sdd:deprecated-fields-required",
360
+ file: rec.file,
361
+ line: rec.line,
362
+ message: `ID "${rec.id}" has lifecycle.status=deprecated but no sunset_version (SDD §1.6).`,
363
+ });
364
+ }
365
+ const repl = rec.parsed.replacement_id;
366
+ if (typeof repl !== "string" || repl.length === 0) {
367
+ out.push({
368
+ severity: "error",
369
+ rule: "sdd:deprecated-fields-required",
370
+ file: rec.file,
371
+ line: rec.line,
372
+ message: `ID "${rec.id}" has lifecycle.status=deprecated but no replacement_id (SDD §1.6).`,
373
+ });
374
+ }
375
+ return out;
376
+ }
377
+ /** ENF-059: an Open-Q with blocking=yes fails spec-valid (SDD §0);
378
+ * a `removed` Open-Q is already resolved and does not fire. */
379
+ export function openQBlockingRule(rec) {
380
+ if (rec.template !== "Open-Q") {
381
+ return [];
382
+ }
383
+ if (rec.lifecycleStatus === "removed") {
384
+ return [];
385
+ }
386
+ const blocking = rec.parsed.blocking;
387
+ if (blocking !== "yes" && blocking !== true) {
388
+ return [];
389
+ }
390
+ return [
391
+ {
392
+ severity: "error",
393
+ rule: "sdd:open-q-blocking",
394
+ file: rec.file,
395
+ line: rec.line,
396
+ message: `Open-Q "${rec.id}" is unresolved with blocking=yes (SDD §0 spec-valid).`,
397
+ },
398
+ ];
399
+ }
400
+ /** ENF-010: ASSUMPTIONs downgraded to blocking=advisory MUST carry an
401
+ * approval_record with a non-agent approver (SDD §7.5); OQ-018 tracks
402
+ * whether blocking=no should also fire. */
403
+ export function assumptionDowngradeApprovalRule(rec, approverBlocklist = []) {
404
+ if (rec.template !== "ASSUMPTION") {
405
+ return [];
406
+ }
407
+ const blocking = rec.parsed.blocking;
408
+ if (blocking !== "advisory") {
409
+ return [];
410
+ }
411
+ const approverIdent = readApproverIdentity(rec);
412
+ if (approverIdent === null) {
413
+ return [
414
+ {
415
+ severity: "error",
416
+ rule: "sdd:assumption-downgrade-approval",
417
+ file: rec.file,
418
+ line: rec.line,
419
+ message: `ASSUMPTION "${rec.id}" has blocking=advisory (downgrade) but no approval_record.approver_identity (SDD §7.5).`,
420
+ },
421
+ ];
422
+ }
423
+ if (isBlockedApprover(approverIdent, approverBlocklist)) {
424
+ return [
425
+ {
426
+ severity: "error",
427
+ rule: "sdd:assumption-downgrade-approval",
428
+ file: rec.file,
429
+ line: rec.line,
430
+ message: `ASSUMPTION "${rec.id}" downgrade approver "${approverIdent}" is in the agent blocklist (SDD §7.5: self-approval forbidden).`,
431
+ },
432
+ ];
433
+ }
434
+ return [];
435
+ }
436
+ function readApproverIdentity(rec) {
437
+ /*
438
+ * approval_record may live nested under lifecycle: or flat at the top level
439
+ * (the spec's two YAML shapes). Both expose approver_identity as a string.
440
+ */
441
+ const lifecycle = rec.parsed.lifecycle;
442
+ if (isRecord(lifecycle)) {
443
+ const nested = lifecycle.approval_record;
444
+ if (isRecord(nested)) {
445
+ const id = nested.approver_identity;
446
+ if (typeof id === "string" && id.length > 0) {
447
+ return id;
448
+ }
449
+ }
450
+ }
451
+ const flat = rec.parsed.approval_record;
452
+ if (isRecord(flat)) {
453
+ const id = flat.approver_identity;
454
+ if (typeof id === "string" && id.length > 0) {
455
+ return id;
456
+ }
457
+ }
458
+ return null;
459
+ }
460
+ /** ENF-011: Partition records MUST carry a default_policy_set field (an array;
461
+ * empty allowed — explicit "no policies" is still a typed value). */
462
+ export function partitionDefaultPolicySetRule(rec) {
463
+ if (rec.template !== "Partition") {
464
+ return [];
465
+ }
466
+ const v = rec.parsed.default_policy_set;
467
+ if (Array.isArray(v)) {
468
+ return [];
469
+ }
470
+ return [
471
+ {
472
+ severity: "error",
473
+ rule: "sdd:partition-default-policy-set",
474
+ file: rec.file,
475
+ line: rec.line,
476
+ message: `Partition "${rec.id}" is missing default_policy_set (must be an array, may be empty) (SDD §3).`,
477
+ },
478
+ ];
479
+ }
480
+ /** ENF-012: GeneratedArtifact records that publish a Surface (published_surface
481
+ * == "yes") MUST carry a surface_ref. */
482
+ export function generatedArtifactSurfaceRefRule(rec) {
483
+ if (rec.template !== "GeneratedArtifact") {
484
+ return [];
485
+ }
486
+ if (rec.parsed.published_surface !== "yes") {
487
+ return [];
488
+ }
489
+ const ref = rec.parsed.surface_ref;
490
+ if (typeof ref === "string" && ref.length > 0) {
491
+ return [];
492
+ }
493
+ return [
494
+ {
495
+ severity: "error",
496
+ rule: "sdd:generated-artifact-surface-ref",
497
+ file: rec.file,
498
+ line: rec.line,
499
+ message: `GeneratedArtifact "${rec.id}" has published_surface=yes but no surface_ref (SDD §10).`,
500
+ },
501
+ ];
502
+ }
503
+ /*
504
+ * P2.1 — Boundary requiredness (ENF-013/014/015/016). Each rule fires only
505
+ * when `rec.id ∈ boundaryIds`. The caller computes boundaryIds once per
506
+ * partition view via reachableBoundaryIds() and threads it in.
507
+ */
508
+ const REQUIRED_CONCURRENCY_FIELDS = [
509
+ "actor_concurrency",
510
+ "read_consistency",
511
+ "idempotency",
512
+ "time_source",
513
+ ];
514
+ /** ENF-013: boundary CTR/BEH must declare policy_refs (or an explicit
515
+ * policy_override block with a rationale). */
516
+ export function boundaryPolicyRefRule(rec, boundaryIds) {
517
+ if (!boundaryIds.has(rec.id)) {
518
+ return [];
519
+ }
520
+ const refs = rec.parsed.policy_refs;
521
+ if (Array.isArray(refs) && refs.length > 0) {
522
+ return [];
523
+ }
524
+ if (isObject(refs) && typeof refs.not_applicable === "string") {
525
+ return [];
526
+ }
527
+ const override = rec.parsed.policy_override;
528
+ if (isObject(override) && typeof override.rationale === "string") {
529
+ return [];
530
+ }
531
+ return [
532
+ {
533
+ severity: "error",
534
+ rule: "sdd:boundary-policy-ref",
535
+ file: rec.file,
536
+ line: rec.line,
537
+ message: `Boundary ${rec.template} "${rec.id}" must declare policy_refs (or policy_override.rationale) (SDD §12).`,
538
+ },
539
+ ];
540
+ }
541
+ /** ENF-014: boundary CTR/BEH must declare concurrency_model with the four
542
+ * required sub-fields. */
543
+ export function boundaryConcurrencyModelRule(rec, boundaryIds) {
544
+ if (!boundaryIds.has(rec.id)) {
545
+ return [];
546
+ }
547
+ const cm = rec.parsed.concurrency_model;
548
+ if (isObject(cm) && typeof cm.not_applicable === "string") {
549
+ return [];
550
+ }
551
+ if (!isObject(cm)) {
552
+ return [
553
+ {
554
+ severity: "error",
555
+ rule: "sdd:boundary-concurrency-model",
556
+ file: rec.file,
557
+ line: rec.line,
558
+ message: `Boundary ${rec.template} "${rec.id}" must declare concurrency_model with sub-fields ${REQUIRED_CONCURRENCY_FIELDS.join(", ")} (SDD §1.4).`,
559
+ },
560
+ ];
561
+ }
562
+ const obj = cm;
563
+ const missing = REQUIRED_CONCURRENCY_FIELDS.filter((k) => obj[k] === undefined || obj[k] === "");
564
+ if (missing.length === 0) {
565
+ return [];
566
+ }
567
+ return [
568
+ {
569
+ severity: "error",
570
+ rule: "sdd:boundary-concurrency-model",
571
+ file: rec.file,
572
+ line: rec.line,
573
+ message: `Boundary ${rec.template} "${rec.id}" concurrency_model is missing sub-fields: ${missing.join(", ")} (SDD §1.4).`,
574
+ },
575
+ ];
576
+ }
577
+ /** ENF-015: boundary CTR/BEH must declare an applicability field (a typed
578
+ * axis-classification block; `invariant_to_all_axes: true` is acceptable). */
579
+ export function applicabilityRequiredRule(rec, boundaryIds) {
580
+ if (!boundaryIds.has(rec.id)) {
581
+ return [];
582
+ }
583
+ const a = rec.parsed.applicability;
584
+ if (isObject(a) && Object.keys(a).length > 0) {
585
+ return [];
586
+ }
587
+ return [
588
+ {
589
+ severity: "error",
590
+ rule: "sdd:applicability-required",
591
+ file: rec.file,
592
+ line: rec.line,
593
+ message: `Boundary ${rec.template} "${rec.id}" is missing applicability (SDD §1.4 — feature_flag/tenant/locale/env/plan_tier/api_version axes).`,
594
+ },
595
+ ];
596
+ }
597
+ /** ENF-016: boundary CTR/BEH touching persistent state must declare
598
+ * data_scope (allowing `not_applicable` with a reason); persistence is
599
+ * detected heuristically — see the spec record for the predicate. */
600
+ export function dataScopeRequiredRule(rec, boundaryIds) {
601
+ if (!boundaryIds.has(rec.id)) {
602
+ return [];
603
+ }
604
+ const ds = rec.parsed.data_scope;
605
+ if (typeof ds === "string" && ds.length > 0) {
606
+ return [];
607
+ }
608
+ if (isObject(ds) && typeof ds.not_applicable === "string") {
609
+ return [];
610
+ }
611
+ return [
612
+ {
613
+ severity: "error",
614
+ rule: "sdd:data-scope-required",
615
+ file: rec.file,
616
+ line: rec.line,
617
+ message: `Boundary ${rec.template} "${rec.id}" is missing data_scope (SDD §14 — set new_writes_only / all_data / post_migration:<MIG>, or use not_applicable + reason).`,
618
+ },
619
+ ];
620
+ }
621
+ function isObject(v) {
622
+ return typeof v === "object" && v !== null && !Array.isArray(v);
623
+ }
624
+ /*
625
+ * P2.2 — Migration consistency (ENF-017/018). These rules need cross-record
626
+ * lookup so the per-record signature carries the full records list.
627
+ */
628
+ const POST_MIGRATION_PREFIX = "post_migration:";
629
+ /** ENF-017: an Invariant/Contract/Behavior with data_scope
630
+ * `post_migration:<MIG-ID>` requires the referenced Migration to exist and
631
+ * to declare a non-empty `enforcement_stage` (presence is what we check). */
632
+ export function migrationEnforcementStageRule(rec, records) {
633
+ if (rec.template !== "Invariant" &&
634
+ rec.template !== "Contract" &&
635
+ rec.template !== "Behavior") {
636
+ return [];
637
+ }
638
+ const ds = rec.parsed.data_scope;
639
+ if (typeof ds !== "string") {
640
+ return [];
641
+ }
642
+ if (!ds.startsWith(POST_MIGRATION_PREFIX)) {
643
+ return [];
644
+ }
645
+ const migId = ds.slice(POST_MIGRATION_PREFIX.length).trim();
646
+ if (migId.length === 0) {
647
+ return [];
648
+ }
649
+ const mig = records.find((r) => r.id === migId);
650
+ if (mig === undefined) {
651
+ return [
652
+ {
653
+ severity: "error",
654
+ rule: "sdd:migration-enforcement-stage",
655
+ file: rec.file,
656
+ line: rec.line,
657
+ message: `${rec.template} "${rec.id}" data_scope=${ds} but referenced Migration "${migId}" is not present in the partition spec (SDD §11.3-bis).`,
658
+ },
659
+ ];
660
+ }
661
+ const stage = mig.parsed.enforcement_stage;
662
+ if (typeof stage === "string" && stage.length > 0) {
663
+ return [];
664
+ }
665
+ if (isObject(stage) && typeof stage.marker === "string") {
666
+ return [];
667
+ }
668
+ return [
669
+ {
670
+ severity: "error",
671
+ rule: "sdd:migration-enforcement-stage",
672
+ file: mig.file,
673
+ line: mig.line,
674
+ message: `Migration "${mig.id}" must declare enforcement_stage with a test-controllable marker because ${rec.template} "${rec.id}" depends on it via data_scope=${ds} (SDD §11.3-bis).`,
675
+ },
676
+ ];
677
+ }
678
+ /** ENF-020 (P3.1): every Partition record must carry an `unmodeled_budget`
679
+ * block with the required sub-fields (current, baseline_at, baseline_value,
680
+ * trend) — see the spec record for the per-field constraints. */
681
+ export function debtBudgetFormRule(rec) {
682
+ if (rec.template !== "Partition") {
683
+ return [];
684
+ }
685
+ const budget = rec.parsed.unmodeled_budget;
686
+ if (!isObject(budget)) {
687
+ return [
688
+ {
689
+ severity: "error",
690
+ rule: "sdd:debt-budget-form",
691
+ file: rec.file,
692
+ line: rec.line,
693
+ message: `Partition "${rec.id}" is missing unmodeled_budget block (must declare current, baseline_at, baseline_value, trend) (SDD §3 — debt budget).`,
694
+ },
695
+ ];
696
+ }
697
+ const out = [];
698
+ const obj = budget;
699
+ const current = obj.current;
700
+ const baselineAt = obj.baseline_at;
701
+ const baselineValue = obj.baseline_value;
702
+ const trend = obj.trend;
703
+ const validTrends = new Set([
704
+ "monotonic_non_increasing",
705
+ "monotonic_decreasing",
706
+ ]);
707
+ if (typeof current !== "number" || !Number.isFinite(current) || current < 0) {
708
+ out.push({
709
+ severity: "error",
710
+ rule: "sdd:debt-budget-form",
711
+ file: rec.file,
712
+ line: rec.line,
713
+ message: `Partition "${rec.id}" unmodeled_budget.current must be an integer >= 0 (SDD §3 — debt budget).`,
714
+ });
715
+ }
716
+ if (typeof baselineAt !== "string" ||
717
+ !/^\d{4}-\d{2}-\d{2}/.test(baselineAt)) {
718
+ out.push({
719
+ severity: "error",
720
+ rule: "sdd:debt-budget-form",
721
+ file: rec.file,
722
+ line: rec.line,
723
+ message: `Partition "${rec.id}" unmodeled_budget.baseline_at must be an ISO date (SDD §3 — debt budget).`,
724
+ });
725
+ }
726
+ if (typeof baselineValue !== "number" ||
727
+ !Number.isFinite(baselineValue) ||
728
+ baselineValue < 0) {
729
+ out.push({
730
+ severity: "error",
731
+ rule: "sdd:debt-budget-form",
732
+ file: rec.file,
733
+ line: rec.line,
734
+ message: `Partition "${rec.id}" unmodeled_budget.baseline_value must be an integer >= 0 (SDD §3 — debt budget).`,
735
+ });
736
+ }
737
+ if (typeof trend !== "string" || !validTrends.has(trend)) {
738
+ out.push({
739
+ severity: "error",
740
+ rule: "sdd:debt-budget-form",
741
+ file: rec.file,
742
+ line: rec.line,
743
+ message: `Partition "${rec.id}" unmodeled_budget.trend must be in {monotonic_non_increasing, monotonic_decreasing} (SDD §3 — debt budget).`,
744
+ });
745
+ }
746
+ return out;
747
+ }
748
+ /** ENF-018: a Migration whose target_ids reference IDs from more than one
749
+ * partition MUST declare partition_slice[] entries with coordinator_id set. */
750
+ export function migrationCrossPartitionRule(rec) {
751
+ if (rec.template !== "Migration") {
752
+ return [];
753
+ }
754
+ const targets = rec.parsed.target_ids;
755
+ if (!Array.isArray(targets)) {
756
+ return [];
757
+ }
758
+ const partitions = new Set();
759
+ for (const t of targets) {
760
+ if (typeof t !== "string") {
761
+ continue;
762
+ }
763
+ const idx = t.indexOf(":");
764
+ if (idx <= 0) {
765
+ continue;
766
+ }
767
+ partitions.add(t.slice(0, idx));
768
+ }
769
+ if (partitions.size <= 1) {
770
+ return [];
771
+ }
772
+ const slices = rec.parsed.partition_slice;
773
+ if (!Array.isArray(slices) || slices.length === 0) {
774
+ return [
775
+ {
776
+ severity: "error",
777
+ rule: "sdd:migration-cross-partition",
778
+ file: rec.file,
779
+ line: rec.line,
780
+ message: `Migration "${rec.id}" target_ids span ${partitions.size} partitions (${[...partitions].sort().join(", ")}); cross-partition Migrations must declare partition_slice[] with coordinator_id (SDD §11.3-bis).`,
781
+ },
782
+ ];
783
+ }
784
+ const missingCoord = slices.some((s) => !isObject(s) || typeof s.coordinator_id !== "string");
785
+ if (missingCoord) {
786
+ return [
787
+ {
788
+ severity: "error",
789
+ rule: "sdd:migration-cross-partition",
790
+ file: rec.file,
791
+ line: rec.line,
792
+ message: `Migration "${rec.id}" partition_slice[] entries must each carry coordinator_id (SDD §11.3-bis).`,
793
+ },
794
+ ];
795
+ }
796
+ return [];
797
+ }
798
+ export function sectionViolations(markdown) {
799
+ const headings = parseHeadings(markdown);
800
+ const out = [];
801
+ for (let i = 0; i < REQUIRED_PARTITION_SECTIONS.length; i++) {
802
+ const required = REQUIRED_PARTITION_SECTIONS[i];
803
+ if (headings[i] === required) {
804
+ continue;
805
+ }
806
+ if (!headings.includes(required)) {
807
+ out.push({
808
+ rule: "sdd:section-presence",
809
+ message: `Missing required section "${required}" (SDD §2).`,
810
+ });
811
+ }
812
+ else {
813
+ out.push({
814
+ rule: "sdd:section-order",
815
+ message: `Section "${required}" is out of order; expected position ${i + 1}, found at position ${headings.indexOf(required) + 1}.`,
816
+ });
817
+ }
818
+ }
819
+ return out;
820
+ }
821
+ function parseHeadings(markdown) {
822
+ const out = [];
823
+ for (const line of markdown.split(/\r?\n/)) {
824
+ const m = /^##\s+(.+?)\s*$/.exec(line);
825
+ if (m !== null && /^\d+\./.test(m[1])) {
826
+ out.push(m[1]);
827
+ }
828
+ }
829
+ return out;
830
+ }
831
+ import { isFieldNormative } from "./TemplateFieldMetadata.js";
832
+ export function weaselFindings(markdown, records) {
833
+ const lines = markdown.split(/\r?\n/);
834
+ const out = absoluteWeaselFindings(lines);
835
+ if (records !== undefined && records.length > 0) {
836
+ out.push(...modalWeaselFindings(lines, records));
837
+ }
838
+ return out;
839
+ }
840
+ /* Pass 1: absolute weasels — section-aware only. */
841
+ function absoluteWeaselFindings(lines) {
842
+ const out = [];
843
+ let currentSection = "";
844
+ for (let i = 0; i < lines.length; i++) {
845
+ const line = lines[i];
846
+ const headingM = /^##\s+(.+?)\s*$/.exec(line);
847
+ if (headingM !== null) {
848
+ currentSection = headingM[1];
849
+ continue;
850
+ }
851
+ if (!NORMATIVE_SECTIONS.includes(currentSection)) {
852
+ continue;
853
+ }
854
+ const trimmed = line.trim();
855
+ if (trimmed.startsWith("#")) {
856
+ continue;
857
+ }
858
+ if (/^-?\s*(id:|test_obligations:|to:|target_ids:|target_id:|source_open_q:)/.test(trimmed)) {
859
+ continue;
860
+ }
861
+ if (/^to:[a-z-]+:[a-z-]+:/.test(trimmed)) {
862
+ continue;
863
+ }
864
+ const lower = line.toLowerCase();
865
+ for (const w of WEASEL_ABSOLUTE) {
866
+ if (lower.includes(w.toLowerCase())) {
867
+ out.push({ line: i + 1, word: w, section: currentSection });
868
+ break;
869
+ }
870
+ }
871
+ }
872
+ return out;
873
+ }
874
+ /* Pass 2: modal weasels — field-aware. */
875
+ function modalWeaselFindings(lines, records) {
876
+ const out = [];
877
+ let currentSection = "";
878
+ for (let i = 0; i < lines.length; i++) {
879
+ const line = lines[i];
880
+ const headingM = /^##\s+(.+?)\s*$/.exec(line);
881
+ if (headingM !== null) {
882
+ currentSection = headingM[1];
883
+ continue;
884
+ }
885
+ if (!NORMATIVE_SECTIONS.includes(currentSection)) {
886
+ continue;
887
+ }
888
+ const lower = line.toLowerCase();
889
+ let matched = null;
890
+ for (const w of WEASEL_MODAL_IN_NORMATIVE) {
891
+ if (lower.includes(w.toLowerCase())) {
892
+ matched = w;
893
+ break;
894
+ }
895
+ }
896
+ if (matched === null) {
897
+ continue;
898
+ }
899
+ const fieldInfo = findFieldAtLine(lines, records, i + 1);
900
+ if (fieldInfo === null) {
901
+ continue;
902
+ }
903
+ if (!isFieldNormative(fieldInfo.record.template, fieldInfo.field)) {
904
+ continue;
905
+ }
906
+ out.push({
907
+ line: i + 1,
908
+ word: matched,
909
+ section: currentSection,
910
+ field: `${fieldInfo.record.template}.${fieldInfo.field}`,
911
+ });
912
+ }
913
+ return out;
914
+ }
915
+ /**
916
+ * Map a 1-based file line to the owning LintRecord + top-level YAML field:
917
+ * the most recent unindented `<key>:` at or before `line`, bounded by the
918
+ * record's closing fence (or the next record's start).
919
+ */
920
+ function findFieldAtLine(lines, records, line) {
921
+ let owner = null;
922
+ for (const r of records) {
923
+ if (r.line <= line && (owner === null || r.line > owner.line)) {
924
+ owner = r;
925
+ }
926
+ }
927
+ if (owner === null) {
928
+ return null;
929
+ }
930
+ const ownerIdx = owner.line - 1;
931
+ let recordEndIdx = lines.length - 1;
932
+ for (let i = ownerIdx; i < lines.length; i++) {
933
+ if (i > ownerIdx && /^```\s*$/.test(lines[i])) {
934
+ recordEndIdx = i - 1;
935
+ break;
936
+ }
937
+ }
938
+ if (line - 1 > recordEndIdx) {
939
+ return null;
940
+ }
941
+ const topLevelRe = /^([a-z_][a-z0-9_]*)\s*:/i;
942
+ let currentField = null;
943
+ for (let i = ownerIdx; i <= recordEndIdx; i++) {
944
+ if (i > line - 1) {
945
+ break;
946
+ }
947
+ const m = topLevelRe.exec(lines[i]);
948
+ if (m !== null) {
949
+ currentField = m[1];
950
+ }
951
+ }
952
+ if (currentField === null) {
953
+ return null;
954
+ }
955
+ return { record: owner, field: currentField };
956
+ }