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,982 @@
1
+ /**
2
+ * AIWF spec workflow - bounded local snapshot / pin / check / review-packet logic.
3
+ *
4
+ * Built-in Node modules only. This module records byte-level snapshots. It does not
5
+ * execute recorded commands and it never asserts approval, verification, or done states.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import crypto from 'node:crypto';
11
+ import { execFileSync } from 'node:child_process';
12
+
13
+ export const SCHEMA_VERSION = 1;
14
+
15
+ // Caps the embedded evidence log size so review packets stay reviewable.
16
+ export const LOG_MAX_BYTES = 1024 * 1024;
17
+
18
+ export const REQUIRED_SPEC_FILES = [
19
+ 'docs/vision.md',
20
+ 'docs/requirements.md',
21
+ 'docs/glossary.md',
22
+ 'docs/entity_model.md',
23
+ 'docs/use_cases.puml'
24
+ ];
25
+
26
+ // Spec directories scanned for additional artifacts, mapped to their kind and extensions.
27
+ export const SPEC_DIRS = {
28
+ 'docs/use_cases': { kind: 'use_case', extensions: ['.md'] },
29
+ 'docs/test_cases': { kind: 'test_case', extensions: ['.md'] },
30
+ 'docs/architecture': { kind: 'architecture', extensions: ['.md'] },
31
+ 'docs/plans': { kind: 'plan', extensions: ['.md'] },
32
+ 'docs/processes': { kind: 'process', extensions: ['.bpmn'] }
33
+ };
34
+
35
+ // Individually tracked optional spec files. The lint baseline is tracked so that suppressed
36
+ // findings cannot change underneath a pin without registering as drift.
37
+ export const OPTIONAL_SPEC_FILES = [
38
+ { path: 'docs/.spec-lint-baseline.json', kind: 'lint_baseline' }
39
+ ];
40
+
41
+ export const EVIDENCE_STATUSES = ['passed', 'failed', 'not_run'];
42
+
43
+ // Kinds a stored pin manifest may legitimately contain.
44
+ const ALLOWED_KINDS = new Set([
45
+ 'required',
46
+ 'use_case',
47
+ 'test_case',
48
+ 'architecture',
49
+ 'plan',
50
+ 'process',
51
+ 'lint_baseline'
52
+ ]);
53
+
54
+ const HEX64 = /^[0-9a-f]{64}$/;
55
+
56
+ export const LIMITATIONS = [
57
+ 'Recorded check output is not proof of business acceptance.',
58
+ 'Check statuses are reported by the submitter and are not independently verified by this tool.',
59
+ 'Remote synchronization was not attempted.',
60
+ 'Human acceptance has not been recorded.',
61
+ 'Structural and semantic lint is not performed by this tool; it remains an upstream step.'
62
+ ];
63
+
64
+ export class SpecError extends Error {
65
+ constructor(code, message) {
66
+ super(message);
67
+ this.name = 'SpecError';
68
+ this.code = code;
69
+ }
70
+ }
71
+
72
+ function toPosix(p) {
73
+ return p.split(path.sep).join('/');
74
+ }
75
+
76
+ function sha256Hex(buffer) {
77
+ return crypto.createHash('sha256').update(buffer).digest('hex');
78
+ }
79
+
80
+ function safeLstat(target) {
81
+ try {
82
+ return fs.lstatSync(target);
83
+ } catch (err) {
84
+ if (err.code === 'ENOENT') {
85
+ return null;
86
+ }
87
+ throw err;
88
+ }
89
+ }
90
+
91
+ function isInsideRoot(root, target) {
92
+ const rel = path.relative(root, target);
93
+ if (rel === '') {
94
+ return true;
95
+ }
96
+ return !rel.startsWith('..') && !path.isAbsolute(rel);
97
+ }
98
+
99
+ function assertInsideRoot(root, target) {
100
+ if (!isInsideRoot(root, target)) {
101
+ throw new SpecError('path_escape', `Path escapes root: ${toPosix(path.relative(root, target))}`);
102
+ }
103
+ }
104
+
105
+ // Walk every component strictly below `root` and reject symlinks so artifacts cannot escape.
106
+ function assertNoSymlink(root, target) {
107
+ const rel = path.relative(root, target);
108
+ if (rel === '') {
109
+ return;
110
+ }
111
+ let current = root;
112
+ for (const part of rel.split(path.sep)) {
113
+ current = path.join(current, part);
114
+ const stat = safeLstat(current);
115
+ if (!stat) {
116
+ return;
117
+ }
118
+ if (stat.isSymbolicLink()) {
119
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${toPosix(path.relative(root, current))}`);
120
+ }
121
+ }
122
+ }
123
+
124
+ function assertRootDirectory(root) {
125
+ const stat = safeLstat(root);
126
+ if (!stat || !stat.isDirectory()) {
127
+ throw new SpecError('root_not_found', `Root is not an existing directory: ${root}`);
128
+ }
129
+ }
130
+
131
+ function hashFile(absPath, relPath, kind) {
132
+ const buffer = fs.readFileSync(absPath);
133
+ return {
134
+ abs: absPath,
135
+ path: toPosix(relPath),
136
+ sha256: sha256Hex(buffer),
137
+ bytes: buffer.length,
138
+ kind
139
+ };
140
+ }
141
+
142
+ function listArtifactsInDir(root, relDir, kind, extensions) {
143
+ const absDir = path.join(root, relDir);
144
+ assertNoSymlink(root, absDir);
145
+ const stat = safeLstat(absDir);
146
+ if (!stat) {
147
+ return [];
148
+ }
149
+ if (stat.isSymbolicLink()) {
150
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${relDir}`);
151
+ }
152
+ if (!stat.isDirectory()) {
153
+ throw new SpecError('artifact_not_directory', `Expected a directory: ${relDir}`);
154
+ }
155
+
156
+ const found = [];
157
+ const walk = dir => {
158
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
159
+ const abs = path.join(dir, entry.name);
160
+ const entryStat = fs.lstatSync(abs);
161
+ const rel = toPosix(path.relative(root, abs));
162
+ if (entryStat.isSymbolicLink()) {
163
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${rel}`);
164
+ }
165
+ if (entryStat.isDirectory()) {
166
+ walk(abs);
167
+ } else if (entryStat.isFile() && extensions.some(ext => entry.name.toLowerCase().endsWith(ext))) {
168
+ found.push(hashFile(abs, rel, kind));
169
+ }
170
+ }
171
+ };
172
+ walk(absDir);
173
+ return found;
174
+ }
175
+
176
+ function sortByPath(a, b) {
177
+ if (a.path < b.path) {
178
+ return -1;
179
+ }
180
+ if (a.path > b.path) {
181
+ return 1;
182
+ }
183
+ return 0;
184
+ }
185
+
186
+ function isIdentifiedArtifact(file, kind) {
187
+ const pattern = kind === 'use_case' ? /^[SB]?UC-.+\.md$/ : /^TC-.+\.md$/;
188
+ return file.kind === kind && pattern.test(path.posix.basename(file.path));
189
+ }
190
+
191
+ /**
192
+ * Collect deterministic byte SHA256 snapshots of all spec artifacts under `root`.
193
+ * Returns files sorted by posix relative path plus counts and missing required files.
194
+ */
195
+ export function collectSnapshot(root, options = {}) {
196
+ const absRoot = path.resolve(root);
197
+ const files = [];
198
+ const missingRequired = [];
199
+
200
+ for (const rel of REQUIRED_SPEC_FILES) {
201
+ const abs = path.join(absRoot, rel);
202
+ assertInsideRoot(absRoot, abs);
203
+ assertNoSymlink(absRoot, abs);
204
+ const stat = safeLstat(abs);
205
+ if (!stat) {
206
+ missingRequired.push(rel);
207
+ continue;
208
+ }
209
+ if (stat.isSymbolicLink()) {
210
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${rel}`);
211
+ }
212
+ if (!stat.isFile()) {
213
+ throw new SpecError('artifact_not_file', `Expected a file: ${rel}`);
214
+ }
215
+ files.push(hashFile(abs, rel, 'required'));
216
+ }
217
+
218
+ for (const [relDir, config] of Object.entries(SPEC_DIRS)) {
219
+ files.push(...listArtifactsInDir(absRoot, relDir, config.kind, config.extensions));
220
+ }
221
+
222
+ for (const optional of OPTIONAL_SPEC_FILES) {
223
+ const abs = path.join(absRoot, optional.path);
224
+ assertInsideRoot(absRoot, abs);
225
+ assertNoSymlink(absRoot, abs);
226
+ const stat = safeLstat(abs);
227
+ if (!stat) {
228
+ continue;
229
+ }
230
+ if (stat.isSymbolicLink()) {
231
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${optional.path}`);
232
+ }
233
+ if (stat.isFile()) {
234
+ files.push(hashFile(abs, optional.path, optional.kind));
235
+ }
236
+ }
237
+
238
+ files.sort(sortByPath);
239
+
240
+ const useCaseCount = files.filter(f => isIdentifiedArtifact(f, 'use_case')).length;
241
+ const testCaseCount = files.filter(f => isIdentifiedArtifact(f, 'test_case')).length;
242
+
243
+ if (options.enforceMinimums && missingRequired.length > 0) {
244
+ throw new SpecError(
245
+ 'missing_required',
246
+ `Missing required spec files: ${missingRequired.join(', ')}`
247
+ );
248
+ }
249
+
250
+ return { root: absRoot, files, missingRequired, useCaseCount, testCaseCount };
251
+ }
252
+
253
+ /**
254
+ * Stable digest over canonical path + content-hash pairs, sorted by path.
255
+ */
256
+ export function computePinDigest(files) {
257
+ const canonical = [...files]
258
+ .sort(sortByPath)
259
+ .map(f => `${f.path}\u0000${f.sha256}`)
260
+ .join('\n');
261
+ return sha256Hex(Buffer.from(canonical, 'utf8'));
262
+ }
263
+
264
+ /**
265
+ * Detect draft markers (case-insensitive) that must block pinning.
266
+ */
267
+ export function findDraftMarkers(files) {
268
+ const results = [];
269
+ for (const file of files) {
270
+ // Authored spec content only: the lint baseline is machine data, not a spec statement.
271
+ if (file.kind === 'lint_baseline') {
272
+ continue;
273
+ }
274
+ const text = fs.readFileSync(file.abs, 'utf8');
275
+ const markers = [];
276
+ // Explicit drafting syntax only; a product named "Todo Manager" is valid prose.
277
+ if (/(?:^|[^A-Za-z0-9])todo\s*:/i.test(text) ||
278
+ /\[todo(?:\s|\])/i.test(text) ||
279
+ /^\s*(?:[-*]\s+)?todo\s*$/im.test(text)) {
280
+ markers.push('TODO');
281
+ }
282
+ if (/needs[\s_-]*clarification/i.test(text)) {
283
+ markers.push('NEEDS CLARIFICATION');
284
+ }
285
+ if (markers.length > 0) {
286
+ results.push({ path: file.path, markers });
287
+ }
288
+ }
289
+ return results;
290
+ }
291
+
292
+ /**
293
+ * Best-effort git HEAD + dirty status. Returns null when the location is not a usable repo.
294
+ * Uses only node built-ins; never throws.
295
+ */
296
+ export function readGitInfo(root) {
297
+ const run = args =>
298
+ execFileSync('git', args, {
299
+ cwd: root,
300
+ encoding: 'utf8',
301
+ stdio: ['ignore', 'pipe', 'ignore']
302
+ });
303
+ try {
304
+ const head = run(['rev-parse', 'HEAD']).trim();
305
+ if (!head) {
306
+ return null;
307
+ }
308
+ let dirty = null;
309
+ try {
310
+ dirty = run(['status', '--porcelain']).trim().length > 0;
311
+ } catch {
312
+ dirty = null;
313
+ }
314
+ return { head, dirty };
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+
320
+ const PIN_DIR = '.aiwf';
321
+ const PIN_FILE = 'spec-pin.json';
322
+
323
+ function pinPathFor(root) {
324
+ return path.join(root, PIN_DIR, PIN_FILE);
325
+ }
326
+
327
+ function buildPin(files, digest, git) {
328
+ const pin = {
329
+ schema_version: SCHEMA_VERSION,
330
+ spec_digest: digest
331
+ };
332
+ // Git context is auxiliary metadata and is intentionally excluded from spec_digest.
333
+ if (git) {
334
+ pin.git = git;
335
+ }
336
+ pin.files = files.map(f => ({ path: f.path, sha256: f.sha256, bytes: f.bytes, kind: f.kind }));
337
+ return pin;
338
+ }
339
+
340
+ function publicFile(file) {
341
+ return { path: file.path, sha256: file.sha256, bytes: file.bytes, kind: file.kind };
342
+ }
343
+
344
+ function invalidPin(code, message) {
345
+ return { valid: false, code, message };
346
+ }
347
+
348
+ // Stored paths must be canonical, relative, posix, and traversal-free.
349
+ function isCanonicalRelativePath(candidate) {
350
+ if (typeof candidate !== 'string' || candidate === '') {
351
+ return false;
352
+ }
353
+ if (candidate.includes('\\') || candidate.startsWith('/') || candidate.startsWith('./')) {
354
+ return false;
355
+ }
356
+ if (path.posix.isAbsolute(candidate)) {
357
+ return false;
358
+ }
359
+ const segments = candidate.split('/');
360
+ if (segments.some(segment => segment === '' || segment === '.' || segment === '..')) {
361
+ return false;
362
+ }
363
+ return path.posix.normalize(candidate) === candidate;
364
+ }
365
+
366
+ /**
367
+ * Strictly validate a stored pin. Returns { valid: true, files } or { valid: false, code, message }.
368
+ * A pin that omits required artifacts, minimum use/test cases, or whose manifest no longer
369
+ * matches its own spec_digest is treated as malformed rather than trusted.
370
+ */
371
+ export function validateStoredPin(pin) {
372
+ if (pin === null || typeof pin !== 'object' || Array.isArray(pin)) {
373
+ return invalidPin('pin_shape', 'Pin must be a JSON object.');
374
+ }
375
+ if (pin.schema_version !== SCHEMA_VERSION) {
376
+ return invalidPin('pin_schema_version', `Unsupported pin schema_version: ${String(pin.schema_version)}`);
377
+ }
378
+ if (typeof pin.spec_digest !== 'string' || !HEX64.test(pin.spec_digest)) {
379
+ return invalidPin('pin_digest_format', 'Pin spec_digest must be a 64-character hex string.');
380
+ }
381
+ if (!Array.isArray(pin.files) || pin.files.length === 0) {
382
+ return invalidPin('pin_files', 'Pin files must be a non-empty array.');
383
+ }
384
+ if (pin.git !== undefined) {
385
+ if (pin.git === null || typeof pin.git !== 'object' || Array.isArray(pin.git)) {
386
+ return invalidPin('pin_git', 'Pin git metadata must be an object when present.');
387
+ }
388
+ if (typeof pin.git.head !== 'string' || !/^[0-9a-f]{7,40}$/.test(pin.git.head)) {
389
+ return invalidPin('pin_git', 'Pin git.head must be a hex commit id.');
390
+ }
391
+ if (pin.git.dirty !== null && typeof pin.git.dirty !== 'boolean') {
392
+ return invalidPin('pin_git', 'Pin git.dirty must be a boolean or null.');
393
+ }
394
+ }
395
+
396
+ const seen = new Set();
397
+ const snapshots = [];
398
+ for (const file of pin.files) {
399
+ if (file === null || typeof file !== 'object' || Array.isArray(file)) {
400
+ return invalidPin('pin_file_entry', 'Each pin file entry must be an object.');
401
+ }
402
+ if (!isCanonicalRelativePath(file.path)) {
403
+ return invalidPin('pin_file_path', `Pin file path is not a safe canonical relative path: ${String(file.path)}`);
404
+ }
405
+ if (seen.has(file.path)) {
406
+ return invalidPin('pin_file_duplicate', `Duplicate pin file path: ${file.path}`);
407
+ }
408
+ seen.add(file.path);
409
+ if (typeof file.sha256 !== 'string' || !HEX64.test(file.sha256)) {
410
+ return invalidPin('pin_file_hash', `Pin file hash must be a 64-character hex string: ${file.path}`);
411
+ }
412
+ if (!Number.isInteger(file.bytes) || file.bytes < 0) {
413
+ return invalidPin('pin_file_bytes', `Pin file bytes must be a non-negative integer: ${file.path}`);
414
+ }
415
+ if (!ALLOWED_KINDS.has(file.kind)) {
416
+ return invalidPin('pin_file_kind', `Pin file kind is not allowed: ${String(file.kind)}`);
417
+ }
418
+ snapshots.push({ path: file.path, sha256: file.sha256, bytes: file.bytes, kind: file.kind });
419
+ }
420
+
421
+ const byPath = new Map(snapshots.map(file => [file.path, file]));
422
+ for (const required of REQUIRED_SPEC_FILES) {
423
+ const entry = byPath.get(required);
424
+ if (!entry) {
425
+ return invalidPin('pin_missing_required', `Pin is missing required artifact: ${required}`);
426
+ }
427
+ if (entry.kind !== 'required') {
428
+ return invalidPin('pin_required_kind', `Pin kind for ${required} must be 'required'.`);
429
+ }
430
+ }
431
+ if (!snapshots.some(file => isIdentifiedArtifact(file, 'use_case'))) {
432
+ return invalidPin('pin_missing_use_case', 'Pin manifest has no use_case artifact.');
433
+ }
434
+ if (!snapshots.some(file => isIdentifiedArtifact(file, 'test_case'))) {
435
+ return invalidPin('pin_missing_test_case', 'Pin manifest has no test_case artifact.');
436
+ }
437
+
438
+ if (computePinDigest(snapshots) !== pin.spec_digest) {
439
+ return invalidPin('pin_digest_mismatch', 'Pin spec_digest does not match its file manifest.');
440
+ }
441
+
442
+ return { valid: true, files: snapshots };
443
+ }
444
+
445
+ function readExistingPin(root) {
446
+ const pinPath = pinPathFor(root);
447
+ assertNoSymlink(root, path.join(root, PIN_DIR));
448
+ assertNoSymlink(root, pinPath);
449
+ const stat = safeLstat(pinPath);
450
+ if (!stat) {
451
+ return { exists: false, pinPath };
452
+ }
453
+ if (stat.isSymbolicLink()) {
454
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${PIN_DIR}/${PIN_FILE}`);
455
+ }
456
+ let parsed = null;
457
+ let text = null;
458
+ try {
459
+ text = fs.readFileSync(pinPath, 'utf8');
460
+ parsed = JSON.parse(text);
461
+ } catch {
462
+ parsed = null;
463
+ }
464
+ return { exists: true, pinPath, text, pin: parsed };
465
+ }
466
+
467
+ /**
468
+ * Pin the current spec snapshot. Refuses to overwrite an existing, differing pin
469
+ * unless `refresh` is true. Identical content is idempotent.
470
+ */
471
+ export function pinSpec(root, options = {}) {
472
+ const absRoot = path.resolve(root);
473
+ assertRootDirectory(absRoot);
474
+ assertNoSymlink(absRoot, path.join(absRoot, PIN_DIR));
475
+ assertNoSymlink(absRoot, path.join(absRoot, 'docs'));
476
+
477
+ const snapshot = collectSnapshot(absRoot, { enforceMinimums: true });
478
+
479
+ const markers = findDraftMarkers(snapshot.files);
480
+ if (markers.length > 0) {
481
+ const detail = markers.map(m => `${m.path} (${m.markers.join(', ')})`).join('; ');
482
+ throw new SpecError('draft_markers', `Draft markers block pinning: ${detail}`);
483
+ }
484
+
485
+ if (snapshot.useCaseCount < 1) {
486
+ throw new SpecError(
487
+ 'insufficient_use_cases',
488
+ 'At least one use case markdown file is required under docs/use_cases before pinning.'
489
+ );
490
+ }
491
+ if (snapshot.testCaseCount < 1) {
492
+ throw new SpecError(
493
+ 'insufficient_test_cases',
494
+ 'At least one test case markdown file is required under docs/test_cases before pinning.'
495
+ );
496
+ }
497
+
498
+ const digest = computePinDigest(snapshot.files);
499
+ const git = readGitInfo(absRoot);
500
+ const pin = buildPin(snapshot.files, digest, git);
501
+ const existing = readExistingPin(absRoot);
502
+
503
+ if (existing.exists) {
504
+ const validation = validateStoredPin(existing.pin);
505
+ if (!validation.valid) {
506
+ if (!options.refresh) {
507
+ throw new SpecError(
508
+ 'pin_invalid_existing',
509
+ `Existing pin is invalid (${validation.code}); use --refresh to replace it.`
510
+ );
511
+ }
512
+ } else if (existing.pin.spec_digest === digest) {
513
+ return { command: 'pin', root: absRoot, unchanged: true, pin, digest };
514
+ } else if (!options.refresh) {
515
+ throw new SpecError('pin_exists', 'A different pin already exists; re-run with --refresh to overwrite it.');
516
+ }
517
+ }
518
+
519
+ fs.mkdirSync(path.join(absRoot, PIN_DIR), { recursive: true });
520
+ assertNoSymlink(absRoot, pinPathFor(absRoot));
521
+ fs.writeFileSync(existing.pinPath, `${JSON.stringify(pin, null, 2)}\n`);
522
+
523
+ return { command: 'pin', root: absRoot, unchanged: false, pin, digest };
524
+ }
525
+
526
+ function compareSnapshots(pin, snapshot) {
527
+ const pinnedByPath = new Map(pin.files.map(f => [f.path, f]));
528
+ const currentByPath = new Map(snapshot.files.map(f => [f.path, f]));
529
+
530
+ const added = [];
531
+ const changed = [];
532
+ const deleted = [];
533
+
534
+ for (const [filePath, file] of currentByPath) {
535
+ const previous = pinnedByPath.get(filePath);
536
+ if (!previous) {
537
+ added.push(filePath);
538
+ } else if (previous.sha256 !== file.sha256) {
539
+ changed.push(filePath);
540
+ }
541
+ }
542
+ for (const filePath of pinnedByPath.keys()) {
543
+ if (!currentByPath.has(filePath)) {
544
+ deleted.push(filePath);
545
+ }
546
+ }
547
+
548
+ return { added: added.sort(), changed: changed.sort(), deleted: deleted.sort() };
549
+ }
550
+
551
+ /**
552
+ * Compare the pinned manifest to the current snapshot. Read-only.
553
+ */
554
+ export function checkSpec(root) {
555
+ const absRoot = path.resolve(root);
556
+ assertRootDirectory(absRoot);
557
+
558
+ // Uniform shape so callers can read array fields regardless of pin state.
559
+ const base = {
560
+ command: 'check',
561
+ root: absRoot,
562
+ in_sync: false,
563
+ pin_digest: null,
564
+ current_digest: null,
565
+ digest_matches: false,
566
+ added: [],
567
+ changed: [],
568
+ deleted: [],
569
+ draft_markers: [],
570
+ missing_required: [],
571
+ use_case_count: 0,
572
+ test_case_count: 0,
573
+ files: [],
574
+ git: null
575
+ };
576
+
577
+ const existing = readExistingPin(absRoot);
578
+ if (!existing.exists) {
579
+ return { ...base, status: 'pin_missing' };
580
+ }
581
+
582
+ const validation = validateStoredPin(existing.pin);
583
+ if (!validation.valid) {
584
+ return { ...base, status: 'pin_invalid', error: { code: validation.code, message: validation.message } };
585
+ }
586
+ const pin = existing.pin;
587
+
588
+ const snapshot = collectSnapshot(absRoot);
589
+ const drift = compareSnapshots(pin, snapshot);
590
+ const draftMarkers = findDraftMarkers(snapshot.files);
591
+ const currentDigest = computePinDigest(snapshot.files);
592
+ const digestMatches = currentDigest === pin.spec_digest;
593
+ const minimumsOk = snapshot.useCaseCount >= 1 && snapshot.testCaseCount >= 1;
594
+ const hasDrift = drift.added.length > 0 || drift.changed.length > 0 || drift.deleted.length > 0;
595
+ const inSync =
596
+ digestMatches &&
597
+ !hasDrift &&
598
+ snapshot.missingRequired.length === 0 &&
599
+ minimumsOk &&
600
+ draftMarkers.length === 0;
601
+
602
+ let status = 'ok';
603
+ if (!inSync) {
604
+ status = draftMarkers.length > 0 ? 'draft' : 'drift';
605
+ }
606
+
607
+ return {
608
+ ...base,
609
+ status,
610
+ in_sync: inSync,
611
+ pin_digest: pin.spec_digest,
612
+ current_digest: currentDigest,
613
+ digest_matches: digestMatches,
614
+ added: drift.added,
615
+ changed: drift.changed,
616
+ deleted: drift.deleted,
617
+ draft_markers: draftMarkers,
618
+ missing_required: snapshot.missingRequired,
619
+ use_case_count: snapshot.useCaseCount,
620
+ test_case_count: snapshot.testCaseCount,
621
+ files: snapshot.files.map(publicFile),
622
+ git: readGitInfo(absRoot)
623
+ };
624
+ }
625
+
626
+ function validateEvidenceShape(raw) {
627
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
628
+ throw new SpecError('evidence_invalid', 'Evidence must be a JSON object.');
629
+ }
630
+ const allowedTop = new Set(['checks', 'unverified']);
631
+ for (const key of Object.keys(raw)) {
632
+ if (!allowedTop.has(key)) {
633
+ throw new SpecError('evidence_unknown_field', `Unknown evidence field: ${key}`);
634
+ }
635
+ }
636
+ if (!Array.isArray(raw.checks)) {
637
+ throw new SpecError('evidence_invalid', 'evidence.checks must be an array.');
638
+ }
639
+ if (raw.checks.length < 1) {
640
+ throw new SpecError('evidence_no_checks', 'At least one check is required.');
641
+ }
642
+ if (raw.unverified !== undefined && !Array.isArray(raw.unverified)) {
643
+ throw new SpecError('evidence_invalid', 'evidence.unverified must be an array of strings.');
644
+ }
645
+ const unverified = raw.unverified === undefined ? [] : raw.unverified;
646
+ for (const entry of unverified) {
647
+ if (typeof entry !== 'string') {
648
+ throw new SpecError('evidence_invalid', 'evidence.unverified entries must be strings.');
649
+ }
650
+ }
651
+
652
+ const allowedCheck = new Set(['name', 'command', 'status', 'log']);
653
+ const names = new Set();
654
+ const checks = raw.checks.map(check => {
655
+ if (check === null || typeof check !== 'object' || Array.isArray(check)) {
656
+ throw new SpecError('evidence_invalid', 'Each check must be an object.');
657
+ }
658
+ for (const key of Object.keys(check)) {
659
+ if (!allowedCheck.has(key)) {
660
+ throw new SpecError('evidence_unknown_field', `Unknown check field: ${key}`);
661
+ }
662
+ }
663
+ if (typeof check.name !== 'string' || check.name.trim() === '') {
664
+ throw new SpecError('evidence_invalid', 'Each check requires a non-empty name.');
665
+ }
666
+ if (typeof check.command !== 'string' || check.command.trim() === '') {
667
+ throw new SpecError('evidence_invalid', 'Each check requires a non-empty command string.');
668
+ }
669
+ if (!EVIDENCE_STATUSES.includes(check.status)) {
670
+ throw new SpecError('evidence_invalid_status', `Invalid check status: ${String(check.status)}`);
671
+ }
672
+ if (names.has(check.name)) {
673
+ throw new SpecError('evidence_duplicate_name', `Duplicate check name: ${check.name}`);
674
+ }
675
+ names.add(check.name);
676
+
677
+ const executed = check.status === 'passed' || check.status === 'failed';
678
+ if (executed) {
679
+ if (typeof check.log !== 'string' || check.log.trim() === '') {
680
+ throw new SpecError('evidence_log_required', `Executed check '${check.name}' requires a log path.`);
681
+ }
682
+ } else if (check.log !== undefined && typeof check.log !== 'string') {
683
+ throw new SpecError('evidence_invalid', `Check '${check.name}' log must be a string.`);
684
+ }
685
+
686
+ return { name: check.name, command: check.command, status: check.status, log: check.log };
687
+ });
688
+
689
+ return { checks, unverified };
690
+ }
691
+
692
+ /**
693
+ * Validate parsed evidence JSON without touching the filesystem.
694
+ */
695
+ export function parseEvidence(raw) {
696
+ return validateEvidenceShape(raw);
697
+ }
698
+
699
+ function readEvidenceFile(evidencePath, cwd) {
700
+ const abs = path.resolve(cwd || process.cwd(), evidencePath);
701
+ const stat = safeLstat(abs);
702
+ if (!stat) {
703
+ throw new SpecError('evidence_missing', `Evidence file not found: ${evidencePath}`);
704
+ }
705
+ if (stat.isSymbolicLink()) {
706
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${evidencePath}`);
707
+ }
708
+ if (!stat.isFile()) {
709
+ throw new SpecError('evidence_invalid', `Evidence path is not a file: ${evidencePath}`);
710
+ }
711
+ let raw;
712
+ try {
713
+ raw = JSON.parse(fs.readFileSync(abs, 'utf8'));
714
+ } catch (err) {
715
+ throw new SpecError('evidence_invalid_json', `Evidence is not valid JSON: ${err.message}`);
716
+ }
717
+ return { abs, evidence: validateEvidenceShape(raw) };
718
+ }
719
+
720
+ function readLogWithinRoot(root, relLog) {
721
+ const abs = path.resolve(root, relLog);
722
+ assertInsideRoot(root, abs);
723
+ assertNoSymlink(root, abs);
724
+ const stat = safeLstat(abs);
725
+ if (!stat) {
726
+ throw new SpecError('log_missing', `Log file not found: ${relLog}`);
727
+ }
728
+ if (stat.isSymbolicLink()) {
729
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${relLog}`);
730
+ }
731
+ if (!stat.isFile()) {
732
+ throw new SpecError('log_not_file', `Log path is not a file: ${relLog}`);
733
+ }
734
+ if (stat.size > LOG_MAX_BYTES) {
735
+ throw new SpecError('log_too_large', `Log exceeds ${LOG_MAX_BYTES} bytes: ${relLog}`);
736
+ }
737
+ const buffer = fs.readFileSync(abs);
738
+ const content = buffer.toString('utf8');
739
+ // Guarantee the embedded body is byte-exact; non-UTF-8 logs are rejected rather than mangled.
740
+ if (!Buffer.from(content, 'utf8').equals(buffer)) {
741
+ throw new SpecError('log_not_utf8', `Log is not valid UTF-8 and cannot be embedded exactly: ${relLog}`);
742
+ }
743
+ return {
744
+ path: toPosix(path.relative(root, abs)),
745
+ sha256: sha256Hex(buffer),
746
+ bytes: buffer.length,
747
+ content
748
+ };
749
+ }
750
+
751
+ /**
752
+ * Build a local review packet. Refuses when the pin is missing, invalid, or drifted.
753
+ * Evidence statuses are recorded as reported, never as independently verified.
754
+ */
755
+ export function createReviewPacket(root, evidencePath, options = {}) {
756
+ const absRoot = path.resolve(root);
757
+ const check = checkSpec(absRoot);
758
+
759
+ if (check.status === 'pin_missing' || check.status === 'pin_invalid') {
760
+ throw new SpecError('packet_requires_pin', 'A valid pin is required before building a review packet.');
761
+ }
762
+ if (check.status !== 'ok') {
763
+ throw new SpecError(
764
+ 'packet_requires_sync',
765
+ `Spec is not in sync (status: ${check.status}); refresh the pin before building a review packet.`
766
+ );
767
+ }
768
+
769
+ const { evidence } = readEvidenceFile(evidencePath, options.cwd);
770
+
771
+ const checks = evidence.checks.map(checkEntry => {
772
+ const record = { name: checkEntry.name, command: checkEntry.command, status: checkEntry.status };
773
+ if (checkEntry.log !== undefined) {
774
+ record.log = readLogWithinRoot(absRoot, checkEntry.log);
775
+ }
776
+ return record;
777
+ });
778
+
779
+ const summary = { total: checks.length, passed: 0, failed: 0, not_run: 0 };
780
+ for (const entry of checks) {
781
+ summary[entry.status] += 1;
782
+ }
783
+
784
+ return {
785
+ schema_version: SCHEMA_VERSION,
786
+ collected_at: new Date().toISOString(),
787
+ status: 'awaiting_review',
788
+ acceptance: 'not_recorded',
789
+ remote_sync: 'not_attempted',
790
+ spec: {
791
+ digest: check.current_digest,
792
+ files: check.files.map(f => ({ path: f.path, sha256: f.sha256, bytes: f.bytes, kind: f.kind }))
793
+ },
794
+ pin: {
795
+ digest: check.pin_digest,
796
+ matched: check.pin_digest === check.current_digest
797
+ },
798
+ drift: { added: check.added, changed: check.changed, deleted: check.deleted },
799
+ git: check.git || null,
800
+ evidence: {
801
+ trust: 'reported_untrusted',
802
+ checks_summary: summary,
803
+ checks,
804
+ unverified: evidence.unverified
805
+ },
806
+ limitations: [...LIMITATIONS]
807
+ };
808
+ }
809
+
810
+ /**
811
+ * Write a review packet to disk. Refuses to overwrite an existing file unless `force`.
812
+ */
813
+ export function writeReviewPacket(root, packet, options = {}) {
814
+ const absRoot = path.resolve(root);
815
+ const cwd = options.cwd || process.cwd();
816
+ const output = options.output
817
+ ? path.resolve(cwd, options.output)
818
+ : path.join(absRoot, PIN_DIR, 'review-packet.json');
819
+
820
+ // The review packet is a local artifact: keep it inside the project root so it cannot
821
+ // be written through an escaping or symlinked path.
822
+ if (output === absRoot || !isInsideRoot(absRoot, output)) {
823
+ throw new SpecError('output_outside_root', `Packet output must stay inside the project root: ${output}`);
824
+ }
825
+ assertNoSymlink(absRoot, output);
826
+
827
+ const existing = safeLstat(output);
828
+ if (existing) {
829
+ if (existing.isSymbolicLink()) {
830
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${output}`);
831
+ }
832
+ if (!existing.isFile()) {
833
+ throw new SpecError('output_not_file', `Packet output exists and is not a regular file: ${output}`);
834
+ }
835
+ if (!options.force) {
836
+ throw new SpecError('output_exists', `Refusing to overwrite existing packet: ${output} (use --force)`);
837
+ }
838
+ }
839
+
840
+ fs.mkdirSync(path.dirname(output), { recursive: true });
841
+ fs.writeFileSync(output, `${JSON.stringify(packet, null, 2)}\n`);
842
+ return { output, written: true };
843
+ }
844
+
845
+ /**
846
+ * Initialize minimal AIUP-compatible Draft specs without overwriting user files.
847
+ */
848
+ export function initSpec(root, name) {
849
+ const absRoot = path.resolve(root);
850
+ assertRootDirectory(absRoot);
851
+ const title = typeof name === 'string' && name.trim() !== '' ? name : path.basename(absRoot);
852
+
853
+ const docsDir = path.join(absRoot, 'docs');
854
+ assertNoSymlink(absRoot, docsDir);
855
+ const docsStat = safeLstat(docsDir);
856
+ if (docsStat && !docsStat.isDirectory()) {
857
+ throw new SpecError('artifact_not_directory', 'docs exists and is not a directory.');
858
+ }
859
+ fs.mkdirSync(docsDir, { recursive: true });
860
+
861
+ const useCasesDir = path.join(docsDir, 'use_cases');
862
+ const testCasesDir = path.join(docsDir, 'test_cases');
863
+ for (const dir of [useCasesDir, testCasesDir]) {
864
+ assertNoSymlink(absRoot, dir);
865
+ const stat = safeLstat(dir);
866
+ if (stat && stat.isSymbolicLink()) {
867
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${toPosix(path.relative(absRoot, dir))}`);
868
+ }
869
+ if (stat && !stat.isDirectory()) {
870
+ throw new SpecError('artifact_not_directory', `Expected a directory: ${toPosix(path.relative(absRoot, dir))}`);
871
+ }
872
+ fs.mkdirSync(dir, { recursive: true });
873
+ }
874
+
875
+ const templates = new Map([
876
+ ['docs/vision.md', templateVision(title)],
877
+ ['docs/requirements.md', templateRequirements()],
878
+ ['docs/glossary.md', templateGlossary()],
879
+ ['docs/entity_model.md', templateEntityModel()],
880
+ ['docs/use_cases.puml', templateUseCasesPuml()]
881
+ ]);
882
+
883
+ const created = [];
884
+ const preserved = [];
885
+
886
+ for (const [rel, content] of templates) {
887
+ const abs = path.join(absRoot, rel);
888
+ assertNoSymlink(absRoot, abs);
889
+ const stat = safeLstat(abs);
890
+ if (stat) {
891
+ if (stat.isSymbolicLink()) {
892
+ throw new SpecError('symlink_not_allowed', `Symlink not allowed: ${rel}`);
893
+ }
894
+ preserved.push(rel);
895
+ continue;
896
+ }
897
+ fs.writeFileSync(abs, content);
898
+ created.push(rel);
899
+ }
900
+
901
+ for (const rel of ['docs/use_cases', 'docs/test_cases']) {
902
+ preserved.push(rel);
903
+ }
904
+
905
+ return { command: 'init', root: absRoot, name: title, created, preserved };
906
+ }
907
+
908
+ function templateVision(title) {
909
+ return [
910
+ '# Vision',
911
+ '',
912
+ '> Status: Draft',
913
+ '',
914
+ '## Product',
915
+ '',
916
+ title,
917
+ '',
918
+ '## Problem',
919
+ '',
920
+ 'TODO: Describe the user problem this product solves.',
921
+ '',
922
+ '## Outcome',
923
+ '',
924
+ 'TODO: Describe the measurable outcome.',
925
+ '',
926
+ '## Non-Goals',
927
+ '',
928
+ 'TODO: List what is explicitly out of scope.',
929
+ ''
930
+ ].join('\n');
931
+ }
932
+
933
+ function templateRequirements() {
934
+ return [
935
+ '# Requirements',
936
+ '',
937
+ '> Status: Draft',
938
+ '',
939
+ '## Functional Requirements',
940
+ '',
941
+ '- TODO: FR-001 Describe a required behavior.',
942
+ '',
943
+ '## Non-Functional Requirements',
944
+ '',
945
+ '- TODO: NFR-001 Describe a quality constraint.',
946
+ ''
947
+ ].join('\n');
948
+ }
949
+
950
+ function templateGlossary() {
951
+ return [
952
+ '# Glossary',
953
+ '',
954
+ '> Status: Draft',
955
+ '',
956
+ '- TODO: term - definition.',
957
+ ''
958
+ ].join('\n');
959
+ }
960
+
961
+ function templateEntityModel() {
962
+ return [
963
+ '# Entity Model',
964
+ '',
965
+ '> Status: Draft',
966
+ '',
967
+ '## Entities',
968
+ '',
969
+ '- TODO: Entity - fields and relationships.',
970
+ ''
971
+ ].join('\n');
972
+ }
973
+
974
+ function templateUseCasesPuml() {
975
+ return [
976
+ '@startuml',
977
+ "' Status: Draft",
978
+ "' TODO: Model actors and use cases before pinning.",
979
+ '@enduml',
980
+ ''
981
+ ].join('\n');
982
+ }