aiwf 0.3.23 → 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.
- package/.claude-plugin/marketplace.json +146 -0
- package/CHANGELOG.md +49 -1
- package/README.ko.md +118 -309
- package/README.md +68 -254
- package/docs/modernization/BROWNFIELD-GREENFIELD.ko.md +105 -0
- package/docs/modernization/CLAUDE-PLAN-REVIEW-2026-10-03.ko.md +77 -0
- package/docs/modernization/CLI-PRODUCTIVITY-REVIEWED-2026-10-03.ko.md +177 -0
- package/docs/modernization/CLI-PRODUCTIVITY.ko.md +193 -0
- package/docs/modernization/CORE-PACKAGE-PLAN.md +7 -0
- package/docs/modernization/DEEP-REVERSE-ENGINEERING.ko.md +88 -0
- package/docs/modernization/DELEGATION-OPTIONAL.ko.md +57 -0
- package/docs/modernization/DIRECTION.ko.md +56 -0
- package/docs/modernization/FULL-TEST-2026-10-03.md +49 -0
- package/docs/modernization/LEGACY-REMOVAL-PLAN.md +9 -0
- package/docs/modernization/PILOT-RESULT-2026-10-03.ko.md +85 -0
- package/docs/modernization/PILOT-UC-001.ko.md +79 -0
- package/docs/modernization/PLAN.md +28 -0
- package/docs/modernization/SKILLS.ko.md +105 -0
- package/docs/modernization/SPRINTABLE.ko.md +47 -0
- package/docs/modernization/SYNC-DOCS-VALIDATION-2026-10-04.ko.md +38 -0
- package/docs/modernization/VALIDATION.md +79 -0
- package/docs/modernization/evidence/example-review-packet.json +115 -0
- package/docs/modernization/evidence/example-spec-pin.json +48 -0
- package/docs/modernization/evidence/full-test-20261003/claude-lint.md +22 -0
- package/docs/modernization/evidence/full-test-20261003/claude-review-retry.md +56 -0
- package/docs/modernization/evidence/full-test-20261003/codex-review-packet.json +159 -0
- package/docs/modernization/evidence/full-test-20261003/src/expense.mjs +40 -0
- package/docs/modernization/evidence/full-test-20261003/summary.json +106 -0
- package/docs/modernization/evidence/full-test-20261003/tests/expense.test.mjs +61 -0
- package/docs/modernization/evidence/local-pilot-20261003/README.ko.md +40 -0
- package/docs/modernization/evidence/local-pilot-20261003/execution.json +1055 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/dependencies.json +24 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/dependencies.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/dependencies.stdout.txt +5 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/docs.json +24 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/docs.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/docs.stdout.txt +6 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/example.json +24 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/example.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/example.stdout.txt +5 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/local-docs.json +24 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/local-docs.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/local-docs.stdout.txt +6 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/node.json +23 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/node.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/node.stdout.txt +535 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/provenance.json +24 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/provenance.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/provenance.stdout.txt +5 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/readme-replay.json +24 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/readme-replay.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/readme-replay.stdout.txt +273 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/upstream.json +24 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/upstream.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/maintenance/upstream.stdout.txt +7 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/archive-service.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/archive-service.stdout.txt +148 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/archive-structure.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/archive-structure.stdout.txt +9 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-check.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-check.stdout.txt +67 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-evidence.json +34 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-packet.json +128 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-packet.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-packet.stdout.txt +128 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-pin.json +54 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-pin.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-pin.stdout.txt +60 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-readback.json +15 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-service.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-service.stdout.txt +82 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-structure.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/baseline-structure.stdout.txt +9 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-check.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-check.stdout.txt +79 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-packet-refused.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-packet-refused.stdout.txt +7 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-pin-refresh.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-pin-refresh.stdout.txt +66 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-pin.json +60 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-structure.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/changed-structure.stdout.txt +9 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-check.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-check.stdout.txt +73 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-evidence.json +34 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-packet.json +134 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-packet.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-packet.stdout.txt +134 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-readback.json +15 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-service.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-service.stdout.txt +148 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-structure.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/final-structure.stdout.txt +9 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/node-version.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/node-version.stdout.txt +1 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/python-version.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/python-version.stdout.txt +1 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-evidence.json +34 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-packet.json +134 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-packet.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-packet.stdout.txt +134 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-readback.json +15 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-service.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-service.stdout.txt +266 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-structure.stderr.txt +0 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/artifacts/red-structure.stdout.txt +9 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/docs/entity_model.md +33 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/docs/glossary.md +9 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/docs/plans/UC-001.md +20 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/docs/requirements.md +10 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/docs/test_cases/TC-001-submit-expense.md +37 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/docs/test_cases/TC-002-description-limit.md +42 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/docs/use_cases/UC-001-submit-expense.md +76 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/docs/use_cases.puml +12 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/docs/vision.md +21 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/src/expense.mjs +48 -0
- package/docs/modernization/evidence/local-pilot-20261003/project/tests/expense.test.mjs +116 -0
- package/docs/modernization/evidence/local-pilot-20261003/scope.json +31 -0
- package/docs/modernization/reviews/2026-10-03-claude-cli/CONTRACTS.ko.md +122 -0
- package/docs/modernization/reviews/2026-10-03-claude-cli/DIRECTION.ko.md +84 -0
- package/examples/spec-workflow/README.md +54 -0
- package/examples/spec-workflow/docs/entity_model.md +33 -0
- package/examples/spec-workflow/docs/glossary.md +9 -0
- package/examples/spec-workflow/docs/requirements.md +10 -0
- package/examples/spec-workflow/docs/test_cases/TC-001-submit-expense.md +37 -0
- package/examples/spec-workflow/docs/use_cases/UC-001-submit-expense.md +63 -0
- package/examples/spec-workflow/docs/use_cases.puml +12 -0
- package/examples/spec-workflow/docs/vision.md +21 -0
- package/package.json +37 -105
- package/plugins/aiwf-angular-jpa/.claude-plugin/plugin.json +11 -0
- package/plugins/aiwf-angular-jpa/LICENSE +201 -0
- package/plugins/aiwf-angular-jpa/NOTICE +16 -0
- package/plugins/aiwf-angular-jpa/README.md +54 -0
- package/plugins/aiwf-angular-jpa/UPSTREAM.json +35 -0
- package/plugins/aiwf-angular-jpa/agents/uc-coverage.md +263 -0
- package/plugins/aiwf-angular-jpa/rules/mcp-servers.md +64 -0
- package/plugins/aiwf-angular-jpa/skills/coverage-check/SKILL.md +190 -0
- package/plugins/aiwf-angular-jpa/skills/flyway-migration/SKILL.md +119 -0
- package/plugins/aiwf-angular-jpa/skills/implement/SKILL.md +528 -0
- package/plugins/aiwf-angular-jpa/skills/implement/references/module-layout.md +95 -0
- package/plugins/aiwf-angular-jpa/skills/playwright-test/SKILL.md +360 -0
- package/plugins/aiwf-angular-jpa/skills/spring-boot-test/SKILL.md +504 -0
- package/plugins/aiwf-angular-jpa/skills/vitest-test/SKILL.md +309 -0
- package/plugins/aiwf-blazor-dotnet/.claude-plugin/plugin.json +11 -0
- package/plugins/aiwf-blazor-dotnet/LICENSE +201 -0
- package/plugins/aiwf-blazor-dotnet/NOTICE +16 -0
- package/plugins/aiwf-blazor-dotnet/README.md +53 -0
- package/plugins/aiwf-blazor-dotnet/UPSTREAM.json +31 -0
- package/plugins/aiwf-blazor-dotnet/rules/mcp-servers.md +25 -0
- package/plugins/aiwf-blazor-dotnet/skills/bunit-test/SKILL.md +110 -0
- package/plugins/aiwf-blazor-dotnet/skills/dotnet-test/SKILL.md +108 -0
- package/plugins/aiwf-blazor-dotnet/skills/ef-migration/SKILL.md +43 -0
- package/plugins/aiwf-blazor-dotnet/skills/implement/SKILL.md +149 -0
- package/plugins/aiwf-blazor-dotnet/skills/playwright-test/SKILL.md +130 -0
- package/plugins/aiwf-core/.claude-plugin/plugin.json +11 -0
- package/plugins/aiwf-core/LICENSE +201 -0
- package/plugins/aiwf-core/NOTICE +20 -0
- package/plugins/aiwf-core/README.md +38 -0
- package/plugins/aiwf-core/UPSTREAM.json +50 -0
- package/plugins/aiwf-core/skills/entity-model/SKILL.md +160 -0
- package/plugins/aiwf-core/skills/entity-model/references/REFERENCE.md +36 -0
- package/plugins/aiwf-core/skills/requirements/SKILL.md +157 -0
- package/plugins/aiwf-core/skills/requirements/references/REFERENCE.md +70 -0
- package/plugins/aiwf-core/skills/requirements/references/glossary.md +7 -0
- package/plugins/aiwf-core/skills/reverse-engineer/SKILL.md +509 -0
- package/plugins/aiwf-core/skills/reverse-engineer/references/stack-signals.md +210 -0
- package/plugins/aiwf-core/skills/spec-review/SKILL.md +197 -0
- package/plugins/aiwf-core/skills/spec-review/references/lint-codes.md +63 -0
- package/plugins/aiwf-core/skills/spec-review/references/review-checklist.md +189 -0
- package/plugins/aiwf-core/skills/spec-review/scripts/spec_lint.py +1216 -0
- package/plugins/aiwf-core/skills/test-case/SKILL.md +138 -0
- package/plugins/aiwf-core/skills/test-case/references/example-process.bpmn +118 -0
- package/plugins/aiwf-core/skills/test-case/references/example.md +38 -0
- package/plugins/aiwf-core/skills/test-case/references/test-case.md +35 -0
- package/plugins/aiwf-core/skills/test-case/scripts/bpmn_paths.py +542 -0
- package/plugins/aiwf-core/skills/use-case-diagram/SKILL.md +99 -0
- package/plugins/aiwf-core/skills/use-case-spec/SKILL.md +257 -0
- package/plugins/aiwf-core/skills/use-case-spec/references/clarify-checklist.md +70 -0
- package/plugins/aiwf-core/skills/use-case-spec/references/example.md +88 -0
- package/plugins/aiwf-core/skills/use-case-spec/references/format-spec.md +246 -0
- package/plugins/aiwf-core/skills/use-case-spec/references/use-case.md +50 -0
- package/plugins/aiwf-core/skills/use-case-spec/scripts/validate_use_case.py +941 -0
- package/plugins/aiwf-delegate-claude/.claude-plugin/plugin.json +9 -0
- package/plugins/aiwf-delegate-claude/LICENSE +21 -0
- package/plugins/aiwf-delegate-claude/NOTICE +4 -0
- package/plugins/aiwf-delegate-claude/README.md +10 -0
- package/plugins/aiwf-delegate-claude/plugin.json +13 -0
- package/plugins/aiwf-delegate-claude/skills/delegate-claude/LICENSE +21 -0
- package/plugins/aiwf-delegate-claude/skills/delegate-claude/NOTICE +4 -0
- package/plugins/aiwf-delegate-claude/skills/delegate-claude/SKILL.md +49 -0
- package/plugins/aiwf-delegate-claude/skills/delegate-claude/agents/openai.yaml +2 -0
- package/plugins/aiwf-delegate-codex/.claude-plugin/plugin.json +9 -0
- package/plugins/aiwf-delegate-codex/LICENSE +21 -0
- package/plugins/aiwf-delegate-codex/NOTICE +4 -0
- package/plugins/aiwf-delegate-codex/README.md +10 -0
- package/plugins/aiwf-delegate-codex/plugin.json +13 -0
- package/plugins/aiwf-delegate-codex/skills/delegate-codex/LICENSE +21 -0
- package/plugins/aiwf-delegate-codex/skills/delegate-codex/NOTICE +4 -0
- package/plugins/aiwf-delegate-codex/skills/delegate-codex/SKILL.md +47 -0
- package/plugins/aiwf-delegate-codex/skills/delegate-codex/agents/openai.yaml +2 -0
- package/plugins/aiwf-nestjs-nextjs/.claude-plugin/plugin.json +11 -0
- package/plugins/aiwf-nestjs-nextjs/LICENSE +201 -0
- package/plugins/aiwf-nestjs-nextjs/NOTICE +16 -0
- package/plugins/aiwf-nestjs-nextjs/README.md +53 -0
- package/plugins/aiwf-nestjs-nextjs/UPSTREAM.json +32 -0
- package/plugins/aiwf-nestjs-nextjs/rules/mcp-servers.md +59 -0
- package/plugins/aiwf-nestjs-nextjs/skills/drizzle-migration/SKILL.md +222 -0
- package/plugins/aiwf-nestjs-nextjs/skills/implement/SKILL.md +393 -0
- package/plugins/aiwf-nestjs-nextjs/skills/implement/references/project-layout.md +158 -0
- package/plugins/aiwf-nestjs-nextjs/skills/nest-test/SKILL.md +300 -0
- package/plugins/aiwf-nestjs-nextjs/skills/playwright-test/SKILL.md +245 -0
- package/plugins/aiwf-nestjs-nextjs/skills/react-test/SKILL.md +217 -0
- package/plugins/aiwf-spec/.claude-plugin/plugin.json +9 -0
- package/plugins/aiwf-spec/LICENSE +201 -0
- package/plugins/aiwf-spec/NOTICE +20 -0
- package/plugins/aiwf-spec/README.md +36 -0
- package/plugins/aiwf-spec/skills/sync-docs/SKILL.md +47 -0
- package/plugins/aiwf-spec/skills/workflow/SKILL.md +42 -0
- package/plugins/aiwf-vaadin-jooq/.claude-plugin/plugin.json +11 -0
- package/plugins/aiwf-vaadin-jooq/LICENSE +201 -0
- package/plugins/aiwf-vaadin-jooq/NOTICE +16 -0
- package/plugins/aiwf-vaadin-jooq/README.md +56 -0
- package/plugins/aiwf-vaadin-jooq/UPSTREAM.json +43 -0
- package/plugins/aiwf-vaadin-jooq/agents/uc-coverage.md +260 -0
- package/plugins/aiwf-vaadin-jooq/rules/mcp-servers.md +44 -0
- package/plugins/aiwf-vaadin-jooq/skills/browserless-test/SKILL.md +407 -0
- package/plugins/aiwf-vaadin-jooq/skills/browserless-test/references/UC001ManagePersonsTest.java +111 -0
- package/plugins/aiwf-vaadin-jooq/skills/coverage-check/SKILL.md +190 -0
- package/plugins/aiwf-vaadin-jooq/skills/flyway-migration/SKILL.md +70 -0
- package/plugins/aiwf-vaadin-jooq/skills/hilla-test/SKILL.md +350 -0
- package/plugins/aiwf-vaadin-jooq/skills/hilla-test/references/UC001ManagePersonsServiceTest.java +81 -0
- package/plugins/aiwf-vaadin-jooq/skills/hilla-test/references/UC001ManagePersonsViewTest.tsx +87 -0
- package/plugins/aiwf-vaadin-jooq/skills/implement/SKILL.md +196 -0
- package/plugins/aiwf-vaadin-jooq/skills/implement-hilla/SKILL.md +216 -0
- package/plugins/aiwf-vaadin-jooq/skills/karibu-test/SKILL.md +298 -0
- package/plugins/aiwf-vaadin-jooq/skills/karibu-test/references/UC001ManagePersonsTest.java +93 -0
- package/plugins/aiwf-vaadin-jooq/skills/playwright-test/SKILL.md +237 -0
- package/plugins/aiwf-vaadin-jooq/skills/playwright-test/references/ExampleViewIT.java +143 -0
- package/plugins/aiwf-vaadin-jooq/skills/playwright-test/references/TC001CustomerOnboardingIT.java +144 -0
- package/plugins/aiwf-vaadin-jooq/skills/playwright-test/references/dramafinder-api.md +190 -0
- package/scripts/check-dependencies.js +26 -96
- package/scripts/install-spec-skills.mjs +126 -0
- package/scripts/validate-spec-plugin.mjs +145 -0
- package/src/cli/spec-cli.js +292 -0
- package/src/lib/spec-workflow.js +982 -0
- package/docs/ADR_MANAGEMENT_GUIDE.ko.md +0 -602
- package/docs/ADR_MANAGEMENT_GUIDE.md +0 -602
- package/docs/AI-WORKFLOW.ko.md +0 -299
- package/docs/AI-WORKFLOW.md +0 -401
- package/docs/API_REFERENCE_FULL.ko.md +0 -1135
- package/docs/API_REFERENCE_FULL.md +0 -1135
- package/docs/ARCHITECTURE.ko.md +0 -314
- package/docs/ARCHITECTURE.md +0 -314
- package/docs/CLI_USAGE_GUIDE.ko.md +0 -634
- package/docs/CLI_USAGE_GUIDE.md +0 -640
- package/docs/CODE_CLEANUP_GUIDE.ko.md +0 -415
- package/docs/CODE_CLEANUP_GUIDE.md +0 -415
- package/docs/COMMANDS_GUIDE.ko.md +0 -1037
- package/docs/COMMANDS_GUIDE.md +0 -1037
- package/docs/CONTRIBUTING.ko.md +0 -408
- package/docs/CONTRIBUTING.md +0 -408
- package/docs/DEVELOPMENT_GUIDE.ko.md +0 -440
- package/docs/DEVELOPMENT_GUIDE.md +0 -727
- package/docs/EXAMPLES.ko.md +0 -695
- package/docs/EXAMPLES.md +0 -693
- package/docs/GETTING_STARTED.ko.md +0 -219
- package/docs/GETTING_STARTED.md +0 -476
- package/docs/MODULE_MANAGEMENT_GUIDE.ko.md +0 -289
- package/docs/MODULE_MANAGEMENT_GUIDE.md +0 -289
- package/docs/PERFORMANCE_ARCHITECTURE.md +0 -494
- package/docs/PERFORMANCE_GUIDELINES.ko.md +0 -388
- package/docs/PERFORMANCE_GUIDELINES.md +0 -553
- package/docs/PRD.ko.md +0 -148
- package/docs/PRD.md +0 -150
- package/docs/ROADMAP_v0.4.0.md +0 -286
- package/docs/STATE_MANAGEMENT_GUIDE.ko.md +0 -278
- package/docs/STATE_MANAGEMENT_GUIDE.md +0 -278
- package/docs/TROUBLESHOOTING.ko.md +0 -366
- package/docs/TROUBLESHOOTING.md +0 -722
- package/docs/VALIDATOR_API.ko.md +0 -324
- package/docs/VALIDATOR_API.md +0 -324
- package/docs/YOLO_SYSTEM_GUIDE.ko.md +0 -542
- package/docs/YOLO_SYSTEM_GUIDE.md +0 -542
- package/docs/designs/AI_PERSONA_SYSTEM_DESIGN.md +0 -516
- package/docs/designs/API_DOCUMENTATION.md +0 -932
- package/docs/designs/API_REFERENCE.md +0 -979
- package/docs/designs/Enhanced_Installation_Flow_Design.md +0 -498
- package/docs/designs/aiwf-metadata-system-prd.md +0 -127
- package/docs/designs/offline-template-cache.md +0 -323
- package/docs/designs/persona-aware-compression.md +0 -168
- package/docs/guides/ai-personas-guide-ko.md +0 -239
- package/docs/guides/ai-personas-guide.md +0 -239
- package/docs/guides/checkpoint-system-guide-ko.md +0 -356
- package/docs/guides/checkpoint-system-guide.md +0 -356
- package/docs/guides/context-compression-guide-ko.md +0 -313
- package/docs/guides/context-compression-guide.md +0 -313
- package/docs/guides/independent-sprint-guide-ko.md +0 -321
- package/docs/guides/independent-sprint-guide.md +0 -321
- package/rules/global/aiwf-code-style-guide.md +0 -30
- package/rules/global/aiwf-coding-principles.md +0 -33
- package/rules/global/aiwf-development-process.md +0 -41
- package/rules/global/aiwf-global-rules.md +0 -84
- package/rules/manual/aiwf-generate-plan-docs.md +0 -280
- package/scripts/run-integration-tests.js +0 -317
- package/scripts/update-file-lists.js +0 -267
- package/scripts/validate-commands.js +0 -254
- package/src/cli/index.js +0 -184
- package/src/commands/sprint-independent.js +0 -393
- package/src/commands/state.js +0 -1164
- package/src/commands/yolo-config.js +0 -502
- package/src/config/file-lists.js +0 -147
- package/src/config/yolo-config-template.yaml +0 -168
- package/src/lib/backup-manager.js +0 -271
- package/src/lib/file-downloader.js +0 -304
- package/src/lib/installer.js +0 -1270
- package/src/lib/rollback-manager.js +0 -418
- package/src/lib/validator.js +0 -376
- package/src/utils/checkpoint-manager.js +0 -435
- package/src/utils/language-utils.js +0 -331
- package/src/utils/messages.js +0 -190
- 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
|
+
}
|