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