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.
- 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 -104
- 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/{src/lib/resources/templates/npm-library/template → plugins/aiwf-delegate-claude}/LICENSE +2 -2
- 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/DEPENDENCY_MAP.md +0 -90
- package/src/cli/cache-cli.js +0 -459
- package/src/cli/checkpoint-cli.js +0 -417
- package/src/cli/index.js +0 -481
- package/src/cli/language-cli.js +0 -286
- package/src/cli/sprint-cli.js +0 -287
- package/src/commands/ai-tool.js +0 -383
- package/src/commands/compress.js +0 -60
- package/src/commands/create-project.js +0 -160
- package/src/commands/evaluate.js +0 -459
- package/src/commands/persona.js +0 -309
- package/src/commands/sprint-independent.js +0 -393
- package/src/commands/sprint-task.js +0 -262
- package/src/commands/state.js +0 -1164
- package/src/commands/token.js +0 -312
- 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/ai-persona-manager.js +0 -711
- package/src/lib/backup-manager.js +0 -271
- package/src/lib/cache-system.js +0 -332
- package/src/lib/context-engine.js +0 -568
- package/src/lib/file-downloader.js +0 -304
- package/src/lib/github-integration.js +0 -402
- package/src/lib/installer.js +0 -1066
- package/src/lib/memory-profiler.js +0 -471
- package/src/lib/metrics-collector.js +0 -885
- package/src/lib/offline-detector.js +0 -386
- package/src/lib/resource-loader-enhanced.js +0 -399
- package/src/lib/resource-loader.js +0 -250
- package/src/lib/resources/commands/ai-persona.js +0 -602
- package/src/lib/resources/commands/compress-context.js +0 -389
- package/src/lib/resources/commands/evaluate.js +0 -246
- package/src/lib/resources/commands/persona-context-apply.js +0 -562
- package/src/lib/resources/commands/token-tracking.js +0 -443
- package/src/lib/resources/config/commit-patterns.js +0 -103
- package/src/lib/resources/config/language.json +0 -6
- package/src/lib/resources/personas/PERSONA_INDEX.md +0 -57
- package/src/lib/resources/personas/analyst.json +0 -44
- package/src/lib/resources/personas/architect/best_practices.md +0 -136
- package/src/lib/resources/personas/architect/knowledge_base.md +0 -377
- package/src/lib/resources/personas/architect.json +0 -44
- package/src/lib/resources/personas/backend/best_practices.md +0 -766
- package/src/lib/resources/personas/backend/knowledge_base.md +0 -1070
- package/src/lib/resources/personas/data_analyst/best_practices.md +0 -594
- package/src/lib/resources/personas/data_analyst/knowledge_base.md +0 -1057
- package/src/lib/resources/personas/developer.json +0 -44
- package/src/lib/resources/personas/developer.md +0 -37
- package/src/lib/resources/personas/evaluation_criteria.json +0 -137
- package/src/lib/resources/personas/frontend/best_practices.md +0 -445
- package/src/lib/resources/personas/frontend/knowledge_base.md +0 -729
- package/src/lib/resources/personas/persona-index.json +0 -31
- package/src/lib/resources/personas/reviewer.json +0 -44
- package/src/lib/resources/personas/security/best_practices.md +0 -307
- package/src/lib/resources/personas/security/knowledge_base.md +0 -498
- package/src/lib/resources/personas/tester.json +0 -44
- package/src/lib/resources/templates/README.md +0 -78
- package/src/lib/resources/templates/api-server/config.json +0 -64
- package/src/lib/resources/templates/api-server/template/.aiwf/config.json +0 -56
- package/src/lib/resources/templates/api-server/template/.aiwf/feature-ledger.json +0 -42
- package/src/lib/resources/templates/api-server/template/.aiwf/personas/backend-engineer.json +0 -40
- package/src/lib/resources/templates/api-server/template/.aiwf/scripts/cli.js +0 -111
- package/src/lib/resources/templates/api-server/template/.env.example +0 -29
- package/src/lib/resources/templates/api-server/template/.eslintrc.json +0 -23
- package/src/lib/resources/templates/api-server/template/README.md +0 -171
- package/src/lib/resources/templates/api-server/template/jest.config.js +0 -26
- package/src/lib/resources/templates/api-server/template/nodemon.json +0 -9
- package/src/lib/resources/templates/api-server/template/package.json +0 -61
- package/src/lib/resources/templates/api-server/template/src/app.ts +0 -60
- package/src/lib/resources/templates/api-server/template/src/config/swagger.ts +0 -38
- package/src/lib/resources/templates/api-server/template/src/controllers/aiwfController.ts +0 -54
- package/src/lib/resources/templates/api-server/template/src/controllers/authController.ts +0 -128
- package/src/lib/resources/templates/api-server/template/src/controllers/statusController.ts +0 -28
- package/src/lib/resources/templates/api-server/template/src/index.ts +0 -28
- package/src/lib/resources/templates/api-server/template/src/middleware/aiwfMiddleware.ts +0 -50
- package/src/lib/resources/templates/api-server/template/src/middleware/authMiddleware.ts +0 -41
- package/src/lib/resources/templates/api-server/template/src/middleware/errorHandler.ts +0 -42
- package/src/lib/resources/templates/api-server/template/src/middleware/notFoundHandler.ts +0 -14
- package/src/lib/resources/templates/api-server/template/src/middleware/requestLogger.ts +0 -20
- package/src/lib/resources/templates/api-server/template/src/routes/aiwf.ts +0 -31
- package/src/lib/resources/templates/api-server/template/src/routes/index.ts +0 -13
- package/src/lib/resources/templates/api-server/template/src/routes/v1/index.ts +0 -67
- package/src/lib/resources/templates/api-server/template/src/utils/logger.ts +0 -37
- package/src/lib/resources/templates/api-server/template/tests/app.test.ts +0 -40
- package/src/lib/resources/templates/api-server/template/tsconfig.json +0 -43
- package/src/lib/resources/templates/npm-library/config.json +0 -61
- package/src/lib/resources/templates/npm-library/template/README.md +0 -201
- package/src/lib/resources/templates/npm-library/template/package.json +0 -78
- package/src/lib/resources/templates/web-app/config.json +0 -53
- package/src/lib/resources/templates/web-app/template/.aiwf/config.json +0 -54
- package/src/lib/resources/templates/web-app/template/.aiwf/feature-ledger.json +0 -37
- package/src/lib/resources/templates/web-app/template/.aiwf/personas/fullstack-developer.json +0 -40
- package/src/lib/resources/templates/web-app/template/.aiwf/scripts/cli.js +0 -110
- package/src/lib/resources/templates/web-app/template/.eslintrc.cjs +0 -20
- package/src/lib/resources/templates/web-app/template/README.md +0 -151
- package/src/lib/resources/templates/web-app/template/index.html +0 -14
- package/src/lib/resources/templates/web-app/template/package.json +0 -44
- package/src/lib/resources/templates/web-app/template/postcss.config.js +0 -6
- package/src/lib/resources/templates/web-app/template/public/vite.svg +0 -1
- package/src/lib/resources/templates/web-app/template/src/App.tsx +0 -21
- package/src/lib/resources/templates/web-app/template/src/components/Layout.tsx +0 -57
- package/src/lib/resources/templates/web-app/template/src/components/aiwf/ContextStatus.tsx +0 -107
- package/src/lib/resources/templates/web-app/template/src/components/aiwf/TokenUsage.tsx +0 -71
- package/src/lib/resources/templates/web-app/template/src/index.css +0 -60
- package/src/lib/resources/templates/web-app/template/src/main.tsx +0 -10
- package/src/lib/resources/templates/web-app/template/src/pages/AiwfDashboard.tsx +0 -57
- package/src/lib/resources/templates/web-app/template/src/pages/HomePage.tsx +0 -89
- package/src/lib/resources/templates/web-app/template/src/pages/NotFound.tsx +0 -25
- package/src/lib/resources/templates/web-app/template/src/stores/aiwfStore.ts +0 -126
- package/src/lib/resources/templates/web-app/template/src/types/global.d.ts +0 -9
- package/src/lib/resources/templates/web-app/template/src/vite-env.d.ts +0 -1
- package/src/lib/resources/templates/web-app/template/tailwind.config.js +0 -30
- package/src/lib/resources/templates/web-app/template/tsconfig.json +0 -37
- package/src/lib/resources/templates/web-app/template/tsconfig.node.json +0 -10
- package/src/lib/resources/templates/web-app/template/vite.config.ts +0 -23
- package/src/lib/resources/utils/background-monitor.js +0 -223
- package/src/lib/resources/utils/compression-metrics.js +0 -1031
- package/src/lib/resources/utils/compression-strategies.js +0 -557
- package/src/lib/resources/utils/content-normalizer.js +0 -455
- package/src/lib/resources/utils/context-compressor.js +0 -583
- package/src/lib/resources/utils/context-rule-parser.js +0 -180
- package/src/lib/resources/utils/context-token-monitor.js +0 -463
- package/src/lib/resources/utils/context-update-manager.js +0 -502
- package/src/lib/resources/utils/git-utils.js +0 -220
- package/src/lib/resources/utils/importance-classifier.js +0 -645
- package/src/lib/resources/utils/information-filter.js +0 -710
- package/src/lib/resources/utils/persona-aware-compressor.js +0 -598
- package/src/lib/resources/utils/prompt-injector.js +0 -242
- package/src/lib/resources/utils/simplified-evaluator.js +0 -119
- package/src/lib/resources/utils/text-summarizer.js +0 -492
- package/src/lib/resources/utils/token-counter.js +0 -177
- package/src/lib/resources/utils/token-monitor.js +0 -484
- package/src/lib/resources/utils/token-reporter.js +0 -452
- package/src/lib/resources/utils/token-storage.js +0 -433
- package/src/lib/resources/utils/token-tracker.js +0 -283
- package/src/lib/rollback-manager.js +0 -418
- package/src/lib/state/priority-calculator.js +0 -217
- package/src/lib/state/state-index.js +0 -155
- package/src/lib/state/task-scanner.js +0 -328
- package/src/lib/task-analyzer.js +0 -599
- package/src/lib/template-cache-system.js +0 -559
- package/src/lib/template-downloader.js +0 -467
- package/src/lib/template-version-manager.js +0 -491
- package/src/lib/token-optimizer.js +0 -517
- package/src/lib/validator.js +0 -376
- package/src/utils/checkpoint-manager.js +0 -435
- package/src/utils/engineering-guard.js +0 -399
- package/src/utils/language-utils.js +0 -331
- package/src/utils/messages.js +0 -166
- package/src/utils/paths.js +0 -112
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025-2026 Simon Martinelli and the AI Unified Process contributors.
|
|
4
|
+
# Part of the AI Unified Process — https://unifiedprocess.ai
|
|
5
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
|
|
6
|
+
"""Validate AI Unified Process use case specification documents.
|
|
7
|
+
|
|
8
|
+
Checks UC-*.md files against the normative format described in
|
|
9
|
+
references/format-spec.md. The grammar and every tolerance mirror the
|
|
10
|
+
Studio structured editor's parser (UseCaseSpecificationDocument.java),
|
|
11
|
+
which is the executable source of truth for what "parses":
|
|
12
|
+
|
|
13
|
+
- ERROR = the Studio structured editor cannot read the document
|
|
14
|
+
(it would open read-only in the plain markdown editor).
|
|
15
|
+
- WARN = Studio tolerates it, but it violates the use-case-spec skill
|
|
16
|
+
contract or is silently rewritten/flattened on save in Studio.
|
|
17
|
+
|
|
18
|
+
Exit code 0 when clean, 1 when any ERROR was found (with --strict also
|
|
19
|
+
when any WARN was found), 2 on usage errors.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
validate_use_case.py [--strict] [--quiet] FILE...
|
|
23
|
+
validate_use_case.py --self-test
|
|
24
|
+
|
|
25
|
+
Requires Python 3.9+, standard library only.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Language definitions (UseCaseLanguage.java)
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
LANGUAGES = {
|
|
38
|
+
"en": {
|
|
39
|
+
"headings": {
|
|
40
|
+
"overview": "## Overview",
|
|
41
|
+
"preconditions": "## Preconditions",
|
|
42
|
+
"main_scenario": "## Main Success Scenario",
|
|
43
|
+
"alternative_flows": "## Alternative Flows",
|
|
44
|
+
"postconditions": "## Postconditions",
|
|
45
|
+
"success": "### Success Postconditions",
|
|
46
|
+
"failure": "### Failure Postconditions",
|
|
47
|
+
"business_rules": "## Business Rules",
|
|
48
|
+
},
|
|
49
|
+
"fields": {
|
|
50
|
+
"id": "**Use Case ID:**",
|
|
51
|
+
"name": "**Use Case Name:**",
|
|
52
|
+
"actor": "**Primary Actor:**",
|
|
53
|
+
"secondary_actors": "**Secondary Actors:**",
|
|
54
|
+
"goal": "**Goal:**",
|
|
55
|
+
"requirements": "**Requirements:**",
|
|
56
|
+
"uc_trigger": "**Trigger:**",
|
|
57
|
+
"trigger": "**Trigger:**",
|
|
58
|
+
"flow": "**Flow:**",
|
|
59
|
+
},
|
|
60
|
+
"uc_trigger_aliases": ["**Trigger:**"],
|
|
61
|
+
"trigger_aliases": ["**Trigger:**"],
|
|
62
|
+
"rule_prefix": "BR",
|
|
63
|
+
},
|
|
64
|
+
"de": {
|
|
65
|
+
"headings": {
|
|
66
|
+
"overview": "## Übersicht",
|
|
67
|
+
"preconditions": "## Vorbedingungen",
|
|
68
|
+
"main_scenario": "## Hauptablauf",
|
|
69
|
+
"alternative_flows": "## Alternativabläufe",
|
|
70
|
+
"postconditions": "## Nachbedingungen",
|
|
71
|
+
"success": "### Erfolgsfall",
|
|
72
|
+
"failure": "### Fehlerfall",
|
|
73
|
+
"business_rules": "## Geschäftsregeln",
|
|
74
|
+
},
|
|
75
|
+
"fields": {
|
|
76
|
+
"id": "**Use-Case-ID:**",
|
|
77
|
+
"name": "**Use-Case-Name:**",
|
|
78
|
+
"actor": "**Primärer Akteur:**",
|
|
79
|
+
"secondary_actors": "**Sekundäre Akteure:**",
|
|
80
|
+
"goal": "**Ziel:**",
|
|
81
|
+
"requirements": "**Anforderungen:**",
|
|
82
|
+
# distinct from the alternative-flow label "Auslöser"
|
|
83
|
+
"uc_trigger": "**Auslösendes Ereignis:**",
|
|
84
|
+
"trigger": "**Auslöser:**",
|
|
85
|
+
"flow": "**Ablauf:**",
|
|
86
|
+
},
|
|
87
|
+
"uc_trigger_aliases": ["**Auslösendes Ereignis:**", "**Trigger:**"],
|
|
88
|
+
# German documents in the wild often keep the English "Trigger" label
|
|
89
|
+
"trigger_aliases": ["**Auslöser:**", "**Trigger:**"],
|
|
90
|
+
"rule_prefix": "GR",
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
TITLE_PREFIX = "# Use Case:"
|
|
95
|
+
STATUS_FIELD = "**Status:**"
|
|
96
|
+
|
|
97
|
+
# UseCaseStatus.java: English enum names and German values, matched
|
|
98
|
+
# case-insensitively after stripping non-letter decoration.
|
|
99
|
+
STATUS_VALUES = [
|
|
100
|
+
"Draft", "Reviewed", "Approved", "Implemented", "Tested", "Done",
|
|
101
|
+
"Obsolete",
|
|
102
|
+
"Entwurf", "Geprüft", "Genehmigt", "Implementiert", "Getestet",
|
|
103
|
+
"Abgeschlossen", "Obsolet",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
NUMBERED_ITEM = re.compile(r"(\d+)\.\s+(.*)")
|
|
107
|
+
ID_TITLE = re.compile(r"#\s+[SB]?UC-[A-Za-z0-9_-]+\s*:.*")
|
|
108
|
+
FLOW_LABEL = re.compile(r"A\d+\s*:\s*(.*)")
|
|
109
|
+
# Hyphenated tails like BR-USER-050 are valid Navigator-style rule ids.
|
|
110
|
+
RULE_LABEL = re.compile(r"(?:BR|GR)-[A-Za-z0-9_-]+\s*:\s*.*")
|
|
111
|
+
RULE_NUMBER = re.compile(r"(?:BR|GR)-(\d+)\s*:")
|
|
112
|
+
PLACEHOLDER = re.compile(r"_[^_].*_|\*[^*].*\*")
|
|
113
|
+
UC_ID_GRAMMAR = re.compile(r"[SB]?UC-[A-Za-z0-9_-]+")
|
|
114
|
+
|
|
115
|
+
STEP_REFERENCE = re.compile(r"\((?:step|schritt)\s*\d", re.IGNORECASE)
|
|
116
|
+
FLOW_TERMINATION = re.compile(
|
|
117
|
+
r"continues at step \d+|use case ends"
|
|
118
|
+
r"|wird bei schritt \d+ fortgesetzt|use case endet",
|
|
119
|
+
re.IGNORECASE,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Implementation-level terms banned from scenario and flow steps by the
|
|
123
|
+
# use-case-spec skill ("Step writing guidelines").
|
|
124
|
+
TECHNICAL_TERMS = [
|
|
125
|
+
(re.compile(r"\bSMTP\b", re.IGNORECASE), "SMTP"),
|
|
126
|
+
(re.compile(r"\bemail server\b", re.IGNORECASE), "email server"),
|
|
127
|
+
(re.compile(r"\bJWT\b", re.IGNORECASE), "JWT"),
|
|
128
|
+
(re.compile(r"\bbcrypt\b", re.IGNORECASE), "bcrypt"),
|
|
129
|
+
(re.compile(r"\bhash(?:es|ed|ing)?\b", re.IGNORECASE), "hash"),
|
|
130
|
+
(re.compile(r"\bsalt(?:ed)?\b", re.IGNORECASE), "salt"),
|
|
131
|
+
(re.compile(r"\btokens?\b", re.IGNORECASE), "token"),
|
|
132
|
+
(re.compile(r"\bSHA-?\d*\b"), "SHA"),
|
|
133
|
+
(re.compile(r"\bSQL\b"), "SQL"),
|
|
134
|
+
(re.compile(r"\bSELECT\b"), "SELECT"),
|
|
135
|
+
(re.compile(r"\bINSERT\b"), "INSERT"),
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
ERROR = "ERROR"
|
|
139
|
+
WARN = "WARN"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Problem:
|
|
143
|
+
def __init__(self, line, severity, code, message):
|
|
144
|
+
self.line = line
|
|
145
|
+
self.severity = severity
|
|
146
|
+
self.code = code
|
|
147
|
+
self.message = message
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Document:
|
|
151
|
+
"""The structured content collected while validating (for warn checks)."""
|
|
152
|
+
|
|
153
|
+
def __init__(self):
|
|
154
|
+
self.language = "en"
|
|
155
|
+
self.overview = {}
|
|
156
|
+
self.overview_seen = False
|
|
157
|
+
self.sections_seen = set()
|
|
158
|
+
self.preconditions = []
|
|
159
|
+
self.main_scenario = []
|
|
160
|
+
self.main_scenario_numbers = []
|
|
161
|
+
self.main_scenario_placeholder = False
|
|
162
|
+
self.flows = [] # dicts: heading, line, trigger, steps
|
|
163
|
+
self.flows_placeholder = False
|
|
164
|
+
self.success_post = []
|
|
165
|
+
self.success_placeholder = False
|
|
166
|
+
self.failure_post = []
|
|
167
|
+
self.failure_placeholder = False
|
|
168
|
+
self.rules = [] # dicts: heading, line
|
|
169
|
+
self.problems = []
|
|
170
|
+
|
|
171
|
+
def add(self, line, severity, code, message):
|
|
172
|
+
self.problems.append(Problem(line, severity, code, message))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# Low-level helpers (mirroring UseCaseSpecificationDocument.java)
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
def detect_language(lines):
|
|
180
|
+
scores = {"en": 0, "de": 0}
|
|
181
|
+
for raw in lines:
|
|
182
|
+
line = raw.strip()
|
|
183
|
+
for key, lang in LANGUAGES.items():
|
|
184
|
+
if line in lang["headings"].values():
|
|
185
|
+
scores[key] += 1
|
|
186
|
+
continue
|
|
187
|
+
markers = list(lang["fields"].values())
|
|
188
|
+
if any(line.startswith(field) for field in markers):
|
|
189
|
+
scores[key] += 1
|
|
190
|
+
return "de" if scores["de"] > scores["en"] else "en"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def parse_status(value):
|
|
194
|
+
"""UseCaseStatus.parse: decoration before/after the value is ignored."""
|
|
195
|
+
start, end = 0, len(value)
|
|
196
|
+
while start < end and not value[start].isalpha():
|
|
197
|
+
start += 1
|
|
198
|
+
while end > start and not value[end - 1].isalpha():
|
|
199
|
+
end -= 1
|
|
200
|
+
candidate = value[start:end]
|
|
201
|
+
for status in STATUS_VALUES:
|
|
202
|
+
if candidate.lower().startswith(status.lower()):
|
|
203
|
+
if len(candidate) == len(status) or not candidate[len(status)].isalpha():
|
|
204
|
+
return status
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def skip_blank(lines, index):
|
|
209
|
+
while index < len(lines) and not lines[index].strip():
|
|
210
|
+
index += 1
|
|
211
|
+
return index
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def is_continuation(line):
|
|
215
|
+
if not line.strip() or line.startswith("#") or line.startswith("**") \
|
|
216
|
+
or line.startswith("- "):
|
|
217
|
+
return False
|
|
218
|
+
return not NUMBERED_ITEM.fullmatch(line.strip()) or line.startswith(" ")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def join_continuation(doc, lines, index, parts):
|
|
222
|
+
"""Joins wrapped lines into the current item; a joined line that itself
|
|
223
|
+
looks like a nested list item is flattened by Studio on save (WARN)."""
|
|
224
|
+
while index < len(lines) and is_continuation(lines[index]):
|
|
225
|
+
stripped = lines[index].strip()
|
|
226
|
+
if stripped.startswith(("- ", "* ")) or NUMBERED_ITEM.fullmatch(stripped):
|
|
227
|
+
doc.add(index + 1, WARN, "SUBBULLET_FLATTENED",
|
|
228
|
+
"nested list item is joined into the line above when "
|
|
229
|
+
"Studio saves the document: " + stripped)
|
|
230
|
+
parts.append(stripped)
|
|
231
|
+
index += 1
|
|
232
|
+
return index
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def is_placeholder_start(line):
|
|
236
|
+
return line.strip().startswith(("_", "*"))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def read_placeholder(doc, lines, index, placeholders):
|
|
240
|
+
parts = [lines[index].strip()]
|
|
241
|
+
nxt = join_continuation(doc, lines, index + 1, parts)
|
|
242
|
+
text = " ".join(parts)
|
|
243
|
+
if PLACEHOLDER.fullmatch(text):
|
|
244
|
+
placeholders.append((index + 1, text))
|
|
245
|
+
else:
|
|
246
|
+
doc.add(index + 1, ERROR, "UNEXPECTED_CONTENT", text)
|
|
247
|
+
return nxt
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def flush_placeholders(doc, placeholders, section_empty):
|
|
251
|
+
"""An italic paragraph standing in for an empty section is data; next to
|
|
252
|
+
real content it is unexpected."""
|
|
253
|
+
for line, text in placeholders:
|
|
254
|
+
if not section_empty:
|
|
255
|
+
doc.add(line, ERROR, "UNEXPECTED_CONTENT", text)
|
|
256
|
+
return bool(placeholders) and section_empty
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# Section parsers
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def parse_title(doc, lines):
|
|
264
|
+
index = skip_blank(lines, 0)
|
|
265
|
+
if index < len(lines) and (lines[index].startswith(TITLE_PREFIX)
|
|
266
|
+
or ID_TITLE.fullmatch(lines[index])):
|
|
267
|
+
return index + 1
|
|
268
|
+
found = lines[index] if index < len(lines) else ""
|
|
269
|
+
doc.add(index + 1, ERROR, "TITLE_MISSING",
|
|
270
|
+
"expected '# Use Case: <name>' (or '# UC-XXX: <name>') as the "
|
|
271
|
+
"first line, found: " + (found or "<end of file>"))
|
|
272
|
+
return index
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def parse_overview(doc, lines, index):
|
|
276
|
+
lang = LANGUAGES[doc.language]
|
|
277
|
+
fields = lang["fields"]
|
|
278
|
+
overview = doc.overview
|
|
279
|
+
while index < len(lines) and not lines[index].startswith("## "):
|
|
280
|
+
line = lines[index]
|
|
281
|
+
if not line.strip():
|
|
282
|
+
index += 1
|
|
283
|
+
elif line.startswith(fields["id"]):
|
|
284
|
+
overview["id"] = line[len(fields["id"]):].strip()
|
|
285
|
+
index += 1
|
|
286
|
+
elif line.startswith(fields["name"]):
|
|
287
|
+
overview["name"] = line[len(fields["name"]):].strip()
|
|
288
|
+
index += 1
|
|
289
|
+
elif line.startswith(fields["actor"]):
|
|
290
|
+
overview["actor"] = line[len(fields["actor"]):].strip()
|
|
291
|
+
index += 1
|
|
292
|
+
elif line.startswith(fields["secondary_actors"]):
|
|
293
|
+
overview["secondary_actors"] = \
|
|
294
|
+
line[len(fields["secondary_actors"]):].strip()
|
|
295
|
+
index += 1
|
|
296
|
+
elif line.startswith(fields["goal"]):
|
|
297
|
+
parts = [line[len(fields["goal"]):].strip()]
|
|
298
|
+
index = join_continuation(doc, lines, index + 1, parts)
|
|
299
|
+
overview["goal"] = " ".join(parts)
|
|
300
|
+
elif line.startswith(STATUS_FIELD):
|
|
301
|
+
parts = [line[len(STATUS_FIELD):].strip()]
|
|
302
|
+
status_line = index + 1
|
|
303
|
+
index = join_continuation(doc, lines, index + 1, parts)
|
|
304
|
+
overview["status"] = " ".join(parts)
|
|
305
|
+
overview["status_line"] = status_line
|
|
306
|
+
elif line.startswith(fields["requirements"]):
|
|
307
|
+
overview["requirements"] = \
|
|
308
|
+
line[len(fields["requirements"]):].strip()
|
|
309
|
+
index += 1
|
|
310
|
+
elif any(line.startswith(a) for a in lang["uc_trigger_aliases"]):
|
|
311
|
+
alias = next(a for a in lang["uc_trigger_aliases"]
|
|
312
|
+
if line.startswith(a))
|
|
313
|
+
parts = [line[len(alias):].strip()]
|
|
314
|
+
overview["trigger_line"] = index + 1
|
|
315
|
+
index = join_continuation(doc, lines, index + 1, parts)
|
|
316
|
+
overview["trigger"] = " ".join(parts)
|
|
317
|
+
else:
|
|
318
|
+
# unknown overview lines are kept verbatim by Studio (pass-through)
|
|
319
|
+
index += 1
|
|
320
|
+
for key in ("id", "name", "actor", "goal", "status"):
|
|
321
|
+
if key not in overview:
|
|
322
|
+
label = STATUS_FIELD if key == "status" else fields[key]
|
|
323
|
+
doc.add(0, ERROR, "FIELD_MISSING",
|
|
324
|
+
"mandatory overview field missing: " + label)
|
|
325
|
+
return index
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def parse_items(doc, lines, index, items):
|
|
329
|
+
"""A bullet list ('- '); returns (index, placeholder_present)."""
|
|
330
|
+
placeholders = []
|
|
331
|
+
while index < len(lines) and not lines[index].startswith("#"):
|
|
332
|
+
line = lines[index]
|
|
333
|
+
if not line.strip():
|
|
334
|
+
index += 1
|
|
335
|
+
elif line.startswith("- "):
|
|
336
|
+
parts = [line[2:].strip()]
|
|
337
|
+
index = join_continuation(doc, lines, index + 1, parts)
|
|
338
|
+
items.append(" ".join(parts))
|
|
339
|
+
elif is_placeholder_start(line):
|
|
340
|
+
index = read_placeholder(doc, lines, index, placeholders)
|
|
341
|
+
else:
|
|
342
|
+
doc.add(index + 1, ERROR, "UNEXPECTED_CONTENT", line.strip())
|
|
343
|
+
index += 1
|
|
344
|
+
return index, flush_placeholders(doc, placeholders, not items)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def parse_numbered_items(doc, lines, index, items, numbers):
|
|
348
|
+
placeholders = []
|
|
349
|
+
while index < len(lines) and not lines[index].startswith("#"):
|
|
350
|
+
line = lines[index]
|
|
351
|
+
if not line.strip():
|
|
352
|
+
index += 1
|
|
353
|
+
continue
|
|
354
|
+
match = NUMBERED_ITEM.fullmatch(line.strip())
|
|
355
|
+
if match and not line.startswith(" "):
|
|
356
|
+
numbers.append(int(match.group(1)))
|
|
357
|
+
item_line = index + 1
|
|
358
|
+
parts = [match.group(2).strip()]
|
|
359
|
+
index = join_continuation(doc, lines, index + 1, parts)
|
|
360
|
+
items.append((item_line, " ".join(parts)))
|
|
361
|
+
elif is_placeholder_start(line):
|
|
362
|
+
index = read_placeholder(doc, lines, index, placeholders)
|
|
363
|
+
else:
|
|
364
|
+
doc.add(index + 1, ERROR, "UNEXPECTED_CONTENT", line.strip())
|
|
365
|
+
index += 1
|
|
366
|
+
return index, flush_placeholders(doc, placeholders, not items)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def is_note_start(doc, line):
|
|
370
|
+
lang = LANGUAGES[doc.language]
|
|
371
|
+
stripped = line.strip()
|
|
372
|
+
if stripped == lang["fields"]["flow"] \
|
|
373
|
+
or any(stripped.startswith(a) for a in lang["trigger_aliases"]):
|
|
374
|
+
return False
|
|
375
|
+
return stripped.startswith(("**", "_", "*", ">"))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def read_flow_notes(doc, lines, index):
|
|
379
|
+
index = skip_blank(lines, index)
|
|
380
|
+
while index < len(lines) and not lines[index].startswith("#") \
|
|
381
|
+
and is_note_start(doc, lines[index]) \
|
|
382
|
+
and not NUMBERED_ITEM.fullmatch(lines[index].strip()):
|
|
383
|
+
if lines[index].startswith(">"):
|
|
384
|
+
while index < len(lines) and lines[index].startswith(">"):
|
|
385
|
+
index += 1
|
|
386
|
+
else:
|
|
387
|
+
parts = [lines[index].strip()]
|
|
388
|
+
index = join_continuation(doc, lines, index + 1, parts)
|
|
389
|
+
index = skip_blank(lines, index)
|
|
390
|
+
return index
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def parse_flow(doc, lines, index):
|
|
394
|
+
lang = LANGUAGES[doc.language]
|
|
395
|
+
line = lines[index]
|
|
396
|
+
if not line.strip():
|
|
397
|
+
return index + 1
|
|
398
|
+
if not line.startswith("### "):
|
|
399
|
+
doc.add(index + 1, ERROR, "UNEXPECTED_CONTENT", line.strip())
|
|
400
|
+
return index + 1
|
|
401
|
+
heading_line = index + 1
|
|
402
|
+
heading = line[4:].strip()
|
|
403
|
+
index = read_flow_notes(doc, lines, index + 1)
|
|
404
|
+
|
|
405
|
+
trigger = None
|
|
406
|
+
if index < len(lines):
|
|
407
|
+
alias = next((a for a in lang["trigger_aliases"]
|
|
408
|
+
if lines[index].startswith(a)), None)
|
|
409
|
+
if alias:
|
|
410
|
+
parts = [lines[index][len(alias):].strip()]
|
|
411
|
+
index = join_continuation(doc, lines, index + 1, parts)
|
|
412
|
+
trigger = " ".join(parts)
|
|
413
|
+
index = read_flow_notes(doc, lines, index)
|
|
414
|
+
flow_field_seen = False
|
|
415
|
+
if index < len(lines) and lines[index].strip() == lang["fields"]["flow"]:
|
|
416
|
+
flow_field_seen = True
|
|
417
|
+
index += 1
|
|
418
|
+
|
|
419
|
+
steps = []
|
|
420
|
+
while index < len(lines) and not lines[index].startswith("#"):
|
|
421
|
+
body = lines[index]
|
|
422
|
+
if not body.strip():
|
|
423
|
+
index += 1
|
|
424
|
+
continue
|
|
425
|
+
match = NUMBERED_ITEM.fullmatch(body.strip())
|
|
426
|
+
if match and not body.startswith(" "):
|
|
427
|
+
step_line = index + 1
|
|
428
|
+
parts = [match.group(2).strip()]
|
|
429
|
+
index = join_continuation(doc, lines, index + 1, parts)
|
|
430
|
+
steps.append((step_line, " ".join(parts)))
|
|
431
|
+
elif is_note_start(doc, body):
|
|
432
|
+
index = read_flow_notes(doc, lines, index)
|
|
433
|
+
else:
|
|
434
|
+
doc.add(index + 1, ERROR, "UNEXPECTED_CONTENT", body.strip())
|
|
435
|
+
index += 1
|
|
436
|
+
|
|
437
|
+
if trigger is None or not flow_field_seen or not steps:
|
|
438
|
+
missing = [name for present, name in [
|
|
439
|
+
(trigger is not None, "trigger line (" + lang["fields"]["trigger"] + ")"),
|
|
440
|
+
(flow_field_seen, "flow field line (" + lang["fields"]["flow"] + ")"),
|
|
441
|
+
(bool(steps), "numbered steps"),
|
|
442
|
+
] if not present]
|
|
443
|
+
doc.add(heading_line, ERROR, "FLOW_INCOMPLETE",
|
|
444
|
+
"alternative flow '" + heading + "' is missing: "
|
|
445
|
+
+ ", ".join(missing))
|
|
446
|
+
else:
|
|
447
|
+
doc.flows.append({"heading": heading, "line": heading_line,
|
|
448
|
+
"trigger": trigger, "steps": steps})
|
|
449
|
+
return index
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def parse_alternative_flows(doc, lines, index):
|
|
453
|
+
placeholders = []
|
|
454
|
+
while index < len(lines) and not lines[index].startswith("## "):
|
|
455
|
+
line = lines[index]
|
|
456
|
+
if line.strip() and is_placeholder_start(line):
|
|
457
|
+
index = read_placeholder(doc, lines, index, placeholders)
|
|
458
|
+
else:
|
|
459
|
+
index = parse_flow(doc, lines, index)
|
|
460
|
+
doc.flows_placeholder = flush_placeholders(doc, placeholders,
|
|
461
|
+
not doc.flows)
|
|
462
|
+
return index
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def parse_postconditions(doc, lines, index):
|
|
466
|
+
lang = LANGUAGES[doc.language]
|
|
467
|
+
placeholders = []
|
|
468
|
+
while index < len(lines) and not lines[index].startswith("## "):
|
|
469
|
+
line = lines[index]
|
|
470
|
+
if not line.strip():
|
|
471
|
+
index += 1
|
|
472
|
+
elif line.strip() == lang["headings"]["success"]:
|
|
473
|
+
doc.sections_seen.add("success")
|
|
474
|
+
index, doc.success_placeholder = \
|
|
475
|
+
parse_items(doc, lines, index + 1, doc.success_post)
|
|
476
|
+
elif line.strip() == lang["headings"]["failure"]:
|
|
477
|
+
doc.sections_seen.add("failure")
|
|
478
|
+
index, doc.failure_placeholder = \
|
|
479
|
+
parse_items(doc, lines, index + 1, doc.failure_post)
|
|
480
|
+
elif is_placeholder_start(line):
|
|
481
|
+
index = read_placeholder(doc, lines, index, placeholders)
|
|
482
|
+
else:
|
|
483
|
+
doc.add(index + 1, ERROR, "UNEXPECTED_CONTENT", line.strip())
|
|
484
|
+
index += 1
|
|
485
|
+
flush_placeholders(doc, placeholders,
|
|
486
|
+
not doc.success_post and not doc.failure_post)
|
|
487
|
+
return index
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def parse_rule(doc, lines, index):
|
|
491
|
+
line = lines[index]
|
|
492
|
+
if not line.strip():
|
|
493
|
+
return index + 1
|
|
494
|
+
if not line.startswith("### "):
|
|
495
|
+
doc.add(index + 1, ERROR, "UNEXPECTED_CONTENT", line.strip())
|
|
496
|
+
return index + 1
|
|
497
|
+
doc.rules.append({"heading": line[4:].strip(), "line": index + 1})
|
|
498
|
+
index += 1
|
|
499
|
+
while index < len(lines) and not lines[index].startswith("### ") \
|
|
500
|
+
and not lines[index].startswith("## "):
|
|
501
|
+
index += 1
|
|
502
|
+
return index
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def parse_business_rules(doc, lines, index):
|
|
506
|
+
placeholders = []
|
|
507
|
+
while index < len(lines) and not lines[index].startswith("## "):
|
|
508
|
+
line = lines[index]
|
|
509
|
+
if line.strip() and is_placeholder_start(line):
|
|
510
|
+
index = read_placeholder(doc, lines, index, placeholders)
|
|
511
|
+
else:
|
|
512
|
+
index = parse_rule(doc, lines, index)
|
|
513
|
+
flush_placeholders(doc, placeholders, not doc.rules)
|
|
514
|
+
return index
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def parse_extra_section(lines, index):
|
|
518
|
+
"""A section outside the template: kept verbatim by Studio."""
|
|
519
|
+
index += 1
|
|
520
|
+
while index < len(lines) and not lines[index].startswith("## "):
|
|
521
|
+
index += 1
|
|
522
|
+
return index
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# ---------------------------------------------------------------------------
|
|
526
|
+
# Document validation
|
|
527
|
+
# ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
def parse_document(text):
|
|
530
|
+
doc = Document()
|
|
531
|
+
lines = [line.rstrip() for line in text.splitlines()]
|
|
532
|
+
doc.language = detect_language(lines)
|
|
533
|
+
lang = LANGUAGES[doc.language]
|
|
534
|
+
headings = lang["headings"]
|
|
535
|
+
index = parse_title(doc, lines)
|
|
536
|
+
while index < len(lines):
|
|
537
|
+
line = lines[index]
|
|
538
|
+
if not line.strip():
|
|
539
|
+
index += 1
|
|
540
|
+
elif line == headings["overview"]:
|
|
541
|
+
doc.overview_seen = True
|
|
542
|
+
doc.sections_seen.add("overview")
|
|
543
|
+
index = parse_overview(doc, lines, index + 1)
|
|
544
|
+
elif line == headings["preconditions"]:
|
|
545
|
+
doc.sections_seen.add("preconditions")
|
|
546
|
+
index, _ = parse_items(doc, lines, index + 1, doc.preconditions)
|
|
547
|
+
elif line == headings["main_scenario"]:
|
|
548
|
+
doc.sections_seen.add("main_scenario")
|
|
549
|
+
index, doc.main_scenario_placeholder = parse_numbered_items(
|
|
550
|
+
doc, lines, index + 1, doc.main_scenario,
|
|
551
|
+
doc.main_scenario_numbers)
|
|
552
|
+
elif line == headings["alternative_flows"]:
|
|
553
|
+
doc.sections_seen.add("alternative_flows")
|
|
554
|
+
index = parse_alternative_flows(doc, lines, index + 1)
|
|
555
|
+
elif line == headings["postconditions"]:
|
|
556
|
+
doc.sections_seen.add("postconditions")
|
|
557
|
+
index = parse_postconditions(doc, lines, index + 1)
|
|
558
|
+
elif line == headings["business_rules"]:
|
|
559
|
+
doc.sections_seen.add("business_rules")
|
|
560
|
+
index = parse_business_rules(doc, lines, index + 1)
|
|
561
|
+
elif line.startswith("#"):
|
|
562
|
+
index = parse_extra_section(lines, index)
|
|
563
|
+
else:
|
|
564
|
+
doc.add(index + 1, ERROR, "UNEXPECTED_CONTENT", line.strip())
|
|
565
|
+
index += 1
|
|
566
|
+
|
|
567
|
+
if not doc.overview_seen:
|
|
568
|
+
doc.add(0, ERROR, "OVERVIEW_MISSING",
|
|
569
|
+
"section missing: " + headings["overview"])
|
|
570
|
+
status = doc.overview.get("status")
|
|
571
|
+
if doc.overview_seen and status is not None and parse_status(status) is None:
|
|
572
|
+
doc.add(doc.overview.get("status_line", 0), ERROR, "STATUS_INVALID",
|
|
573
|
+
"not a recognizable status value: " + status)
|
|
574
|
+
return doc
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def check_contract(doc, path):
|
|
578
|
+
"""Skill-contract checks beyond what Studio's parser enforces (WARN)."""
|
|
579
|
+
lang = LANGUAGES[doc.language]
|
|
580
|
+
headings = lang["headings"]
|
|
581
|
+
|
|
582
|
+
for key in ("preconditions", "main_scenario", "alternative_flows",
|
|
583
|
+
"postconditions", "business_rules"):
|
|
584
|
+
if key not in doc.sections_seen:
|
|
585
|
+
doc.add(0, WARN, "SECTION_MISSING",
|
|
586
|
+
"template section missing: " + headings[key])
|
|
587
|
+
if "postconditions" in doc.sections_seen:
|
|
588
|
+
for key in ("success", "failure"):
|
|
589
|
+
if key not in doc.sections_seen:
|
|
590
|
+
doc.add(0, WARN, "SECTION_MISSING",
|
|
591
|
+
"postconditions subsection missing: " + headings[key])
|
|
592
|
+
|
|
593
|
+
uc_id = doc.overview.get("id")
|
|
594
|
+
if uc_id and not UC_ID_GRAMMAR.fullmatch(uc_id):
|
|
595
|
+
doc.add(0, WARN, "ID_GRAMMAR",
|
|
596
|
+
"use case id does not match [SB]?UC-[A-Za-z0-9_-]+: " + uc_id)
|
|
597
|
+
basename = os.path.basename(path)
|
|
598
|
+
if uc_id and re.match(r"[SB]?UC", basename) \
|
|
599
|
+
and not basename.startswith(uc_id):
|
|
600
|
+
doc.add(0, WARN, "ID_FILENAME_MISMATCH",
|
|
601
|
+
"filename does not start with the use case id " + uc_id)
|
|
602
|
+
|
|
603
|
+
# The use case trigger is optional (older documents have none), but when
|
|
604
|
+
# present it must name the starting event, not restate a precondition.
|
|
605
|
+
trigger = doc.overview.get("trigger")
|
|
606
|
+
if trigger is not None:
|
|
607
|
+
trigger_line = doc.overview.get("trigger_line", 0)
|
|
608
|
+
if not trigger.strip():
|
|
609
|
+
doc.add(trigger_line, WARN, "UC_TRIGGER_EMPTY",
|
|
610
|
+
"the use case trigger line names no event")
|
|
611
|
+
elif STEP_REFERENCE.search(trigger):
|
|
612
|
+
doc.add(trigger_line, WARN, "UC_TRIGGER_STEP_REF",
|
|
613
|
+
"the use case trigger references a step; '(step N)' "
|
|
614
|
+
"belongs to alternative-flow triggers only")
|
|
615
|
+
normalized = trigger.strip().rstrip(".").lower()
|
|
616
|
+
for condition in doc.preconditions:
|
|
617
|
+
if condition.strip().rstrip(".").lower() == normalized:
|
|
618
|
+
doc.add(trigger_line, WARN, "UC_TRIGGER_IS_PRECONDITION",
|
|
619
|
+
"the use case trigger repeats a precondition; a "
|
|
620
|
+
"trigger is the event that starts the use case, a "
|
|
621
|
+
"precondition is what is already true: " + trigger)
|
|
622
|
+
|
|
623
|
+
if not doc.main_scenario and not doc.main_scenario_placeholder \
|
|
624
|
+
and "main_scenario" in doc.sections_seen:
|
|
625
|
+
doc.add(0, WARN, "MAIN_SCENARIO_EMPTY",
|
|
626
|
+
"the main success scenario has no steps")
|
|
627
|
+
if doc.main_scenario_numbers != list(
|
|
628
|
+
range(1, len(doc.main_scenario_numbers) + 1)):
|
|
629
|
+
doc.add(0, WARN, "NUMBERING",
|
|
630
|
+
"main scenario step numbers are not 1..n without gaps "
|
|
631
|
+
"(Studio renumbers them on save)")
|
|
632
|
+
|
|
633
|
+
if not doc.flows and not doc.flows_placeholder \
|
|
634
|
+
and "alternative_flows" in doc.sections_seen:
|
|
635
|
+
doc.add(0, WARN, "NO_ALTERNATIVE_FLOWS",
|
|
636
|
+
"no alternative flow is defined; analyze every main-scenario "
|
|
637
|
+
"step for alternative or exception conditions, and when "
|
|
638
|
+
"there really is none, state it with an italic placeholder "
|
|
639
|
+
"such as '_None — no step can fail or branch._' instead of "
|
|
640
|
+
"inventing a flow")
|
|
641
|
+
for flow in doc.flows:
|
|
642
|
+
if not STEP_REFERENCE.search(flow["trigger"]):
|
|
643
|
+
doc.add(flow["line"], WARN, "TRIGGER_STEP_REF",
|
|
644
|
+
"trigger of '" + flow["heading"] + "' does not reference "
|
|
645
|
+
"a main-scenario step as '(step N)'")
|
|
646
|
+
if flow["steps"] and not FLOW_TERMINATION.search(flow["steps"][-1][1]):
|
|
647
|
+
doc.add(flow["line"], WARN, "FLOW_TERMINATION",
|
|
648
|
+
"'" + flow["heading"] + "' does not end with 'Use case "
|
|
649
|
+
"continues at step N.' or 'Use case ends.'")
|
|
650
|
+
|
|
651
|
+
if "success" in doc.sections_seen and not doc.success_post \
|
|
652
|
+
and not doc.success_placeholder:
|
|
653
|
+
doc.add(0, WARN, "POSTCONDITIONS_EMPTY",
|
|
654
|
+
"success postconditions are empty")
|
|
655
|
+
if "failure" in doc.sections_seen and not doc.failure_post \
|
|
656
|
+
and not doc.failure_placeholder:
|
|
657
|
+
doc.add(0, WARN, "POSTCONDITIONS_EMPTY",
|
|
658
|
+
"failure postconditions are empty")
|
|
659
|
+
|
|
660
|
+
prefix = lang["rule_prefix"]
|
|
661
|
+
for rule in doc.rules:
|
|
662
|
+
if not RULE_LABEL.fullmatch(rule["heading"]):
|
|
663
|
+
doc.add(rule["line"], WARN, "RULE_LABEL_MISSING",
|
|
664
|
+
"business rule heading has no " + prefix + "-XXX label: "
|
|
665
|
+
+ rule["heading"])
|
|
666
|
+
# Rule ids are scoped to their use case: every document numbers its rules
|
|
667
|
+
# from BR-001 without gaps (Studio renumbers them so on save). Checked only
|
|
668
|
+
# when every label is plainly numeric — Navigator-style ids (BR-USER-050)
|
|
669
|
+
# follow their own scheme.
|
|
670
|
+
numbers = [RULE_NUMBER.match(rule["heading"]) for rule in doc.rules]
|
|
671
|
+
if numbers and all(numbers):
|
|
672
|
+
values = [int(match.group(1)) for match in numbers]
|
|
673
|
+
if values != list(range(1, len(values) + 1)):
|
|
674
|
+
doc.add(doc.rules[0]["line"], WARN, "RULE_NUMBERING",
|
|
675
|
+
"business rule ids are not " + prefix + "-001.."
|
|
676
|
+
+ prefix + ("-%03d" % len(values)) + " without gaps "
|
|
677
|
+
"(ids restart per document; Studio renumbers on save)")
|
|
678
|
+
|
|
679
|
+
steps = doc.main_scenario + [s for f in doc.flows for s in f["steps"]]
|
|
680
|
+
for step_line, step in steps:
|
|
681
|
+
for pattern, term in TECHNICAL_TERMS:
|
|
682
|
+
if pattern.search(step):
|
|
683
|
+
doc.add(step_line, WARN, "TECHNICAL_TERM",
|
|
684
|
+
"implementation-level term '" + term + "' in step: "
|
|
685
|
+
+ step)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def validate_file(path):
|
|
689
|
+
with open(path, encoding="utf-8") as handle:
|
|
690
|
+
text = handle.read()
|
|
691
|
+
doc = parse_document(text)
|
|
692
|
+
check_contract(doc, path)
|
|
693
|
+
return doc.problems
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
# ---------------------------------------------------------------------------
|
|
697
|
+
# Self test
|
|
698
|
+
# ---------------------------------------------------------------------------
|
|
699
|
+
|
|
700
|
+
VALID_EN = """\
|
|
701
|
+
# Use Case: Create Reservation
|
|
702
|
+
|
|
703
|
+
## Overview
|
|
704
|
+
|
|
705
|
+
**Use Case ID:** UC-001
|
|
706
|
+
**Use Case Name:** Create Reservation
|
|
707
|
+
**Primary Actor:** Clerk
|
|
708
|
+
**Goal:** Create a reservation for a guest
|
|
709
|
+
**Trigger:** A guest asks the clerk to book a room
|
|
710
|
+
**Status:** Approved
|
|
711
|
+
|
|
712
|
+
**Requirements:** [FR-001, FR-002](../requirements.md)
|
|
713
|
+
|
|
714
|
+
## Preconditions
|
|
715
|
+
|
|
716
|
+
- Clerk is logged into the system
|
|
717
|
+
|
|
718
|
+
## Main Success Scenario
|
|
719
|
+
|
|
720
|
+
1. Clerk selects "New Reservation".
|
|
721
|
+
2. System displays the reservation form.
|
|
722
|
+
3. Clerk confirms the reservation.
|
|
723
|
+
4. System creates the reservation and displays a confirmation number.
|
|
724
|
+
|
|
725
|
+
## Alternative Flows
|
|
726
|
+
|
|
727
|
+
### A1: Guest Already Exists
|
|
728
|
+
|
|
729
|
+
**Trigger:** Guest email matches existing record (step 2)
|
|
730
|
+
**Flow:**
|
|
731
|
+
|
|
732
|
+
1. System displays existing guest information.
|
|
733
|
+
2. Use case continues at step 3.
|
|
734
|
+
|
|
735
|
+
## Postconditions
|
|
736
|
+
|
|
737
|
+
### Success Postconditions
|
|
738
|
+
|
|
739
|
+
- Reservation is stored
|
|
740
|
+
|
|
741
|
+
### Failure Postconditions
|
|
742
|
+
|
|
743
|
+
- No reservation is created
|
|
744
|
+
|
|
745
|
+
## Business Rules
|
|
746
|
+
|
|
747
|
+
### BR-001: Minimum Stay
|
|
748
|
+
|
|
749
|
+
Reservations must be for at least one night.
|
|
750
|
+
"""
|
|
751
|
+
|
|
752
|
+
VALID_DE_TOLERANT = """\
|
|
753
|
+
# UC-013a: Mitteilung erfassen
|
|
754
|
+
|
|
755
|
+
## Übersicht
|
|
756
|
+
|
|
757
|
+
**Use-Case-ID:** UC-013a
|
|
758
|
+
**Use-Case-Name:** Mitteilung erfassen
|
|
759
|
+
**Primärer Akteur:** SachbearbeiterIn
|
|
760
|
+
**Sekundäre Akteure:** TeamleiterIn
|
|
761
|
+
**Ziel:** Eine Mitteilung erfassen und dem Team zustellen
|
|
762
|
+
**Auslösendes Ereignis:** Eine Kundin meldet einen Sachverhalt telefonisch
|
|
763
|
+
**Status:** ✅ Implementiert (2025-07-11)
|
|
764
|
+
|
|
765
|
+
**Priorität:** Hoch
|
|
766
|
+
|
|
767
|
+
## Vorbedingungen
|
|
768
|
+
|
|
769
|
+
- Der Benutzer ist eingeloggt
|
|
770
|
+
|
|
771
|
+
## Suchkriterien
|
|
772
|
+
|
|
773
|
+
Die Suche akzeptiert Name und Nummer.
|
|
774
|
+
|
|
775
|
+
## Hauptablauf
|
|
776
|
+
|
|
777
|
+
1. Der Benutzer öffnet die Erfassung.
|
|
778
|
+
2. Das System speichert die Mitteilung.
|
|
779
|
+
|
|
780
|
+
## Alternativabläufe
|
|
781
|
+
|
|
782
|
+
### A1: Pflichtfeld fehlt
|
|
783
|
+
|
|
784
|
+
**Trigger:** Ein Pflichtfeld ist leer (Schritt 1)
|
|
785
|
+
|
|
786
|
+
> Hinweis: Die Feldliste ist konfigurierbar.
|
|
787
|
+
|
|
788
|
+
**Ablauf:**
|
|
789
|
+
|
|
790
|
+
1. Das System zeigt eine Fehlermeldung.
|
|
791
|
+
2. Der Use Case wird bei Schritt 1 fortgesetzt.
|
|
792
|
+
|
|
793
|
+
## Nachbedingungen
|
|
794
|
+
|
|
795
|
+
### Erfolgsfall
|
|
796
|
+
|
|
797
|
+
- Die Mitteilung ist gespeichert
|
|
798
|
+
|
|
799
|
+
### Fehlerfall
|
|
800
|
+
|
|
801
|
+
_Keine — die Erfassung ist wiederholbar._
|
|
802
|
+
|
|
803
|
+
## Geschäftsregeln
|
|
804
|
+
|
|
805
|
+
### GR-001: Zustellung
|
|
806
|
+
|
|
807
|
+
Mitteilungen werden nur dem eigenen Team zugestellt.
|
|
808
|
+
"""
|
|
809
|
+
|
|
810
|
+
INVALID = """\
|
|
811
|
+
# Use Case: Broken
|
|
812
|
+
|
|
813
|
+
## Overview
|
|
814
|
+
|
|
815
|
+
**Use Case ID:** UC-002
|
|
816
|
+
**Use Case Name:** Broken
|
|
817
|
+
**Primary Actor:** User
|
|
818
|
+
**Goal:** Show every error class
|
|
819
|
+
**Trigger:** User is logged in
|
|
820
|
+
**Status:** In Progress
|
|
821
|
+
|
|
822
|
+
## Preconditions
|
|
823
|
+
|
|
824
|
+
- User is logged in
|
|
825
|
+
|
|
826
|
+
The user is logged in.
|
|
827
|
+
|
|
828
|
+
## Main Success Scenario
|
|
829
|
+
|
|
830
|
+
1. User does something.
|
|
831
|
+
|
|
832
|
+
## Alternative Flows
|
|
833
|
+
|
|
834
|
+
### A1: No Trigger
|
|
835
|
+
|
|
836
|
+
**Flow:**
|
|
837
|
+
|
|
838
|
+
1. System shows an error.
|
|
839
|
+
|
|
840
|
+
## Postconditions
|
|
841
|
+
|
|
842
|
+
### Success Postconditions
|
|
843
|
+
|
|
844
|
+
- Something happened
|
|
845
|
+
|
|
846
|
+
### Failure Postconditions
|
|
847
|
+
|
|
848
|
+
- Nothing happened
|
|
849
|
+
|
|
850
|
+
## Business Rules
|
|
851
|
+
"""
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def self_test():
|
|
855
|
+
failures = []
|
|
856
|
+
|
|
857
|
+
def expect(name, problems, expected_codes, forbidden_severity=None):
|
|
858
|
+
codes = {p.code for p in problems}
|
|
859
|
+
for code in expected_codes:
|
|
860
|
+
if code not in codes:
|
|
861
|
+
failures.append(name + ": expected " + code + ", got "
|
|
862
|
+
+ str(sorted(codes)))
|
|
863
|
+
if forbidden_severity:
|
|
864
|
+
bad = [p for p in problems if p.severity == forbidden_severity]
|
|
865
|
+
for p in bad:
|
|
866
|
+
failures.append(name + ": unexpected " + p.severity + " "
|
|
867
|
+
+ p.code + ": " + p.message)
|
|
868
|
+
|
|
869
|
+
doc = parse_document(VALID_EN)
|
|
870
|
+
check_contract(doc, "UC-001-create-reservation.md")
|
|
871
|
+
expect("valid-en", doc.problems, [], forbidden_severity=ERROR)
|
|
872
|
+
expect("valid-en", doc.problems, [], forbidden_severity=WARN)
|
|
873
|
+
|
|
874
|
+
doc = parse_document(VALID_DE_TOLERANT)
|
|
875
|
+
check_contract(doc, "UC-013a-mitteilung-erfassen.md")
|
|
876
|
+
if doc.language != "de":
|
|
877
|
+
failures.append("valid-de: language not detected as German")
|
|
878
|
+
expect("valid-de", doc.problems, [], forbidden_severity=ERROR)
|
|
879
|
+
expect("valid-de", doc.problems, [], forbidden_severity=WARN)
|
|
880
|
+
|
|
881
|
+
doc = parse_document(INVALID)
|
|
882
|
+
check_contract(doc, "UC-002-broken.md")
|
|
883
|
+
expect("invalid", doc.problems,
|
|
884
|
+
["STATUS_INVALID", "UNEXPECTED_CONTENT", "FLOW_INCOMPLETE",
|
|
885
|
+
"UC_TRIGGER_IS_PRECONDITION"])
|
|
886
|
+
|
|
887
|
+
if failures:
|
|
888
|
+
for failure in failures:
|
|
889
|
+
print("SELF-TEST FAIL: " + failure)
|
|
890
|
+
return 1
|
|
891
|
+
print("self-test passed")
|
|
892
|
+
return 0
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
# ---------------------------------------------------------------------------
|
|
896
|
+
# CLI
|
|
897
|
+
# ---------------------------------------------------------------------------
|
|
898
|
+
|
|
899
|
+
def main(argv):
|
|
900
|
+
parser = argparse.ArgumentParser(
|
|
901
|
+
description="Validate AI Unified Process use case specifications.")
|
|
902
|
+
parser.add_argument("files", nargs="*", help="UC-*.md files to validate")
|
|
903
|
+
parser.add_argument("--strict", action="store_true",
|
|
904
|
+
help="treat warnings as failures")
|
|
905
|
+
parser.add_argument("--quiet", action="store_true",
|
|
906
|
+
help="print problems only, no summary")
|
|
907
|
+
parser.add_argument("--self-test", action="store_true",
|
|
908
|
+
help="run the built-in fixtures and exit")
|
|
909
|
+
args = parser.parse_args(argv)
|
|
910
|
+
|
|
911
|
+
if args.self_test:
|
|
912
|
+
return self_test()
|
|
913
|
+
if not args.files:
|
|
914
|
+
parser.print_usage()
|
|
915
|
+
return 2
|
|
916
|
+
|
|
917
|
+
errors = warnings = 0
|
|
918
|
+
for path in args.files:
|
|
919
|
+
try:
|
|
920
|
+
problems = validate_file(path)
|
|
921
|
+
except OSError as exc:
|
|
922
|
+
print(path + ": ERROR IO: " + str(exc))
|
|
923
|
+
errors += 1
|
|
924
|
+
continue
|
|
925
|
+
for problem in sorted(problems, key=lambda p: p.line):
|
|
926
|
+
print("%s:%d: %s %s: %s" % (path, problem.line, problem.severity,
|
|
927
|
+
problem.code, problem.message))
|
|
928
|
+
if problem.severity == ERROR:
|
|
929
|
+
errors += 1
|
|
930
|
+
else:
|
|
931
|
+
warnings += 1
|
|
932
|
+
if not args.quiet:
|
|
933
|
+
print("%d file(s) checked: %d error(s), %d warning(s)"
|
|
934
|
+
% (len(args.files), errors, warnings))
|
|
935
|
+
if errors or (args.strict and warnings):
|
|
936
|
+
return 1
|
|
937
|
+
return 0
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
if __name__ == "__main__":
|
|
941
|
+
sys.exit(main(sys.argv[1:]))
|