aiwf 0.3.22 → 0.4.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.
Files changed (456) hide show
  1. package/.claude-plugin/marketplace.json +146 -0
  2. package/CHANGELOG.md +49 -1
  3. package/README.ko.md +118 -309
  4. package/README.md +68 -254
  5. package/docs/modernization/BROWNFIELD-GREENFIELD.ko.md +105 -0
  6. package/docs/modernization/CLAUDE-PLAN-REVIEW-2026-10-03.ko.md +77 -0
  7. package/docs/modernization/CLI-PRODUCTIVITY-REVIEWED-2026-10-03.ko.md +177 -0
  8. package/docs/modernization/CLI-PRODUCTIVITY.ko.md +193 -0
  9. package/docs/modernization/CORE-PACKAGE-PLAN.md +7 -0
  10. package/docs/modernization/DEEP-REVERSE-ENGINEERING.ko.md +88 -0
  11. package/docs/modernization/DELEGATION-OPTIONAL.ko.md +57 -0
  12. package/docs/modernization/DIRECTION.ko.md +56 -0
  13. package/docs/modernization/FULL-TEST-2026-10-03.md +49 -0
  14. package/docs/modernization/LEGACY-REMOVAL-PLAN.md +9 -0
  15. package/docs/modernization/PILOT-RESULT-2026-10-03.ko.md +85 -0
  16. package/docs/modernization/PILOT-UC-001.ko.md +79 -0
  17. package/docs/modernization/PLAN.md +28 -0
  18. package/docs/modernization/SKILLS.ko.md +105 -0
  19. package/docs/modernization/SPRINTABLE.ko.md +47 -0
  20. package/docs/modernization/SYNC-DOCS-VALIDATION-2026-10-04.ko.md +38 -0
  21. package/docs/modernization/VALIDATION.md +79 -0
  22. package/docs/modernization/evidence/example-review-packet.json +115 -0
  23. package/docs/modernization/evidence/example-spec-pin.json +48 -0
  24. package/docs/modernization/evidence/full-test-20261003/claude-lint.md +22 -0
  25. package/docs/modernization/evidence/full-test-20261003/claude-review-retry.md +56 -0
  26. package/docs/modernization/evidence/full-test-20261003/codex-review-packet.json +159 -0
  27. package/docs/modernization/evidence/full-test-20261003/src/expense.mjs +40 -0
  28. package/docs/modernization/evidence/full-test-20261003/summary.json +106 -0
  29. package/docs/modernization/evidence/full-test-20261003/tests/expense.test.mjs +61 -0
  30. package/docs/modernization/evidence/local-pilot-20261003/README.ko.md +40 -0
  31. package/docs/modernization/evidence/local-pilot-20261003/execution.json +1055 -0
  32. package/docs/modernization/evidence/local-pilot-20261003/maintenance/dependencies.json +24 -0
  33. package/docs/modernization/evidence/local-pilot-20261003/maintenance/dependencies.stderr.txt +0 -0
  34. package/docs/modernization/evidence/local-pilot-20261003/maintenance/dependencies.stdout.txt +5 -0
  35. package/docs/modernization/evidence/local-pilot-20261003/maintenance/docs.json +24 -0
  36. package/docs/modernization/evidence/local-pilot-20261003/maintenance/docs.stderr.txt +0 -0
  37. package/docs/modernization/evidence/local-pilot-20261003/maintenance/docs.stdout.txt +6 -0
  38. package/docs/modernization/evidence/local-pilot-20261003/maintenance/example.json +24 -0
  39. package/docs/modernization/evidence/local-pilot-20261003/maintenance/example.stderr.txt +0 -0
  40. package/docs/modernization/evidence/local-pilot-20261003/maintenance/example.stdout.txt +5 -0
  41. package/docs/modernization/evidence/local-pilot-20261003/maintenance/local-docs.json +24 -0
  42. package/docs/modernization/evidence/local-pilot-20261003/maintenance/local-docs.stderr.txt +0 -0
  43. package/docs/modernization/evidence/local-pilot-20261003/maintenance/local-docs.stdout.txt +6 -0
  44. package/docs/modernization/evidence/local-pilot-20261003/maintenance/node.json +23 -0
  45. package/docs/modernization/evidence/local-pilot-20261003/maintenance/node.stderr.txt +0 -0
  46. package/docs/modernization/evidence/local-pilot-20261003/maintenance/node.stdout.txt +535 -0
  47. package/docs/modernization/evidence/local-pilot-20261003/maintenance/provenance.json +24 -0
  48. package/docs/modernization/evidence/local-pilot-20261003/maintenance/provenance.stderr.txt +0 -0
  49. package/docs/modernization/evidence/local-pilot-20261003/maintenance/provenance.stdout.txt +5 -0
  50. package/docs/modernization/evidence/local-pilot-20261003/maintenance/readme-replay.json +24 -0
  51. package/docs/modernization/evidence/local-pilot-20261003/maintenance/readme-replay.stderr.txt +0 -0
  52. package/docs/modernization/evidence/local-pilot-20261003/maintenance/readme-replay.stdout.txt +273 -0
  53. package/docs/modernization/evidence/local-pilot-20261003/maintenance/upstream.json +24 -0
  54. package/docs/modernization/evidence/local-pilot-20261003/maintenance/upstream.stderr.txt +0 -0
  55. package/docs/modernization/evidence/local-pilot-20261003/maintenance/upstream.stdout.txt +7 -0
  56. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/archive-service.stderr.txt +0 -0
  57. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/archive-service.stdout.txt +148 -0
  58. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/archive-structure.stderr.txt +0 -0
  59. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/archive-structure.stdout.txt +9 -0
  60. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-check.stderr.txt +0 -0
  61. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-check.stdout.txt +67 -0
  62. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-evidence.json +34 -0
  63. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-packet.json +128 -0
  64. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-packet.stderr.txt +0 -0
  65. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-packet.stdout.txt +128 -0
  66. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-pin.json +54 -0
  67. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-pin.stderr.txt +0 -0
  68. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-pin.stdout.txt +60 -0
  69. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-readback.json +15 -0
  70. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-service.stderr.txt +0 -0
  71. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-service.stdout.txt +82 -0
  72. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-structure.stderr.txt +0 -0
  73. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-structure.stdout.txt +9 -0
  74. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-check.stderr.txt +0 -0
  75. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-check.stdout.txt +79 -0
  76. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-packet-refused.stderr.txt +0 -0
  77. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-packet-refused.stdout.txt +7 -0
  78. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-pin-refresh.stderr.txt +0 -0
  79. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-pin-refresh.stdout.txt +66 -0
  80. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-pin.json +60 -0
  81. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-structure.stderr.txt +0 -0
  82. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-structure.stdout.txt +9 -0
  83. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-check.stderr.txt +0 -0
  84. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-check.stdout.txt +73 -0
  85. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-evidence.json +34 -0
  86. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-packet.json +134 -0
  87. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-packet.stderr.txt +0 -0
  88. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-packet.stdout.txt +134 -0
  89. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-readback.json +15 -0
  90. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-service.stderr.txt +0 -0
  91. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-service.stdout.txt +148 -0
  92. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-structure.stderr.txt +0 -0
  93. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-structure.stdout.txt +9 -0
  94. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/node-version.stderr.txt +0 -0
  95. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/node-version.stdout.txt +1 -0
  96. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/python-version.stderr.txt +0 -0
  97. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/python-version.stdout.txt +1 -0
  98. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-evidence.json +34 -0
  99. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-packet.json +134 -0
  100. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-packet.stderr.txt +0 -0
  101. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-packet.stdout.txt +134 -0
  102. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-readback.json +15 -0
  103. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-service.stderr.txt +0 -0
  104. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-service.stdout.txt +266 -0
  105. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-structure.stderr.txt +0 -0
  106. package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-structure.stdout.txt +9 -0
  107. package/docs/modernization/evidence/local-pilot-20261003/project/docs/entity_model.md +33 -0
  108. package/docs/modernization/evidence/local-pilot-20261003/project/docs/glossary.md +9 -0
  109. package/docs/modernization/evidence/local-pilot-20261003/project/docs/plans/UC-001.md +20 -0
  110. package/docs/modernization/evidence/local-pilot-20261003/project/docs/requirements.md +10 -0
  111. package/docs/modernization/evidence/local-pilot-20261003/project/docs/test_cases/TC-001-submit-expense.md +37 -0
  112. package/docs/modernization/evidence/local-pilot-20261003/project/docs/test_cases/TC-002-description-limit.md +42 -0
  113. package/docs/modernization/evidence/local-pilot-20261003/project/docs/use_cases/UC-001-submit-expense.md +76 -0
  114. package/docs/modernization/evidence/local-pilot-20261003/project/docs/use_cases.puml +12 -0
  115. package/docs/modernization/evidence/local-pilot-20261003/project/docs/vision.md +21 -0
  116. package/docs/modernization/evidence/local-pilot-20261003/project/src/expense.mjs +48 -0
  117. package/docs/modernization/evidence/local-pilot-20261003/project/tests/expense.test.mjs +116 -0
  118. package/docs/modernization/evidence/local-pilot-20261003/scope.json +31 -0
  119. package/docs/modernization/reviews/2026-10-03-claude-cli/CONTRACTS.ko.md +122 -0
  120. package/docs/modernization/reviews/2026-10-03-claude-cli/DIRECTION.ko.md +84 -0
  121. package/examples/spec-workflow/README.md +54 -0
  122. package/examples/spec-workflow/docs/entity_model.md +33 -0
  123. package/examples/spec-workflow/docs/glossary.md +9 -0
  124. package/examples/spec-workflow/docs/requirements.md +10 -0
  125. package/examples/spec-workflow/docs/test_cases/TC-001-submit-expense.md +37 -0
  126. package/examples/spec-workflow/docs/use_cases/UC-001-submit-expense.md +63 -0
  127. package/examples/spec-workflow/docs/use_cases.puml +12 -0
  128. package/examples/spec-workflow/docs/vision.md +21 -0
  129. package/package.json +37 -104
  130. package/plugins/aiwf-angular-jpa/.claude-plugin/plugin.json +11 -0
  131. package/plugins/aiwf-angular-jpa/LICENSE +201 -0
  132. package/plugins/aiwf-angular-jpa/NOTICE +16 -0
  133. package/plugins/aiwf-angular-jpa/README.md +54 -0
  134. package/plugins/aiwf-angular-jpa/UPSTREAM.json +35 -0
  135. package/plugins/aiwf-angular-jpa/agents/uc-coverage.md +263 -0
  136. package/plugins/aiwf-angular-jpa/rules/mcp-servers.md +64 -0
  137. package/plugins/aiwf-angular-jpa/skills/coverage-check/SKILL.md +190 -0
  138. package/plugins/aiwf-angular-jpa/skills/flyway-migration/SKILL.md +119 -0
  139. package/plugins/aiwf-angular-jpa/skills/implement/SKILL.md +528 -0
  140. package/plugins/aiwf-angular-jpa/skills/implement/references/module-layout.md +95 -0
  141. package/plugins/aiwf-angular-jpa/skills/playwright-test/SKILL.md +360 -0
  142. package/plugins/aiwf-angular-jpa/skills/spring-boot-test/SKILL.md +504 -0
  143. package/plugins/aiwf-angular-jpa/skills/vitest-test/SKILL.md +309 -0
  144. package/plugins/aiwf-blazor-dotnet/.claude-plugin/plugin.json +11 -0
  145. package/plugins/aiwf-blazor-dotnet/LICENSE +201 -0
  146. package/plugins/aiwf-blazor-dotnet/NOTICE +16 -0
  147. package/plugins/aiwf-blazor-dotnet/README.md +53 -0
  148. package/plugins/aiwf-blazor-dotnet/UPSTREAM.json +31 -0
  149. package/plugins/aiwf-blazor-dotnet/rules/mcp-servers.md +25 -0
  150. package/plugins/aiwf-blazor-dotnet/skills/bunit-test/SKILL.md +110 -0
  151. package/plugins/aiwf-blazor-dotnet/skills/dotnet-test/SKILL.md +108 -0
  152. package/plugins/aiwf-blazor-dotnet/skills/ef-migration/SKILL.md +43 -0
  153. package/plugins/aiwf-blazor-dotnet/skills/implement/SKILL.md +149 -0
  154. package/plugins/aiwf-blazor-dotnet/skills/playwright-test/SKILL.md +130 -0
  155. package/plugins/aiwf-core/.claude-plugin/plugin.json +11 -0
  156. package/plugins/aiwf-core/LICENSE +201 -0
  157. package/plugins/aiwf-core/NOTICE +20 -0
  158. package/plugins/aiwf-core/README.md +38 -0
  159. package/plugins/aiwf-core/UPSTREAM.json +50 -0
  160. package/plugins/aiwf-core/skills/entity-model/SKILL.md +160 -0
  161. package/plugins/aiwf-core/skills/entity-model/references/REFERENCE.md +36 -0
  162. package/plugins/aiwf-core/skills/requirements/SKILL.md +157 -0
  163. package/plugins/aiwf-core/skills/requirements/references/REFERENCE.md +70 -0
  164. package/plugins/aiwf-core/skills/requirements/references/glossary.md +7 -0
  165. package/plugins/aiwf-core/skills/reverse-engineer/SKILL.md +509 -0
  166. package/plugins/aiwf-core/skills/reverse-engineer/references/stack-signals.md +210 -0
  167. package/plugins/aiwf-core/skills/spec-review/SKILL.md +197 -0
  168. package/plugins/aiwf-core/skills/spec-review/references/lint-codes.md +63 -0
  169. package/plugins/aiwf-core/skills/spec-review/references/review-checklist.md +189 -0
  170. package/plugins/aiwf-core/skills/spec-review/scripts/spec_lint.py +1216 -0
  171. package/plugins/aiwf-core/skills/test-case/SKILL.md +138 -0
  172. package/plugins/aiwf-core/skills/test-case/references/example-process.bpmn +118 -0
  173. package/plugins/aiwf-core/skills/test-case/references/example.md +38 -0
  174. package/plugins/aiwf-core/skills/test-case/references/test-case.md +35 -0
  175. package/plugins/aiwf-core/skills/test-case/scripts/bpmn_paths.py +542 -0
  176. package/plugins/aiwf-core/skills/use-case-diagram/SKILL.md +99 -0
  177. package/plugins/aiwf-core/skills/use-case-spec/SKILL.md +257 -0
  178. package/plugins/aiwf-core/skills/use-case-spec/references/clarify-checklist.md +70 -0
  179. package/plugins/aiwf-core/skills/use-case-spec/references/example.md +88 -0
  180. package/plugins/aiwf-core/skills/use-case-spec/references/format-spec.md +246 -0
  181. package/plugins/aiwf-core/skills/use-case-spec/references/use-case.md +50 -0
  182. package/plugins/aiwf-core/skills/use-case-spec/scripts/validate_use_case.py +941 -0
  183. package/plugins/aiwf-delegate-claude/.claude-plugin/plugin.json +9 -0
  184. package/{src/lib/resources/templates/npm-library/template → plugins/aiwf-delegate-claude}/LICENSE +2 -2
  185. package/plugins/aiwf-delegate-claude/NOTICE +4 -0
  186. package/plugins/aiwf-delegate-claude/README.md +10 -0
  187. package/plugins/aiwf-delegate-claude/plugin.json +13 -0
  188. package/plugins/aiwf-delegate-claude/skills/delegate-claude/LICENSE +21 -0
  189. package/plugins/aiwf-delegate-claude/skills/delegate-claude/NOTICE +4 -0
  190. package/plugins/aiwf-delegate-claude/skills/delegate-claude/SKILL.md +49 -0
  191. package/plugins/aiwf-delegate-claude/skills/delegate-claude/agents/openai.yaml +2 -0
  192. package/plugins/aiwf-delegate-codex/.claude-plugin/plugin.json +9 -0
  193. package/plugins/aiwf-delegate-codex/LICENSE +21 -0
  194. package/plugins/aiwf-delegate-codex/NOTICE +4 -0
  195. package/plugins/aiwf-delegate-codex/README.md +10 -0
  196. package/plugins/aiwf-delegate-codex/plugin.json +13 -0
  197. package/plugins/aiwf-delegate-codex/skills/delegate-codex/LICENSE +21 -0
  198. package/plugins/aiwf-delegate-codex/skills/delegate-codex/NOTICE +4 -0
  199. package/plugins/aiwf-delegate-codex/skills/delegate-codex/SKILL.md +47 -0
  200. package/plugins/aiwf-delegate-codex/skills/delegate-codex/agents/openai.yaml +2 -0
  201. package/plugins/aiwf-nestjs-nextjs/.claude-plugin/plugin.json +11 -0
  202. package/plugins/aiwf-nestjs-nextjs/LICENSE +201 -0
  203. package/plugins/aiwf-nestjs-nextjs/NOTICE +16 -0
  204. package/plugins/aiwf-nestjs-nextjs/README.md +53 -0
  205. package/plugins/aiwf-nestjs-nextjs/UPSTREAM.json +32 -0
  206. package/plugins/aiwf-nestjs-nextjs/rules/mcp-servers.md +59 -0
  207. package/plugins/aiwf-nestjs-nextjs/skills/drizzle-migration/SKILL.md +222 -0
  208. package/plugins/aiwf-nestjs-nextjs/skills/implement/SKILL.md +393 -0
  209. package/plugins/aiwf-nestjs-nextjs/skills/implement/references/project-layout.md +158 -0
  210. package/plugins/aiwf-nestjs-nextjs/skills/nest-test/SKILL.md +300 -0
  211. package/plugins/aiwf-nestjs-nextjs/skills/playwright-test/SKILL.md +245 -0
  212. package/plugins/aiwf-nestjs-nextjs/skills/react-test/SKILL.md +217 -0
  213. package/plugins/aiwf-spec/.claude-plugin/plugin.json +9 -0
  214. package/plugins/aiwf-spec/LICENSE +201 -0
  215. package/plugins/aiwf-spec/NOTICE +20 -0
  216. package/plugins/aiwf-spec/README.md +36 -0
  217. package/plugins/aiwf-spec/skills/sync-docs/SKILL.md +47 -0
  218. package/plugins/aiwf-spec/skills/workflow/SKILL.md +42 -0
  219. package/plugins/aiwf-vaadin-jooq/.claude-plugin/plugin.json +11 -0
  220. package/plugins/aiwf-vaadin-jooq/LICENSE +201 -0
  221. package/plugins/aiwf-vaadin-jooq/NOTICE +16 -0
  222. package/plugins/aiwf-vaadin-jooq/README.md +56 -0
  223. package/plugins/aiwf-vaadin-jooq/UPSTREAM.json +43 -0
  224. package/plugins/aiwf-vaadin-jooq/agents/uc-coverage.md +260 -0
  225. package/plugins/aiwf-vaadin-jooq/rules/mcp-servers.md +44 -0
  226. package/plugins/aiwf-vaadin-jooq/skills/browserless-test/SKILL.md +407 -0
  227. package/plugins/aiwf-vaadin-jooq/skills/browserless-test/references/UC001ManagePersonsTest.java +111 -0
  228. package/plugins/aiwf-vaadin-jooq/skills/coverage-check/SKILL.md +190 -0
  229. package/plugins/aiwf-vaadin-jooq/skills/flyway-migration/SKILL.md +70 -0
  230. package/plugins/aiwf-vaadin-jooq/skills/hilla-test/SKILL.md +350 -0
  231. package/plugins/aiwf-vaadin-jooq/skills/hilla-test/references/UC001ManagePersonsServiceTest.java +81 -0
  232. package/plugins/aiwf-vaadin-jooq/skills/hilla-test/references/UC001ManagePersonsViewTest.tsx +87 -0
  233. package/plugins/aiwf-vaadin-jooq/skills/implement/SKILL.md +196 -0
  234. package/plugins/aiwf-vaadin-jooq/skills/implement-hilla/SKILL.md +216 -0
  235. package/plugins/aiwf-vaadin-jooq/skills/karibu-test/SKILL.md +298 -0
  236. package/plugins/aiwf-vaadin-jooq/skills/karibu-test/references/UC001ManagePersonsTest.java +93 -0
  237. package/plugins/aiwf-vaadin-jooq/skills/playwright-test/SKILL.md +237 -0
  238. package/plugins/aiwf-vaadin-jooq/skills/playwright-test/references/ExampleViewIT.java +143 -0
  239. package/plugins/aiwf-vaadin-jooq/skills/playwright-test/references/TC001CustomerOnboardingIT.java +144 -0
  240. package/plugins/aiwf-vaadin-jooq/skills/playwright-test/references/dramafinder-api.md +190 -0
  241. package/scripts/check-dependencies.js +26 -96
  242. package/scripts/install-spec-skills.mjs +126 -0
  243. package/scripts/validate-spec-plugin.mjs +145 -0
  244. package/src/cli/spec-cli.js +292 -0
  245. package/src/lib/spec-workflow.js +982 -0
  246. package/docs/ADR_MANAGEMENT_GUIDE.ko.md +0 -602
  247. package/docs/ADR_MANAGEMENT_GUIDE.md +0 -602
  248. package/docs/AI-WORKFLOW.ko.md +0 -299
  249. package/docs/AI-WORKFLOW.md +0 -401
  250. package/docs/API_REFERENCE_FULL.ko.md +0 -1135
  251. package/docs/API_REFERENCE_FULL.md +0 -1135
  252. package/docs/ARCHITECTURE.ko.md +0 -314
  253. package/docs/ARCHITECTURE.md +0 -314
  254. package/docs/CLI_USAGE_GUIDE.ko.md +0 -634
  255. package/docs/CLI_USAGE_GUIDE.md +0 -640
  256. package/docs/CODE_CLEANUP_GUIDE.ko.md +0 -415
  257. package/docs/CODE_CLEANUP_GUIDE.md +0 -415
  258. package/docs/COMMANDS_GUIDE.ko.md +0 -1037
  259. package/docs/COMMANDS_GUIDE.md +0 -1037
  260. package/docs/CONTRIBUTING.ko.md +0 -408
  261. package/docs/CONTRIBUTING.md +0 -408
  262. package/docs/DEVELOPMENT_GUIDE.ko.md +0 -440
  263. package/docs/DEVELOPMENT_GUIDE.md +0 -727
  264. package/docs/EXAMPLES.ko.md +0 -695
  265. package/docs/EXAMPLES.md +0 -693
  266. package/docs/GETTING_STARTED.ko.md +0 -219
  267. package/docs/GETTING_STARTED.md +0 -476
  268. package/docs/MODULE_MANAGEMENT_GUIDE.ko.md +0 -289
  269. package/docs/MODULE_MANAGEMENT_GUIDE.md +0 -289
  270. package/docs/PERFORMANCE_ARCHITECTURE.md +0 -494
  271. package/docs/PERFORMANCE_GUIDELINES.ko.md +0 -388
  272. package/docs/PERFORMANCE_GUIDELINES.md +0 -553
  273. package/docs/PRD.ko.md +0 -148
  274. package/docs/PRD.md +0 -150
  275. package/docs/ROADMAP_v0.4.0.md +0 -286
  276. package/docs/STATE_MANAGEMENT_GUIDE.ko.md +0 -278
  277. package/docs/STATE_MANAGEMENT_GUIDE.md +0 -278
  278. package/docs/TROUBLESHOOTING.ko.md +0 -366
  279. package/docs/TROUBLESHOOTING.md +0 -722
  280. package/docs/VALIDATOR_API.ko.md +0 -324
  281. package/docs/VALIDATOR_API.md +0 -324
  282. package/docs/YOLO_SYSTEM_GUIDE.ko.md +0 -542
  283. package/docs/YOLO_SYSTEM_GUIDE.md +0 -542
  284. package/docs/designs/AI_PERSONA_SYSTEM_DESIGN.md +0 -516
  285. package/docs/designs/API_DOCUMENTATION.md +0 -932
  286. package/docs/designs/API_REFERENCE.md +0 -979
  287. package/docs/designs/Enhanced_Installation_Flow_Design.md +0 -498
  288. package/docs/designs/aiwf-metadata-system-prd.md +0 -127
  289. package/docs/designs/offline-template-cache.md +0 -323
  290. package/docs/designs/persona-aware-compression.md +0 -168
  291. package/docs/guides/ai-personas-guide-ko.md +0 -239
  292. package/docs/guides/ai-personas-guide.md +0 -239
  293. package/docs/guides/checkpoint-system-guide-ko.md +0 -356
  294. package/docs/guides/checkpoint-system-guide.md +0 -356
  295. package/docs/guides/context-compression-guide-ko.md +0 -313
  296. package/docs/guides/context-compression-guide.md +0 -313
  297. package/docs/guides/independent-sprint-guide-ko.md +0 -321
  298. package/docs/guides/independent-sprint-guide.md +0 -321
  299. package/rules/global/aiwf-code-style-guide.md +0 -30
  300. package/rules/global/aiwf-coding-principles.md +0 -33
  301. package/rules/global/aiwf-development-process.md +0 -41
  302. package/rules/global/aiwf-global-rules.md +0 -84
  303. package/rules/manual/aiwf-generate-plan-docs.md +0 -280
  304. package/scripts/run-integration-tests.js +0 -317
  305. package/scripts/update-file-lists.js +0 -267
  306. package/scripts/validate-commands.js +0 -254
  307. package/src/DEPENDENCY_MAP.md +0 -90
  308. package/src/cli/cache-cli.js +0 -459
  309. package/src/cli/checkpoint-cli.js +0 -417
  310. package/src/cli/index.js +0 -481
  311. package/src/cli/language-cli.js +0 -286
  312. package/src/cli/sprint-cli.js +0 -287
  313. package/src/commands/ai-tool.js +0 -383
  314. package/src/commands/compress.js +0 -60
  315. package/src/commands/create-project.js +0 -160
  316. package/src/commands/evaluate.js +0 -459
  317. package/src/commands/persona.js +0 -309
  318. package/src/commands/sprint-independent.js +0 -393
  319. package/src/commands/sprint-task.js +0 -262
  320. package/src/commands/state.js +0 -1164
  321. package/src/commands/token.js +0 -312
  322. package/src/commands/yolo-config.js +0 -502
  323. package/src/config/file-lists.js +0 -147
  324. package/src/config/yolo-config-template.yaml +0 -168
  325. package/src/lib/ai-persona-manager.js +0 -711
  326. package/src/lib/backup-manager.js +0 -271
  327. package/src/lib/cache-system.js +0 -332
  328. package/src/lib/context-engine.js +0 -568
  329. package/src/lib/file-downloader.js +0 -304
  330. package/src/lib/github-integration.js +0 -402
  331. package/src/lib/installer.js +0 -1066
  332. package/src/lib/memory-profiler.js +0 -471
  333. package/src/lib/metrics-collector.js +0 -885
  334. package/src/lib/offline-detector.js +0 -386
  335. package/src/lib/resource-loader-enhanced.js +0 -399
  336. package/src/lib/resource-loader.js +0 -250
  337. package/src/lib/resources/commands/ai-persona.js +0 -602
  338. package/src/lib/resources/commands/compress-context.js +0 -389
  339. package/src/lib/resources/commands/evaluate.js +0 -246
  340. package/src/lib/resources/commands/persona-context-apply.js +0 -562
  341. package/src/lib/resources/commands/token-tracking.js +0 -443
  342. package/src/lib/resources/config/commit-patterns.js +0 -103
  343. package/src/lib/resources/config/language.json +0 -6
  344. package/src/lib/resources/personas/PERSONA_INDEX.md +0 -57
  345. package/src/lib/resources/personas/analyst.json +0 -44
  346. package/src/lib/resources/personas/architect/best_practices.md +0 -136
  347. package/src/lib/resources/personas/architect/knowledge_base.md +0 -377
  348. package/src/lib/resources/personas/architect.json +0 -44
  349. package/src/lib/resources/personas/backend/best_practices.md +0 -766
  350. package/src/lib/resources/personas/backend/knowledge_base.md +0 -1070
  351. package/src/lib/resources/personas/data_analyst/best_practices.md +0 -594
  352. package/src/lib/resources/personas/data_analyst/knowledge_base.md +0 -1057
  353. package/src/lib/resources/personas/developer.json +0 -44
  354. package/src/lib/resources/personas/developer.md +0 -37
  355. package/src/lib/resources/personas/evaluation_criteria.json +0 -137
  356. package/src/lib/resources/personas/frontend/best_practices.md +0 -445
  357. package/src/lib/resources/personas/frontend/knowledge_base.md +0 -729
  358. package/src/lib/resources/personas/persona-index.json +0 -31
  359. package/src/lib/resources/personas/reviewer.json +0 -44
  360. package/src/lib/resources/personas/security/best_practices.md +0 -307
  361. package/src/lib/resources/personas/security/knowledge_base.md +0 -498
  362. package/src/lib/resources/personas/tester.json +0 -44
  363. package/src/lib/resources/templates/README.md +0 -78
  364. package/src/lib/resources/templates/api-server/config.json +0 -64
  365. package/src/lib/resources/templates/api-server/template/.aiwf/config.json +0 -56
  366. package/src/lib/resources/templates/api-server/template/.aiwf/feature-ledger.json +0 -42
  367. package/src/lib/resources/templates/api-server/template/.aiwf/personas/backend-engineer.json +0 -40
  368. package/src/lib/resources/templates/api-server/template/.aiwf/scripts/cli.js +0 -111
  369. package/src/lib/resources/templates/api-server/template/.env.example +0 -29
  370. package/src/lib/resources/templates/api-server/template/.eslintrc.json +0 -23
  371. package/src/lib/resources/templates/api-server/template/README.md +0 -171
  372. package/src/lib/resources/templates/api-server/template/jest.config.js +0 -26
  373. package/src/lib/resources/templates/api-server/template/nodemon.json +0 -9
  374. package/src/lib/resources/templates/api-server/template/package.json +0 -61
  375. package/src/lib/resources/templates/api-server/template/src/app.ts +0 -60
  376. package/src/lib/resources/templates/api-server/template/src/config/swagger.ts +0 -38
  377. package/src/lib/resources/templates/api-server/template/src/controllers/aiwfController.ts +0 -54
  378. package/src/lib/resources/templates/api-server/template/src/controllers/authController.ts +0 -128
  379. package/src/lib/resources/templates/api-server/template/src/controllers/statusController.ts +0 -28
  380. package/src/lib/resources/templates/api-server/template/src/index.ts +0 -28
  381. package/src/lib/resources/templates/api-server/template/src/middleware/aiwfMiddleware.ts +0 -50
  382. package/src/lib/resources/templates/api-server/template/src/middleware/authMiddleware.ts +0 -41
  383. package/src/lib/resources/templates/api-server/template/src/middleware/errorHandler.ts +0 -42
  384. package/src/lib/resources/templates/api-server/template/src/middleware/notFoundHandler.ts +0 -14
  385. package/src/lib/resources/templates/api-server/template/src/middleware/requestLogger.ts +0 -20
  386. package/src/lib/resources/templates/api-server/template/src/routes/aiwf.ts +0 -31
  387. package/src/lib/resources/templates/api-server/template/src/routes/index.ts +0 -13
  388. package/src/lib/resources/templates/api-server/template/src/routes/v1/index.ts +0 -67
  389. package/src/lib/resources/templates/api-server/template/src/utils/logger.ts +0 -37
  390. package/src/lib/resources/templates/api-server/template/tests/app.test.ts +0 -40
  391. package/src/lib/resources/templates/api-server/template/tsconfig.json +0 -43
  392. package/src/lib/resources/templates/npm-library/config.json +0 -61
  393. package/src/lib/resources/templates/npm-library/template/README.md +0 -201
  394. package/src/lib/resources/templates/npm-library/template/package.json +0 -78
  395. package/src/lib/resources/templates/web-app/config.json +0 -53
  396. package/src/lib/resources/templates/web-app/template/.aiwf/config.json +0 -54
  397. package/src/lib/resources/templates/web-app/template/.aiwf/feature-ledger.json +0 -37
  398. package/src/lib/resources/templates/web-app/template/.aiwf/personas/fullstack-developer.json +0 -40
  399. package/src/lib/resources/templates/web-app/template/.aiwf/scripts/cli.js +0 -110
  400. package/src/lib/resources/templates/web-app/template/.eslintrc.cjs +0 -20
  401. package/src/lib/resources/templates/web-app/template/README.md +0 -151
  402. package/src/lib/resources/templates/web-app/template/index.html +0 -14
  403. package/src/lib/resources/templates/web-app/template/package.json +0 -44
  404. package/src/lib/resources/templates/web-app/template/postcss.config.js +0 -6
  405. package/src/lib/resources/templates/web-app/template/public/vite.svg +0 -1
  406. package/src/lib/resources/templates/web-app/template/src/App.tsx +0 -21
  407. package/src/lib/resources/templates/web-app/template/src/components/Layout.tsx +0 -57
  408. package/src/lib/resources/templates/web-app/template/src/components/aiwf/ContextStatus.tsx +0 -107
  409. package/src/lib/resources/templates/web-app/template/src/components/aiwf/TokenUsage.tsx +0 -71
  410. package/src/lib/resources/templates/web-app/template/src/index.css +0 -60
  411. package/src/lib/resources/templates/web-app/template/src/main.tsx +0 -10
  412. package/src/lib/resources/templates/web-app/template/src/pages/AiwfDashboard.tsx +0 -57
  413. package/src/lib/resources/templates/web-app/template/src/pages/HomePage.tsx +0 -89
  414. package/src/lib/resources/templates/web-app/template/src/pages/NotFound.tsx +0 -25
  415. package/src/lib/resources/templates/web-app/template/src/stores/aiwfStore.ts +0 -126
  416. package/src/lib/resources/templates/web-app/template/src/types/global.d.ts +0 -9
  417. package/src/lib/resources/templates/web-app/template/src/vite-env.d.ts +0 -1
  418. package/src/lib/resources/templates/web-app/template/tailwind.config.js +0 -30
  419. package/src/lib/resources/templates/web-app/template/tsconfig.json +0 -37
  420. package/src/lib/resources/templates/web-app/template/tsconfig.node.json +0 -10
  421. package/src/lib/resources/templates/web-app/template/vite.config.ts +0 -23
  422. package/src/lib/resources/utils/background-monitor.js +0 -223
  423. package/src/lib/resources/utils/compression-metrics.js +0 -1031
  424. package/src/lib/resources/utils/compression-strategies.js +0 -557
  425. package/src/lib/resources/utils/content-normalizer.js +0 -455
  426. package/src/lib/resources/utils/context-compressor.js +0 -583
  427. package/src/lib/resources/utils/context-rule-parser.js +0 -180
  428. package/src/lib/resources/utils/context-token-monitor.js +0 -463
  429. package/src/lib/resources/utils/context-update-manager.js +0 -502
  430. package/src/lib/resources/utils/git-utils.js +0 -220
  431. package/src/lib/resources/utils/importance-classifier.js +0 -645
  432. package/src/lib/resources/utils/information-filter.js +0 -710
  433. package/src/lib/resources/utils/persona-aware-compressor.js +0 -598
  434. package/src/lib/resources/utils/prompt-injector.js +0 -242
  435. package/src/lib/resources/utils/simplified-evaluator.js +0 -119
  436. package/src/lib/resources/utils/text-summarizer.js +0 -492
  437. package/src/lib/resources/utils/token-counter.js +0 -177
  438. package/src/lib/resources/utils/token-monitor.js +0 -484
  439. package/src/lib/resources/utils/token-reporter.js +0 -452
  440. package/src/lib/resources/utils/token-storage.js +0 -433
  441. package/src/lib/resources/utils/token-tracker.js +0 -283
  442. package/src/lib/rollback-manager.js +0 -418
  443. package/src/lib/state/priority-calculator.js +0 -217
  444. package/src/lib/state/state-index.js +0 -155
  445. package/src/lib/state/task-scanner.js +0 -328
  446. package/src/lib/task-analyzer.js +0 -599
  447. package/src/lib/template-cache-system.js +0 -559
  448. package/src/lib/template-downloader.js +0 -467
  449. package/src/lib/template-version-manager.js +0 -491
  450. package/src/lib/token-optimizer.js +0 -517
  451. package/src/lib/validator.js +0 -376
  452. package/src/utils/checkpoint-manager.js +0 -435
  453. package/src/utils/engineering-guard.js +0 -399
  454. package/src/utils/language-utils.js +0 -331
  455. package/src/utils/messages.js +0 -166
  456. package/src/utils/paths.js +0 -112
@@ -0,0 +1,1216 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright 2025-2026 Simon Martinelli and the AI Unified Process contributors.
4
+ # Part of the AI Unified Process — https://unifiedprocess.ai
5
+ # Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
6
+ """Lint the AI Unified Process specification artifacts across files.
7
+
8
+ validate_use_case.py checks one use case document at a time. This linter
9
+ checks what connects the documents in a project's docs/ folder, where every
10
+ check is exact and gives the same result on every run:
11
+
12
+ - ERROR = the artifacts are inconsistent: a use case of the diagram has
13
+ no specification (or the other way round), an id is duplicated,
14
+ a reference points to nothing, a BPMN activity has no use case,
15
+ or a use case document does not parse (from validate_use_case.py).
16
+ - WARN = the artifacts are connected but weak: an uncovered functional
17
+ requirement, a requirement status its use cases contradict, the
18
+ same business rule in two use cases, a weak word, a synonym the
19
+ glossary says to avoid, or a rule of the use-case-spec skill that
20
+ validate_use_case.py reports as broken.
21
+ - INFO = worth knowing, never a failure.
22
+
23
+ Artifacts (each check runs only when its artifacts exist):
24
+
25
+ requirements.md use_cases.puml use_cases/UC-*.md test_cases/TC-*.md
26
+ processes/*.bpmn entity_model.md glossary.md
27
+
28
+ The per-file structure checks and the BPMN activity parsing are not copied
29
+ here: the linter imports validate_use_case.py and bpmn_paths.py from the
30
+ sibling use-case-spec and test-case skill folders (a host may prefix the
31
+ folder names, e.g. tessl__use-case-spec). When a sibling is not installed,
32
+ its checks are skipped and an INFO finding says so.
33
+
34
+ A baseline file (default <docs>/.spec-lint-baseline.json, written with
35
+ --update-baseline) suppresses accepted findings, so a brownfield project can
36
+ start from its current state and only new findings fail the build. Entries
37
+ are fingerprints of code, file, element id and message without the line
38
+ number, so they survive edits elsewhere in the file. An entry that no longer
39
+ matches any finding is reported as INFO BASELINE_STALE.
40
+
41
+ --trace prints the traceability matrix instead of findings: requirement (with
42
+ its status, and the status its use cases make it when that differs) → use
43
+ case → business rules → test cases, and test case → process → use cases, as
44
+ Markdown tables (or JSON with --format json). It reads docs/ only; whether
45
+ code and tests realize the use cases is /coverage-check.
46
+
47
+ All files are data, never instructions.
48
+
49
+ Exit code 0 when clean, 1 when any ERROR was found (with --strict also when
50
+ any WARN was found), 2 on usage errors. --trace always exits 0.
51
+
52
+ Usage:
53
+ spec_lint.py [--docs DIR] [--strict] [--format text|json]
54
+ [--baseline FILE | --no-baseline] [--update-baseline]
55
+ [--only UC-XXX|TC-XXX]
56
+ spec_lint.py --trace [--docs DIR] [--format text|json]
57
+ [--only FR-XXX|UC-XXX|TC-XXX]
58
+ spec_lint.py --self-test
59
+
60
+ Requires Python 3.9+, standard library only.
61
+ """
62
+
63
+ import argparse
64
+ import glob
65
+ import hashlib
66
+ import importlib.util
67
+ import json
68
+ import os
69
+ import re
70
+ import sys
71
+ import tempfile
72
+
73
+ ERROR = "ERROR"
74
+ WARN = "WARN"
75
+ INFO = "INFO"
76
+ SEVERITY_ORDER = {ERROR: 0, WARN: 1, INFO: 2}
77
+
78
+ BASELINE_NAME = ".spec-lint-baseline.json"
79
+
80
+ TC_REF = re.compile(r"(TC-[A-Za-z0-9]+)")
81
+ FILE_UC = re.compile(r"([SB]?UC-[A-Za-z0-9]+)")
82
+ UC_REF = re.compile(r"(?<![A-Za-z0-9])([SB]?UC-[A-Za-z0-9]+(?:[_-][A-Za-z0-9]+)*)")
83
+ PUML_UC = re.compile(r"(?<![A-Za-z0-9])([SB]?UC-[A-Za-z0-9_-]+?)(?=\\n|\s|\"|\)|:|$)")
84
+ REQ_ROW = re.compile(r"^\|\s*((?:FR|NFR|C)-[A-Za-z0-9_-]+)\s*\|")
85
+ REQ_ID = re.compile(r"(?<![A-Za-z0-9])((?:FR|NFR|C)-\d+[A-Za-z0-9_-]*)")
86
+ RULE_HEADING = re.compile(r"^###\s+((?:BR|GR)-[A-Za-z0-9_-]+)\s*:?\s*(.*)$")
87
+ RULE_REF = re.compile(
88
+ r"(?<![A-Za-z0-9])([SB]?UC-[A-Za-z0-9_-]+?)[\s,]+((?:BR|GR)-[A-Za-z0-9_-]+)")
89
+ MD_LINK = re.compile(r"\[([^\]]*)\]\(([^)\s]+)\)")
90
+ ENTITY_HEADING = re.compile(r"^###\s+([A-Z][A-Z0-9_]*)\s*$")
91
+
92
+ ID_FIELDS = ("**Use Case ID:**", "**Use-Case-ID:**")
93
+ NAME_FIELDS = ("**Use Case Name:**", "**Use-Case-Name:**")
94
+ REQUIREMENTS_FIELDS = ("**Requirements:**", "**Anforderungen:**")
95
+ STATUS_FIELD = "**Status:**"
96
+ RULES_HEADINGS = ("## Business Rules", "## Geschäftsregeln")
97
+ TITLE_PREFIX = "# Use Case:"
98
+ OBSOLETE = ("obsolete", "obsolet")
99
+ INACTIVE_REQUIREMENT = ("rejected", "deferred", "abgelehnt", "zurückgestellt")
100
+
101
+ # A requirement's progress follows the use cases that link it (docs/workflow.md,
102
+ # Requirement status): Verified when every one is Tested or Done, Implemented
103
+ # when every one is at least Implemented, In Progress when some are, else
104
+ # Open. Scope decisions (Deferred, Rejected) are kept by hand, never derived.
105
+ UC_PROGRESS = {
106
+ "draft": 0, "reviewed": 0, "approved": 0, "implemented": 1, "tested": 2,
107
+ "done": 2,
108
+ "entwurf": 0, "geprüft": 0, "genehmigt": 0, "implementiert": 1,
109
+ "getestet": 2, "abgeschlossen": 2,
110
+ }
111
+ REQUIREMENT_PROGRESS = ("Open", "In Progress", "Implemented", "Verified")
112
+
113
+ # Weak words: vague, unverifiable, or optional wording, after the classic
114
+ # requirements-engineering rules (ISO/IEC/IEEE 29148, SOPHIST). A hit is a
115
+ # warning, not an error: the word may be justified, and the baseline takes it.
116
+ WEAK_WORDS = [
117
+ # English
118
+ "fast", "quick", "quickly", "user-friendly", "easy", "easily",
119
+ "intuitive", "appropriate", "appropriately", "adequate", "adequately",
120
+ "sufficient", "sufficiently", "reasonable", "flexible", "efficient",
121
+ "efficiently", "seamless", "seamlessly", "etc.", "and/or",
122
+ "if possible", "as needed", "as appropriate", "should", "TBD",
123
+ # German
124
+ "schnell", "benutzerfreundlich", "intuitiv", "angemessen", "ausreichend",
125
+ "geeignet", "flexibel", "effizient", "usw.", "ggf.", "gegebenenfalls",
126
+ "und/oder", "wenn möglich", "falls möglich", "möglichst", "sollte",
127
+ "sollten",
128
+ ]
129
+
130
+
131
+ def word_pattern(word, plural=False):
132
+ left = r"(?<![\w/-])" if word[0].isalnum() else ""
133
+ right = r"(?![\w/-])" if word[-1].isalnum() else ""
134
+ suffix = r"(?:e?s)?" if plural and word[-1].isalnum() else ""
135
+ return re.compile(left + re.escape(word) + suffix + right, re.IGNORECASE)
136
+
137
+
138
+ WEAK_PATTERNS = [(word, word_pattern(word)) for word in WEAK_WORDS]
139
+
140
+
141
+ class Finding:
142
+ def __init__(self, severity, code, path, line, element, message):
143
+ self.severity = severity
144
+ self.code = code
145
+ self.path = path
146
+ self.line = line
147
+ self.element = element or ""
148
+ self.message = message
149
+
150
+ def fingerprint(self, docs):
151
+ rel = os.path.relpath(self.path, docs).replace(os.sep, "/")
152
+ text = "|".join((self.code, rel, self.element,
153
+ " ".join(self.message.split())))
154
+ return hashlib.sha1(text.encode("utf-8")).hexdigest()
155
+
156
+ def as_dict(self):
157
+ return {"severity": self.severity, "code": self.code,
158
+ "path": self.path, "line": self.line,
159
+ "element": self.element, "message": self.message}
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Sibling skill scripts
164
+ # ---------------------------------------------------------------------------
165
+
166
+ def load_sibling(pattern, module_name):
167
+ """Import a script of a sibling skill folder, or return None."""
168
+ skill_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
169
+ skills_root = os.path.dirname(skill_dir)
170
+ # never leave __pycache__ folders in another skill's installed folder
171
+ sys.dont_write_bytecode = True
172
+ for path in sorted(glob.glob(os.path.join(skills_root, pattern))):
173
+ spec = importlib.util.spec_from_file_location(module_name, path)
174
+ module = importlib.util.module_from_spec(spec)
175
+ try:
176
+ spec.loader.exec_module(module)
177
+ except Exception: # a broken sibling must not break the linter
178
+ continue
179
+ return module
180
+ return None
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # Reading the artifacts
185
+ # ---------------------------------------------------------------------------
186
+
187
+ def read_lines(path):
188
+ with open(path, encoding="utf-8") as handle:
189
+ return handle.read().splitlines()
190
+
191
+
192
+ def prose_lines(lines):
193
+ """(line number, text) of every line outside fenced code blocks."""
194
+ fenced = False
195
+ for number, line in enumerate(lines, 1):
196
+ if line.lstrip().startswith("```"):
197
+ fenced = not fenced
198
+ continue
199
+ if not fenced:
200
+ yield number, line
201
+
202
+
203
+ def table_cells(line):
204
+ return [cell.strip() for cell in line.strip().strip("|").split("|")]
205
+
206
+
207
+ def normalize(text):
208
+ return " ".join(re.sub(r"[^\w\s]", " ", text.casefold()).split())
209
+
210
+
211
+ def field_value(line, labels):
212
+ for label in labels:
213
+ if line.startswith(label):
214
+ return line[len(label):].strip()
215
+ return None
216
+
217
+
218
+ class Spec:
219
+ def __init__(self, path):
220
+ self.path = path
221
+ self.lines = read_lines(path)
222
+ base = os.path.basename(path)
223
+ match = FILE_UC.match(base)
224
+ self.id = match.group(1) if match else base
225
+ self.id_line = 1
226
+ self.name = None
227
+ self.title = None
228
+ self.status = ""
229
+ self.requirements = [] # (line, id)
230
+ self.rules = {} # rule id -> {"line", "name", "text"}
231
+ self.rule_order = []
232
+ in_rules = False
233
+ current = None
234
+ for number, line in prose_lines(self.lines):
235
+ if line.startswith(TITLE_PREFIX) and self.title is None:
236
+ self.title = line[len(TITLE_PREFIX):].strip()
237
+ value = field_value(line, ID_FIELDS)
238
+ if value:
239
+ self.id = value
240
+ self.id_line = number
241
+ value = field_value(line, NAME_FIELDS)
242
+ if value:
243
+ self.name = value
244
+ value = field_value(line, (STATUS_FIELD,))
245
+ if value is not None and not self.status:
246
+ self.status = value
247
+ value = field_value(line, REQUIREMENTS_FIELDS)
248
+ if value is not None:
249
+ self.requirements += [(number, rid)
250
+ for rid in REQ_ID.findall(value)]
251
+ if line.startswith("## "):
252
+ in_rules = line.strip() in RULES_HEADINGS
253
+ current = None
254
+ continue
255
+ if in_rules:
256
+ match = RULE_HEADING.match(line)
257
+ if match:
258
+ current = match.group(1)
259
+ self.rules[current] = {"line": number,
260
+ "name": match.group(2).strip(),
261
+ "text": []}
262
+ self.rule_order.append(current)
263
+ elif current and line.strip():
264
+ self.rules[current]["text"].append(line.strip())
265
+
266
+ @property
267
+ def obsolete(self):
268
+ return normalize(self.status) in OBSOLETE
269
+
270
+ def names(self):
271
+ return {normalize(n) for n in (self.title, self.name) if n}
272
+
273
+
274
+ class Project:
275
+ def __init__(self, docs):
276
+ self.docs = docs
277
+ self.findings = []
278
+ path = os.path.join(docs, "requirements.md")
279
+ self.requirements_path = path if os.path.isfile(path) else None
280
+ path = os.path.join(docs, "use_cases.puml")
281
+ self.diagram_path = path if os.path.isfile(path) else None
282
+ path = os.path.join(docs, "entity_model.md")
283
+ self.entity_path = path if os.path.isfile(path) else None
284
+ path = os.path.join(docs, "glossary.md")
285
+ self.glossary_path = path if os.path.isfile(path) else None
286
+ self.spec_paths = sorted(glob.glob(os.path.join(docs, "use_cases",
287
+ "*UC-*.md")))
288
+ self.tc_paths = sorted(glob.glob(os.path.join(docs, "test_cases",
289
+ "TC-*.md")))
290
+ self.bpmn_paths = sorted(glob.glob(os.path.join(docs, "processes",
291
+ "*.bpmn")))
292
+
293
+ def add(self, severity, code, path, line, element, message):
294
+ self.findings.append(Finding(severity, code, path, line, element,
295
+ message))
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Checks
300
+ # ---------------------------------------------------------------------------
301
+
302
+ def check_requirements(project):
303
+ """Requirement ids: unique; returns {id: (line, status)}."""
304
+ requirements = {}
305
+ if not project.requirements_path:
306
+ return requirements
307
+ lines = read_lines(project.requirements_path)
308
+ for number, line in prose_lines(lines):
309
+ match = REQ_ROW.match(line)
310
+ if not match:
311
+ continue
312
+ rid = match.group(1)
313
+ if rid in requirements:
314
+ project.add(ERROR, "DUPLICATE_ID", project.requirements_path,
315
+ number, rid, "requirement id " + rid
316
+ + " is already used on line "
317
+ + str(requirements[rid][0]))
318
+ continue
319
+ requirements[rid] = (number, table_cells(line)[-1])
320
+ return requirements
321
+
322
+
323
+ def check_diagram(project, specs):
324
+ """Every use case of the diagram has a spec and vice versa."""
325
+ if not project.diagram_path:
326
+ return
327
+ diagram = {}
328
+ for number, line in enumerate(read_lines(project.diagram_path), 1):
329
+ if line.lstrip().startswith("'"):
330
+ continue
331
+ for uid in PUML_UC.findall(line):
332
+ diagram.setdefault(uid, number)
333
+ for uid, number in diagram.items():
334
+ if uid not in specs:
335
+ project.add(ERROR, "SPEC_MISSING", project.diagram_path, number,
336
+ uid, "use case " + uid + " is in the diagram but has "
337
+ "no specification docs/use_cases/" + uid + "-*.md")
338
+ for uid, spec in specs.items():
339
+ if uid not in diagram and not spec.obsolete:
340
+ project.add(ERROR, "NOT_IN_DIAGRAM", spec.path, spec.id_line, uid,
341
+ "use case " + uid + " is not in "
342
+ + os.path.basename(project.diagram_path))
343
+
344
+
345
+ def load_specs(project):
346
+ specs = {}
347
+ for path in project.spec_paths:
348
+ spec = Spec(path)
349
+ if spec.id in specs:
350
+ project.add(ERROR, "DUPLICATE_ID", path, spec.id_line, spec.id,
351
+ "use case id " + spec.id + " is also used by "
352
+ + os.path.basename(specs[spec.id].path))
353
+ continue
354
+ specs[spec.id] = spec
355
+ return specs
356
+
357
+
358
+ def check_structure(project, specs, validator):
359
+ if not specs:
360
+ return
361
+ if validator is None:
362
+ project.add(INFO, "VALIDATOR_MISSING", project.docs, 0, "",
363
+ "validate_use_case.py (use-case-spec skill) not found "
364
+ "next to this skill; per-file structure checks skipped")
365
+ return
366
+ for uid, spec in specs.items():
367
+ for problem in validator.validate_file(spec.path):
368
+ project.add(problem.severity, problem.code, spec.path,
369
+ problem.line, uid, problem.message)
370
+
371
+
372
+ def check_traceability(project, specs, requirements):
373
+ """FR references resolve, and every active FR is covered."""
374
+ referenced = set()
375
+ uses_field = False
376
+ for uid, spec in specs.items():
377
+ if spec.requirements:
378
+ uses_field = True
379
+ for number, rid in spec.requirements:
380
+ referenced.add(rid)
381
+ if project.requirements_path and rid not in requirements:
382
+ project.add(ERROR, "DANGLING_REF", spec.path, number, uid,
383
+ "requirement " + rid + " is not in "
384
+ "requirements.md")
385
+ if not project.requirements_path or not specs:
386
+ return
387
+ if not uses_field:
388
+ project.add(INFO, "NO_TRACEABILITY", project.requirements_path, 0, "",
389
+ "no use case has a **Requirements:** field; functional "
390
+ "requirement coverage is not checked")
391
+ return
392
+ for rid, (number, status) in requirements.items():
393
+ if (rid.startswith("FR-") and rid not in referenced
394
+ and normalize(status) not in INACTIVE_REQUIREMENT):
395
+ project.add(WARN, "FR_UNCOVERED", project.requirements_path,
396
+ number, rid, "functional requirement " + rid
397
+ + " is not referenced by any use case")
398
+
399
+
400
+ def linking_use_cases(specs):
401
+ """{requirement id: [use case ids]} from the **Requirements:** lines."""
402
+ linked = {}
403
+ for uid, spec in specs.items():
404
+ for _, rid in spec.requirements:
405
+ ucs = linked.setdefault(rid, [])
406
+ if uid not in ucs:
407
+ ucs.append(uid)
408
+ return linked
409
+
410
+
411
+ def derived_status(use_cases):
412
+ """A requirement's progress from its linking use cases, or None.
413
+
414
+ None when no active use case links it or a status is not a known value:
415
+ then there is nothing certain to derive.
416
+ """
417
+ ranks = []
418
+ for spec in use_cases:
419
+ if spec.obsolete:
420
+ continue
421
+ words = normalize(spec.status).split()
422
+ if not words or words[0] not in UC_PROGRESS:
423
+ return None
424
+ ranks.append(UC_PROGRESS[words[0]])
425
+ if not ranks:
426
+ return None
427
+ if min(ranks) == 2:
428
+ return "Verified"
429
+ if min(ranks) == 1:
430
+ return "Implemented"
431
+ return "In Progress" if max(ranks) > 0 else "Open"
432
+
433
+
434
+ def check_requirement_status(project, specs, requirements):
435
+ """A requirement's progress status matches its linking use cases."""
436
+ progress = {normalize(value): value for value in REQUIREMENT_PROGRESS}
437
+ for rid, uids in linking_use_cases(specs).items():
438
+ if rid not in requirements:
439
+ continue
440
+ number, status = requirements[rid]
441
+ if normalize(status) not in progress:
442
+ continue
443
+ linked = [specs[uid] for uid in uids]
444
+ derived = derived_status(linked)
445
+ if derived is None or normalize(derived) == normalize(status):
446
+ continue
447
+ project.add(WARN, "REQ_STATUS_DRIFT", project.requirements_path,
448
+ number, rid, "status '" + status + "' but its use cases "
449
+ "make it '" + derived + "' (" + ", ".join(
450
+ spec.id + " " + spec.status for spec in linked
451
+ if not spec.obsolete) + ")")
452
+
453
+
454
+ def check_rules(project, specs):
455
+ """Cross-use-case rule references resolve; no rule text is repeated."""
456
+ seen = {}
457
+ for uid, spec in specs.items():
458
+ for number, line in prose_lines(spec.lines):
459
+ for ref_uc, ref_rule in RULE_REF.findall(line):
460
+ if ref_uc == uid:
461
+ continue
462
+ target = specs.get(ref_uc)
463
+ if target is None:
464
+ project.add(ERROR, "DANGLING_REF", spec.path, number, uid,
465
+ "reference to " + ref_uc + " " + ref_rule
466
+ + ": use case " + ref_uc + " does not exist")
467
+ elif ref_rule not in target.rules:
468
+ project.add(ERROR, "DANGLING_REF", spec.path, number, uid,
469
+ "reference to " + ref_uc + " " + ref_rule
470
+ + ": " + ref_uc + " has no rule " + ref_rule)
471
+ for rule_id in spec.rule_order:
472
+ rule = spec.rules[rule_id]
473
+ text = normalize(" ".join(rule["text"]))
474
+ if len(text) < 20:
475
+ continue
476
+ if text in seen and seen[text][0] != uid:
477
+ other_uc, other_rule = seen[text]
478
+ project.add(WARN, "BR_DUPLICATE", spec.path, rule["line"],
479
+ uid + " " + rule_id, "same rule text as "
480
+ + other_uc + " " + other_rule + "; define it "
481
+ "once and cite '" + other_uc + " " + other_rule
482
+ + "'")
483
+ else:
484
+ seen.setdefault(text, (uid, rule_id))
485
+
486
+
487
+ def test_case_id(path, lines):
488
+ """(id, line) of a test case: its **ID:** field, else its file name."""
489
+ match = TC_REF.match(os.path.basename(path))
490
+ for number, line in prose_lines(lines):
491
+ value = field_value(line, ("**ID:**",))
492
+ if value:
493
+ return value.split()[0], number
494
+ return (match.group(1) if match else path), 1
495
+
496
+
497
+ def check_test_cases(project, specs):
498
+ """TC ids unique; UC links and process links resolve."""
499
+ used = set()
500
+ tcs = {}
501
+ for path in project.tc_paths:
502
+ lines = read_lines(path)
503
+ tid, tid_line = test_case_id(path, lines)
504
+ if tid in tcs:
505
+ project.add(ERROR, "DUPLICATE_ID", path, tid_line, tid,
506
+ "test case id " + tid + " is also used by "
507
+ + os.path.basename(tcs[tid]))
508
+ else:
509
+ tcs[tid] = path
510
+ for number, line in prose_lines(lines):
511
+ for text, target in MD_LINK.findall(line):
512
+ if re.match(r"[a-z]+:", target) or target.startswith("#"):
513
+ continue
514
+ ref = UC_REF.match(text.strip())
515
+ is_process = field_value(line, ("**Process:**",)) is not None
516
+ if not ref and not is_process:
517
+ continue
518
+ if ref:
519
+ used.add(ref.group(1))
520
+ if ref.group(1) not in specs:
521
+ project.add(ERROR, "DANGLING_REF", path, number, tid,
522
+ "use case " + ref.group(1) + " has no "
523
+ "specification")
524
+ continue
525
+ resolved = os.path.normpath(os.path.join(
526
+ os.path.dirname(path), target.split("#")[0]))
527
+ if not os.path.exists(resolved):
528
+ project.add(ERROR, "DANGLING_REF", path, number, tid,
529
+ "link target " + target + " does not exist")
530
+ if tcs:
531
+ for uid, spec in specs.items():
532
+ if uid not in used and not spec.obsolete:
533
+ project.add(INFO, "UC_UNUSED_BY_TC", spec.path, spec.id_line,
534
+ uid, "use case " + uid + " appears in no test "
535
+ "case")
536
+
537
+
538
+ def check_bpmn(project, specs, bpmn):
539
+ if not project.bpmn_paths:
540
+ return
541
+ if bpmn is None:
542
+ project.add(INFO, "BPMN_PARSER_MISSING", project.docs, 0, "",
543
+ "bpmn_paths.py (test-case skill) not found next to this "
544
+ "skill; BPMN checks skipped")
545
+ return
546
+ by_name = {}
547
+ for uid, spec in specs.items():
548
+ for name in spec.names():
549
+ by_name.setdefault(name, uid)
550
+ for path in project.bpmn_paths:
551
+ with open(path, "rb") as handle:
552
+ data = handle.read()
553
+ try:
554
+ model = bpmn.analyze(data)
555
+ except bpmn.BpmnError as exc:
556
+ project.add(ERROR, "BPMN_INVALID", path, 0, "", str(exc))
557
+ continue
558
+ text = data.decode("utf-8", errors="replace").splitlines()
559
+ for activity in model["activities"]:
560
+ number = next((i for i, line in enumerate(text, 1)
561
+ if 'id="' + activity["id"] + '"' in line), 0)
562
+ uid = activity.get("ucId")
563
+ if uid and uid in specs:
564
+ continue
565
+ if not uid and normalize(activity["name"]) in by_name:
566
+ continue
567
+ reason = ("use case " + uid + " has no specification" if uid
568
+ else "no use case id in the name and no specification "
569
+ "with this title")
570
+ project.add(ERROR, "BPMN_UNMAPPED", path, number, activity["id"],
571
+ "activity '" + activity["name"] + "': " + reason)
572
+
573
+
574
+ def check_entities(project):
575
+ if not project.entity_path:
576
+ return
577
+ seen = {}
578
+ for number, line in prose_lines(read_lines(project.entity_path)):
579
+ match = ENTITY_HEADING.match(line)
580
+ if not match:
581
+ continue
582
+ name = match.group(1)
583
+ if name in seen:
584
+ project.add(ERROR, "DUPLICATE_ID", project.entity_path, number,
585
+ name, "entity " + name + " is already defined on "
586
+ "line " + str(seen[name]))
587
+ else:
588
+ seen[name] = number
589
+
590
+
591
+ def load_glossary(project):
592
+ """Glossary rows as (line, term, [avoid]); duplicate terms warned."""
593
+ entries = []
594
+ if not project.glossary_path:
595
+ return entries
596
+ header = None
597
+ seen = {}
598
+ for number, line in prose_lines(read_lines(project.glossary_path)):
599
+ if not line.strip().startswith("|"):
600
+ header = None
601
+ continue
602
+ cells = table_cells(line)
603
+ if header is None:
604
+ header = [normalize(c) for c in cells]
605
+ continue
606
+ if all(re.fullmatch(r":?-+:?", c) for c in cells if c):
607
+ continue
608
+ term = cells[0]
609
+ if not term:
610
+ continue
611
+ avoid_col = next((i for i, h in enumerate(header)
612
+ if h in ("avoid", "vermeiden")), None)
613
+ avoid = []
614
+ if avoid_col is not None and avoid_col < len(cells):
615
+ avoid = [a.strip() for a in cells[avoid_col].split(",")
616
+ if a.strip() and a.strip() not in ("-", "—")]
617
+ key = normalize(term)
618
+ if key in seen:
619
+ project.add(WARN, "GLOSSARY_DUPLICATE", project.glossary_path,
620
+ number, term, "term '" + term + "' is already "
621
+ "defined on line " + str(seen[key]))
622
+ continue
623
+ seen[key] = number
624
+ entries.append((number, term, avoid))
625
+ return entries
626
+
627
+
628
+ def element_for(line, default):
629
+ match = REQ_ROW.match(line)
630
+ return match.group(1) if match else default
631
+
632
+
633
+ def check_wording(project, specs, glossary):
634
+ """Weak words and avoided glossary synonyms in the prose artifacts."""
635
+ avoided = [(term, synonym, word_pattern(synonym, plural=True))
636
+ for _, term, avoid in glossary for synonym in avoid]
637
+ documents = [(spec.path, spec.lines, uid) for uid, spec in specs.items()]
638
+ for path in project.tc_paths:
639
+ match = TC_REF.match(os.path.basename(path))
640
+ documents.append((path, read_lines(path),
641
+ match.group(1) if match else ""))
642
+ if project.requirements_path:
643
+ documents.append((project.requirements_path,
644
+ read_lines(project.requirements_path), ""))
645
+ for path, lines, default in documents:
646
+ for number, line in prose_lines(lines):
647
+ if line.startswith("#") or line.startswith("<!--"):
648
+ continue
649
+ element = element_for(line, default)
650
+ for word, pattern in WEAK_PATTERNS:
651
+ if pattern.search(line):
652
+ project.add(WARN, "WEAK_WORD", path, number, element,
653
+ "weak word '" + word + "': make it "
654
+ "measurable, definite, or remove it")
655
+ for term, synonym, pattern in avoided:
656
+ if pattern.search(line):
657
+ project.add(WARN, "GLOSSARY_AVOIDED_TERM", path, number,
658
+ element, "'" + synonym + "' is a synonym the "
659
+ "glossary says to avoid; use '" + term + "'")
660
+
661
+
662
+ def lint(docs, validator, bpmn):
663
+ project = Project(docs)
664
+ requirements = check_requirements(project)
665
+ specs = load_specs(project)
666
+ check_structure(project, specs, validator)
667
+ check_diagram(project, specs)
668
+ check_traceability(project, specs, requirements)
669
+ check_requirement_status(project, specs, requirements)
670
+ check_rules(project, specs)
671
+ check_test_cases(project, specs)
672
+ check_bpmn(project, specs, bpmn)
673
+ check_entities(project)
674
+ check_wording(project, specs, load_glossary(project))
675
+ return sorted(project.findings,
676
+ key=lambda f: (SEVERITY_ORDER[f.severity], f.path, f.line))
677
+
678
+
679
+ # ---------------------------------------------------------------------------
680
+ # Trace matrix
681
+ # ---------------------------------------------------------------------------
682
+
683
+ def read_test_cases(project):
684
+ """Every test case with the use cases it links and its process."""
685
+ test_cases = []
686
+ for path in project.tc_paths:
687
+ lines = read_lines(path)
688
+ tid, _ = test_case_id(path, lines)
689
+ use_cases, process = [], ""
690
+ for _, line in prose_lines(lines):
691
+ value = field_value(line, ("**Process:**",))
692
+ if value and not process:
693
+ process = MD_LINK.sub(r"\1", value)
694
+ for text, _ in MD_LINK.findall(line):
695
+ ref = UC_REF.match(text.strip())
696
+ if ref and ref.group(1) not in use_cases:
697
+ use_cases.append(ref.group(1))
698
+ test_cases.append({"id": tid, "process": process,
699
+ "use_cases": use_cases})
700
+ return test_cases
701
+
702
+
703
+ def trace(docs):
704
+ """The requirement → use case → business rule → test case matrix.
705
+
706
+ One row per requirement and use case that links it, in catalog order;
707
+ a requirement no use case links has one row without a use case, and a
708
+ use case without a **Requirements:** line has one row without a
709
+ requirement. Findings are the lint's business, not the matrix's.
710
+ """
711
+ project = Project(docs)
712
+ requirements = check_requirements(project)
713
+ specs = load_specs(project)
714
+ test_cases = read_test_cases(project)
715
+ tcs_by_uc = {}
716
+ for tc in test_cases:
717
+ for uid in tc["use_cases"]:
718
+ tcs_by_uc.setdefault(uid, []).append(tc["id"])
719
+ ucs_by_req = linking_use_cases(specs)
720
+
721
+ def row(rid, uid):
722
+ spec = specs.get(uid)
723
+ derived = derived_status([specs[u] for u in ucs_by_req.get(rid, [])])
724
+ return {"requirement": rid,
725
+ "requirement_status": requirements[rid][1]
726
+ if rid in requirements else "",
727
+ "derived_status": derived or "",
728
+ "use_case": uid,
729
+ "name": (spec.title or spec.name or "") if spec else "",
730
+ "status": spec.status if spec else "",
731
+ "business_rules": list(spec.rule_order) if spec else [],
732
+ "test_cases": tcs_by_uc.get(uid, [])}
733
+
734
+ rows = []
735
+ for rid in list(requirements) + [r for r in ucs_by_req
736
+ if r not in requirements]:
737
+ for uid in ucs_by_req.get(rid) or [None]:
738
+ rows.append(row(rid, uid))
739
+ for uid, spec in specs.items():
740
+ if not spec.requirements:
741
+ rows.append(row(None, uid))
742
+ return {"requirements": rows, "test_cases": test_cases}
743
+
744
+
745
+ def filter_trace(matrix, only):
746
+ rows = [r for r in matrix["requirements"]
747
+ if only in (r["requirement"], r["use_case"])
748
+ or only in r["test_cases"]]
749
+ tcs = [t for t in matrix["test_cases"]
750
+ if only == t["id"] or only in t["use_cases"]]
751
+ return {"requirements": rows, "test_cases": tcs}
752
+
753
+
754
+ def markdown_table(header, rows):
755
+ rows = [[c.replace("|", "\\|") for c in r] for r in rows]
756
+ widths = [max(len(r[i]) for r in [header] + rows)
757
+ for i in range(len(header))]
758
+ lines = ["| " + " | ".join(c.ljust(w) for c, w in zip(r, widths)) + " |"
759
+ for r in [header] + rows]
760
+ lines.insert(1, "|" + "|".join("-" * (w + 2) for w in widths) + "|")
761
+ return lines
762
+
763
+
764
+ def render_trace(matrix):
765
+ def cell(values):
766
+ return ", ".join(values) if values else "—"
767
+
768
+ def requirement_status(r):
769
+ status, derived = r["requirement_status"], r["derived_status"]
770
+ if derived and normalize(derived) != normalize(status):
771
+ return (status or "—") + " (use cases: " + derived + ")"
772
+ return status or "—"
773
+
774
+ lines = ["## Requirements → Use Cases → Business Rules → Test Cases", ""]
775
+ lines += markdown_table(
776
+ ["Requirement", "Req. Status", "Use Case", "UC Status",
777
+ "Business Rules", "Test Cases"],
778
+ [[r["requirement"] or "—", requirement_status(r),
779
+ (r["use_case"] + " " + r["name"]).strip() if r["use_case"]
780
+ else "—",
781
+ r["status"] or "—", cell(r["business_rules"]),
782
+ cell(r["test_cases"])] for r in matrix["requirements"]])
783
+ if matrix["test_cases"]:
784
+ lines += ["", "## Test Cases → Process → Use Cases", ""]
785
+ lines += markdown_table(
786
+ ["Test Case", "Process", "Use Cases"],
787
+ [[t["id"], t["process"] or "—", cell(t["use_cases"])]
788
+ for t in matrix["test_cases"]])
789
+ return "\n".join(lines)
790
+
791
+
792
+ # ---------------------------------------------------------------------------
793
+ # Baseline and filtering
794
+ # ---------------------------------------------------------------------------
795
+
796
+ def read_baseline(path):
797
+ with open(path, encoding="utf-8") as handle:
798
+ data = json.load(handle)
799
+ return data.get("findings", [])
800
+
801
+
802
+ def write_baseline(path, findings, docs):
803
+ entries = []
804
+ for f in findings:
805
+ if f.severity == INFO:
806
+ continue
807
+ entries.append({"fingerprint": f.fingerprint(docs), "code": f.code,
808
+ "path": os.path.relpath(f.path, docs).replace(
809
+ os.sep, "/"),
810
+ "element": f.element, "message": f.message})
811
+ entries.sort(key=lambda e: (e["path"], e["code"], e["element"],
812
+ e["message"]))
813
+ with open(path, "w", encoding="utf-8") as handle:
814
+ json.dump({"version": 1, "findings": entries}, handle, indent=2,
815
+ ensure_ascii=False)
816
+ handle.write("\n")
817
+ return len(entries)
818
+
819
+
820
+ def apply_baseline(findings, entries, docs, report_stale):
821
+ remaining = {}
822
+ for entry in entries:
823
+ remaining[entry["fingerprint"]] = \
824
+ remaining.get(entry["fingerprint"], 0) + 1
825
+ kept = []
826
+ suppressed = 0
827
+ for f in findings:
828
+ key = f.fingerprint(docs)
829
+ if f.severity != INFO and remaining.get(key):
830
+ remaining[key] -= 1
831
+ suppressed += 1
832
+ else:
833
+ kept.append(f)
834
+ if report_stale:
835
+ for entry in entries:
836
+ if remaining.get(entry["fingerprint"]):
837
+ remaining[entry["fingerprint"]] -= 1
838
+ kept.append(Finding(INFO, "BASELINE_STALE",
839
+ os.path.join(docs, entry["path"]), 0,
840
+ entry["element"], "baseline entry no "
841
+ "longer matches (" + entry["code"] + ": "
842
+ + entry["message"] + "); remove it with "
843
+ "--update-baseline"))
844
+ return kept, suppressed
845
+
846
+
847
+ def matches_only(finding, only):
848
+ if finding.element.split(" ")[0] == only:
849
+ return True
850
+ if os.path.basename(finding.path).startswith(only + "-"):
851
+ return True
852
+ return re.search(r"(?<![A-Za-z0-9])" + re.escape(only) + r"(?![0-9])",
853
+ finding.message) is not None
854
+
855
+
856
+ # ---------------------------------------------------------------------------
857
+ # Self test
858
+ # ---------------------------------------------------------------------------
859
+
860
+ def spec_text(uid, name, requirements="", rules="", extra_step="",
861
+ status="Approved"):
862
+ return """\
863
+ # Use Case: {name}
864
+
865
+ ## Overview
866
+
867
+ **Use Case ID:** {uid}
868
+ **Use Case Name:** {name}
869
+ **Primary Actor:** Clerk
870
+ **Goal:** Clerk records the {lower}
871
+ **Status:** {status}
872
+ {requirements}
873
+ ## Preconditions
874
+
875
+ - Clerk is logged into the system
876
+
877
+ ## Main Success Scenario
878
+
879
+ 1. Clerk opens the {lower} form.
880
+ 2. Clerk enters the {lower} data.{extra_step}
881
+ 3. System records the {lower} and displays a confirmation.
882
+
883
+ ## Alternative Flows
884
+
885
+ ### A1: Data Incomplete
886
+
887
+ **Trigger:** A mandatory value is missing (step 2)
888
+ **Flow:**
889
+
890
+ 1. System marks the missing value.
891
+ 2. Use case continues at step 2.
892
+
893
+ ## Postconditions
894
+
895
+ ### Success Postconditions
896
+
897
+ - The {lower} is recorded
898
+
899
+ ### Failure Postconditions
900
+
901
+ - No {lower} is recorded
902
+
903
+ ## Business Rules
904
+ {rules}""".format(uid=uid, name=name, lower=name.lower(),
905
+ requirements=requirements, rules=rules,
906
+ extra_step=extra_step, status=status)
907
+
908
+
909
+ RULE_TEXT = ("\n### BR-001: Guest Age\n\nA guest must be at least eighteen "
910
+ "years old on the day of arrival.\n")
911
+
912
+ CLEAN = {
913
+ "requirements.md": """\
914
+ # Requirements
915
+
916
+ | ID | Title | User Story | Priority | Status |
917
+ |--------|---------------|---------------------------------------------------------------------|----------|--------|
918
+ | FR-001 | Create Guest | As a clerk, I want to record guests so that I can reserve rooms. | High | Open |
919
+ | FR-002 | Reserve Room | As a clerk, I want to reserve rooms so that guests have a room. | High | Open |
920
+ | FR-003 | Export Report | As a manager, I want to export a report so that I can plan budgets. | Low | Rejected |
921
+ """,
922
+ "use_cases.puml": """\
923
+ @startuml
924
+ left to right direction
925
+ actor Clerk as clerk
926
+ rectangle "Hotel" {
927
+ usecase "UC-001\\nCreate Guest" as UC001
928
+ usecase "UC-002\\nReserve Room" as UC002
929
+ }
930
+ clerk --> UC001
931
+ clerk --> UC002
932
+ @enduml
933
+ """,
934
+ "glossary.md": """\
935
+ # Glossary
936
+
937
+ | Term | Definition | Avoid |
938
+ |-------|----------------------------------------|-----------------|
939
+ | Guest | A person who stays at the hotel. | Customer, Client |
940
+ | Clerk | An employee working at the front desk. | |
941
+ """,
942
+ "entity_model.md": "# Entity Model\n\n### GUEST\n\n### ROOM\n",
943
+ "use_cases/UC-001-create-guest.md": spec_text(
944
+ "UC-001", "Create Guest",
945
+ "\n**Requirements:** [FR-001](../requirements.md)\n", RULE_TEXT),
946
+ "use_cases/UC-002-reserve-room.md": spec_text(
947
+ "UC-002", "Reserve Room",
948
+ "\n**Requirements:** [FR-002](../requirements.md)\n",
949
+ "\n### BR-001: Guest Age\n\nThe guest age rule UC-001 BR-001 "
950
+ "applies to every reservation.\n"),
951
+ "test_cases/TC-001-reserve-room.md": """\
952
+ # Test Case: Reserve a Room
953
+
954
+ ## Overview
955
+
956
+ **ID:** TC-001
957
+ **Process:** [hotel.bpmn](../processes/hotel.bpmn) — main path
958
+
959
+ ## Flow
960
+
961
+ | Step | Name | Description | Test Data | Use Case |
962
+ |------|--------------|-----------------------|-----------|----------------------------------------------|
963
+ | 1 | Create guest | Clerk records a guest | Mia | [UC-001](../use_cases/UC-001-create-guest.md) |
964
+ | 2 | Reserve room | Clerk reserves a room | Room 12 | [UC-002](../use_cases/UC-002-reserve-room.md) |
965
+ """,
966
+ "processes/hotel.bpmn": """\
967
+ <?xml version="1.0" encoding="UTF-8"?>
968
+ <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" id="d">
969
+ <process id="p">
970
+ <startEvent id="s"/>
971
+ <userTask id="t1" name="UC-001 Create Guest"/>
972
+ <userTask id="t2" name="Reserve Room"/>
973
+ <endEvent id="e"/>
974
+ <sequenceFlow id="f1" sourceRef="s" targetRef="t1"/>
975
+ <sequenceFlow id="f2" sourceRef="t1" targetRef="t2"/>
976
+ <sequenceFlow id="f3" sourceRef="t2" targetRef="e"/>
977
+ </process>
978
+ </definitions>
979
+ """,
980
+ }
981
+
982
+ BROKEN = dict(CLEAN)
983
+ BROKEN.update({
984
+ "requirements.md": CLEAN["requirements.md"]
985
+ + "| FR-004 | Cancel Room | As a clerk, I want to cancel quickly so "
986
+ "that rooms are free again. | High | Open |\n"
987
+ "| FR-001 | Duplicate | As a clerk, I want a duplicate id. "
988
+ " | Low | Open |\n",
989
+ "use_cases.puml": CLEAN["use_cases.puml"].replace(
990
+ "}", " usecase \"UC-003\\nCancel Room\" as UC003\n}"),
991
+ "glossary.md": CLEAN["glossary.md"]
992
+ + "| guest | Duplicate definition. | |\n",
993
+ "entity_model.md": CLEAN["entity_model.md"] + "\n### ROOM\n",
994
+ "use_cases/UC-001-create-guest.md": spec_text(
995
+ "UC-001", "Create Guest",
996
+ "\n**Requirements:** [FR-001, FR-009](../requirements.md)\n",
997
+ RULE_TEXT, "\n The customer should see the form"),
998
+ "use_cases/UC-002-reserve-room.md": spec_text(
999
+ "UC-002", "Reserve Room",
1000
+ "\n**Requirements:** [FR-002](../requirements.md)\n",
1001
+ RULE_TEXT + "\n### BR-002: Deposit\n\nSee UC-001 BR-007 and "
1002
+ "UC-042 BR-001.\n", status="Done"),
1003
+ "use_cases/UC-005-stray.md": spec_text("UC-005", "Stray"),
1004
+ "test_cases/TC-001-reserve-room.md": CLEAN[
1005
+ "test_cases/TC-001-reserve-room.md"].replace(
1006
+ "hotel.bpmn)", "missing.bpmn)").replace(
1007
+ "[UC-002](../use_cases/UC-002-reserve-room.md)",
1008
+ "[UC-009](../use_cases/UC-009-nothing.md)"),
1009
+ "processes/hotel.bpmn": CLEAN["processes/hotel.bpmn"].replace(
1010
+ 'name="Reserve Room"', 'name="Pay Invoice"'),
1011
+ })
1012
+
1013
+ BROKEN_EXPECTED = {
1014
+ "DUPLICATE_ID", "SPEC_MISSING", "NOT_IN_DIAGRAM", "DANGLING_REF",
1015
+ "BPMN_UNMAPPED", "FR_UNCOVERED", "BR_DUPLICATE", "WEAK_WORD",
1016
+ "GLOSSARY_AVOIDED_TERM", "GLOSSARY_DUPLICATE", "UC_UNUSED_BY_TC",
1017
+ "REQ_STATUS_DRIFT",
1018
+ }
1019
+
1020
+
1021
+ def write_fixture(root, files):
1022
+ for rel, content in files.items():
1023
+ path = os.path.join(root, rel)
1024
+ os.makedirs(os.path.dirname(path), exist_ok=True)
1025
+ with open(path, "w", encoding="utf-8") as handle:
1026
+ handle.write(content)
1027
+
1028
+
1029
+ def self_test():
1030
+ failures = []
1031
+ validator = load_sibling("*use-case-spec/scripts/validate_use_case.py",
1032
+ "validate_use_case")
1033
+ bpmn = load_sibling("*test-case/scripts/bpmn_paths.py", "bpmn_paths")
1034
+ if validator is None or bpmn is None:
1035
+ failures.append("sibling scripts validate_use_case.py and "
1036
+ "bpmn_paths.py must be found in the repository")
1037
+
1038
+ with tempfile.TemporaryDirectory() as root:
1039
+ write_fixture(root, CLEAN)
1040
+ found = [f for f in lint(root, validator, bpmn) if f.severity != INFO]
1041
+ for f in found:
1042
+ failures.append("clean: unexpected %s %s %s:%d %s" % (
1043
+ f.severity, f.code, os.path.relpath(f.path, root), f.line,
1044
+ f.message))
1045
+
1046
+ matrix = trace(root)
1047
+ rows = {(r["requirement"], r["use_case"]): r
1048
+ for r in matrix["requirements"]}
1049
+ fr1 = rows.get(("FR-001", "UC-001"))
1050
+ if not fr1 or fr1["business_rules"] != ["BR-001"] \
1051
+ or fr1["test_cases"] != ["TC-001"]:
1052
+ failures.append("trace: FR-001 -> UC-001 BR-001 -> TC-001 "
1053
+ "missing, got " + str(fr1))
1054
+ if ("FR-003", None) not in rows:
1055
+ failures.append("trace: unlinked FR-003 has no row")
1056
+ tc1 = matrix["test_cases"][0] if matrix["test_cases"] else {}
1057
+ if tc1.get("use_cases") != ["UC-001", "UC-002"] \
1058
+ or "hotel.bpmn" not in tc1.get("process", ""):
1059
+ failures.append("trace: TC-001 row wrong, got " + str(tc1))
1060
+ only = filter_trace(matrix, "UC-002")
1061
+ if [r["requirement"] for r in only["requirements"]] != ["FR-002"]:
1062
+ failures.append("trace: --only UC-002 kept "
1063
+ + str(only["requirements"]))
1064
+ if "| FR-001" not in render_trace(matrix):
1065
+ failures.append("trace: rendered matrix lacks FR-001")
1066
+ if fr1 and (fr1["requirement_status"], fr1["derived_status"]) \
1067
+ != ("Open", "Open"):
1068
+ failures.append("trace: FR-001 status should be Open/Open, got "
1069
+ + str(fr1))
1070
+
1071
+ def uc(status):
1072
+ spec = Spec.__new__(Spec)
1073
+ spec.id, spec.status = "UC-X", status
1074
+ return spec
1075
+
1076
+ for statuses, expected in (
1077
+ (["Approved"], "Open"), (["Draft", "Implemented"], "In Progress"),
1078
+ (["Implemented", "Done"], "Implemented"),
1079
+ (["Tested", "Done", "Obsolete"], "Verified"),
1080
+ (["Getestet"], "Verified"), (["Obsolete"], None),
1081
+ (["Done", "Unknown"], None)):
1082
+ got = derived_status([uc(s) for s in statuses])
1083
+ if got != expected:
1084
+ failures.append("derived_status(%s): expected %s, got %s"
1085
+ % (statuses, expected, got))
1086
+
1087
+ with tempfile.TemporaryDirectory() as root:
1088
+ write_fixture(root, BROKEN)
1089
+ found = lint(root, validator, bpmn)
1090
+ codes = {f.code for f in found}
1091
+ for code in sorted(BROKEN_EXPECTED - codes):
1092
+ failures.append("broken: expected " + code + ", got "
1093
+ + str(sorted(codes)))
1094
+ dangling = [f.message for f in found if f.code == "DANGLING_REF"]
1095
+ for needle in ("FR-009", "UC-001 BR-007", "UC-042", "UC-009",
1096
+ "missing.bpmn"):
1097
+ if not any(needle in m for m in dangling):
1098
+ failures.append("broken: no DANGLING_REF for " + needle)
1099
+
1100
+ baseline = os.path.join(root, BASELINE_NAME)
1101
+ write_baseline(baseline, found, root)
1102
+ kept, suppressed = apply_baseline(found, read_baseline(baseline),
1103
+ root, True)
1104
+ if [f for f in kept if f.severity != INFO] or not suppressed:
1105
+ failures.append("baseline: findings not suppressed")
1106
+ write_fixture(root, {"use_cases/UC-005-stray.md":
1107
+ spec_text("UC-003", "Cancel Room")})
1108
+ os.rename(os.path.join(root, "use_cases/UC-005-stray.md"),
1109
+ os.path.join(root, "use_cases/UC-003-cancel-room.md"))
1110
+ kept, _ = apply_baseline(lint(root, validator, bpmn),
1111
+ read_baseline(baseline), root, True)
1112
+ if "BASELINE_STALE" not in {f.code for f in kept}:
1113
+ failures.append("baseline: fixed finding not reported as stale")
1114
+
1115
+ if failures:
1116
+ for failure in failures:
1117
+ print("SELF-TEST FAIL: " + failure)
1118
+ return 1
1119
+ print("self-test passed")
1120
+ return 0
1121
+
1122
+
1123
+ # ---------------------------------------------------------------------------
1124
+ # CLI
1125
+ # ---------------------------------------------------------------------------
1126
+
1127
+ def main(argv):
1128
+ parser = argparse.ArgumentParser(
1129
+ description="Lint AI Unified Process specification artifacts "
1130
+ "across files.")
1131
+ parser.add_argument("--docs", default="docs",
1132
+ help="documentation folder (default: docs)")
1133
+ parser.add_argument("--strict", action="store_true",
1134
+ help="treat warnings as failures")
1135
+ parser.add_argument("--format", choices=("text", "json"), default="text")
1136
+ parser.add_argument("--baseline",
1137
+ help="baseline file (default: <docs>/"
1138
+ + BASELINE_NAME + " when it exists)")
1139
+ parser.add_argument("--no-baseline", action="store_true",
1140
+ help="ignore the baseline file")
1141
+ parser.add_argument("--update-baseline", action="store_true",
1142
+ help="accept all current findings into the baseline")
1143
+ parser.add_argument("--only", metavar="ID",
1144
+ help="report only findings about this UC-XXX or "
1145
+ "TC-XXX")
1146
+ parser.add_argument("--trace", action="store_true",
1147
+ help="print the requirement -> use case -> business "
1148
+ "rule -> test case matrix instead of findings")
1149
+ parser.add_argument("--self-test", action="store_true",
1150
+ help="run the built-in fixtures and exit")
1151
+ args = parser.parse_args(argv)
1152
+
1153
+ if args.self_test:
1154
+ return self_test()
1155
+ if not os.path.isdir(args.docs):
1156
+ print("spec_lint.py: no such directory: " + args.docs,
1157
+ file=sys.stderr)
1158
+ return 2
1159
+
1160
+ if args.trace:
1161
+ matrix = trace(args.docs)
1162
+ if args.only:
1163
+ matrix = filter_trace(matrix, args.only)
1164
+ if args.format == "json":
1165
+ print(json.dumps(matrix, indent=2, ensure_ascii=False))
1166
+ else:
1167
+ print(render_trace(matrix))
1168
+ return 0
1169
+
1170
+ validator = load_sibling("*use-case-spec/scripts/validate_use_case.py",
1171
+ "validate_use_case")
1172
+ bpmn = load_sibling("*test-case/scripts/bpmn_paths.py", "bpmn_paths")
1173
+ findings = lint(args.docs, validator, bpmn)
1174
+
1175
+ baseline = args.baseline or os.path.join(args.docs, BASELINE_NAME)
1176
+ if args.update_baseline:
1177
+ count = write_baseline(baseline, findings, args.docs)
1178
+ print("baseline written: %d finding(s) accepted in %s"
1179
+ % (count, baseline))
1180
+ return 0
1181
+
1182
+ suppressed = 0
1183
+ if not args.no_baseline and os.path.isfile(baseline):
1184
+ findings, suppressed = apply_baseline(
1185
+ findings, read_baseline(baseline), args.docs, not args.only)
1186
+ elif args.baseline and not args.no_baseline:
1187
+ print("spec_lint.py: baseline not found: " + args.baseline,
1188
+ file=sys.stderr)
1189
+ return 2
1190
+ if args.only:
1191
+ findings = [f for f in findings if matches_only(f, args.only)]
1192
+
1193
+ counts = {sev: sum(1 for f in findings if f.severity == sev)
1194
+ for sev in (ERROR, WARN, INFO)}
1195
+ if args.format == "json":
1196
+ print(json.dumps({"findings": [f.as_dict() for f in findings],
1197
+ "summary": {"errors": counts[ERROR],
1198
+ "warnings": counts[WARN],
1199
+ "infos": counts[INFO],
1200
+ "suppressed": suppressed}},
1201
+ indent=2, ensure_ascii=False))
1202
+ else:
1203
+ for f in findings:
1204
+ element = " [" + f.element + "]" if f.element else ""
1205
+ print("%s:%d: %s %s%s: %s" % (f.path, f.line, f.severity, f.code,
1206
+ element, f.message))
1207
+ print("%d error(s), %d warning(s), %d info(s), %d suppressed by "
1208
+ "baseline" % (counts[ERROR], counts[WARN], counts[INFO],
1209
+ suppressed))
1210
+ if counts[ERROR] or (args.strict and counts[WARN]):
1211
+ return 1
1212
+ return 0
1213
+
1214
+
1215
+ if __name__ == "__main__":
1216
+ sys.exit(main(sys.argv[1:]))