opencode-skills-collection 4.1.1 → 4.1.3
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.
Potentially problematic release.
This version of opencode-skills-collection might be problematic. Click here for more details.
- package/bundled-skills/.antigravity-install-manifest.json +133 -1
- package/bundled-skills/360-feedback-system/SKILL.md +481 -0
- package/bundled-skills/access-matrix/SKILL.md +338 -0
- package/bundled-skills/accounting-audit-system-builder/SKILL.md +302 -0
- package/bundled-skills/accounting-audit-system-builder/references/catalog.md +114 -0
- package/bundled-skills/accounting-software-selection/SKILL.md +427 -0
- package/bundled-skills/accounting-software-selection/references/build-artifacts.md +362 -0
- package/bundled-skills/accounting-software-selection/references/common-pitfalls.md +44 -0
- package/bundled-skills/accounting-software-selection/references/field-reference.md +93 -0
- package/bundled-skills/accounting-software-selection/references/select-options.md +202 -0
- package/bundled-skills/admin-access-register/SKILL.md +381 -0
- package/bundled-skills/advanced-analytics-dashboard/SKILL.md +318 -0
- package/bundled-skills/ai-image-generation-studio/SKILL.md +259 -0
- package/bundled-skills/ai-logo-maker/SKILL.md +259 -0
- package/bundled-skills/ai-multilingual-dubbing/SKILL.md +259 -0
- package/bundled-skills/ai-photo-restyler/SKILL.md +259 -0
- package/bundled-skills/ai-podcast-voiceover/SKILL.md +259 -0
- package/bundled-skills/alumni-re-hire-tracker/SKILL.md +314 -0
- package/bundled-skills/announcement-board/SKILL.md +336 -0
- package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +63 -4
- package/bundled-skills/asset-it-management/SKILL.md +407 -0
- package/bundled-skills/attendance/SKILL.md +331 -0
- package/bundled-skills/audit-log/SKILL.md +314 -0
- package/bundled-skills/audit-preparation/SKILL.md +416 -0
- package/bundled-skills/beatra/SKILL.md +260 -0
- package/bundled-skills/board-governance/SKILL.md +360 -0
- package/bundled-skills/brand-growth-system-builder/SKILL.md +360 -0
- package/bundled-skills/brand-kit-print-collateral/SKILL.md +498 -0
- package/bundled-skills/buddy-program-manager/SKILL.md +329 -0
- package/bundled-skills/budget-cash-flow/SKILL.md +352 -0
- package/bundled-skills/busabase/SKILL.md +118 -0
- package/bundled-skills/business-email-template/SKILL.md +480 -0
- package/bundled-skills/business-email-template/references/common-pitfalls.md +30 -0
- package/bundled-skills/business-website-setup/SKILL.md +489 -0
- package/bundled-skills/business-website-setup/references/common-pitfalls.md +32 -0
- package/bundled-skills/candidate-talent-pool/SKILL.md +359 -0
- package/bundled-skills/capacity-workload-planner/SKILL.md +354 -0
- package/bundled-skills/changelog-entry/SKILL.md +141 -0
- package/bundled-skills/clients-accounts/SKILL.md +398 -0
- package/bundled-skills/code-of-conduct/SKILL.md +488 -0
- package/bundled-skills/code-of-conduct/references/common-pitfalls.md +31 -0
- package/bundled-skills/company-email-accounts/SKILL.md +404 -0
- package/bundled-skills/competency-matrix/SKILL.md +335 -0
- package/bundled-skills/contract-document-renewal/SKILL.md +396 -0
- package/bundled-skills/course-upskilling-requests/SKILL.md +354 -0
- package/bundled-skills/credit-cycle-analysis/SKILL.md +303 -0
- package/bundled-skills/credit-cycle-analysis/references/best-practices.md +35 -0
- package/bundled-skills/credit-cycle-analysis/references/common-pitfalls.md +5 -0
- package/bundled-skills/credit-cycle-analysis/references/examples.md +34 -0
- package/bundled-skills/credit-cycle-analysis/references/field-reference.md +47 -0
- package/bundled-skills/credit-cycle-analysis/references/how-it-works.md +494 -0
- package/bundled-skills/credit-cycle-analysis/references/security-notes.md +16 -0
- package/bundled-skills/credit-cycle-analysis/references/select-options.md +32 -0
- package/bundled-skills/csv-manual-export/SKILL.md +499 -0
- package/bundled-skills/csv-manual-export/references/common-pitfalls.md +19 -0
- package/bundled-skills/culture-retention/SKILL.md +346 -0
- package/bundled-skills/data-export-engine/SKILL.md +308 -0
- package/bundled-skills/data-privacy-controls/SKILL.md +317 -0
- package/bundled-skills/day-book/SKILL.md +499 -0
- package/bundled-skills/day-book/references/best-practices.md +32 -0
- package/bundled-skills/day-book/references/common-pitfalls.md +58 -0
- package/bundled-skills/day-book/references/field-reference.md +45 -0
- package/bundled-skills/day-book/references/how-it-works.md +7 -0
- package/bundled-skills/day-book/references/related-skills.md +18 -0
- package/bundled-skills/day-book/references/relations.md +14 -0
- package/bundled-skills/day-book/references/reusable-prompt.md +50 -0
- package/bundled-skills/day-book/references/security-safety-notes.md +19 -0
- package/bundled-skills/day-book/references/select-options.md +38 -0
- package/bundled-skills/deep-research-framework/SKILL.md +81 -0
- package/bundled-skills/dei-dashboard/SKILL.md +307 -0
- package/bundled-skills/design-theme-guide/SKILL.md +472 -0
- package/bundled-skills/disciplinary-pip-tracker/SKILL.md +345 -0
- package/bundled-skills/document-management-system/SKILL.md +328 -0
- package/bundled-skills/ecommerce-listing-image-set/SKILL.md +259 -0
- package/bundled-skills/employee-suggestion-hub/SKILL.md +322 -0
- package/bundled-skills/esop-equity-tracker/SKILL.md +331 -0
- package/bundled-skills/events-activities/SKILL.md +355 -0
- package/bundled-skills/expense-accounting/SKILL.md +496 -0
- package/bundled-skills/expense-accounting/references/common-pitfalls.md +41 -0
- package/bundled-skills/expense-accounting/references/related-skills.md +11 -0
- package/bundled-skills/expense-accounting/references/reusable-prompt.md +36 -0
- package/bundled-skills/expense-management/SKILL.md +351 -0
- package/bundled-skills/five-axis-code-review/SKILL.md +78 -0
- package/bundled-skills/free-design-resources/SKILL.md +480 -0
- package/bundled-skills/free-design-resources/references/common-pitfalls.md +44 -0
- package/bundled-skills/free-design-resources/references/related-skills.md +24 -0
- package/bundled-skills/free-design-resources/references/reusable-prompt.md +12 -0
- package/bundled-skills/gamification-engine/SKILL.md +307 -0
- package/bundled-skills/gbp-local-seo-intent/SKILL.md +466 -0
- package/bundled-skills/gbp-local-seo-intent/references/best-practices.md +41 -0
- package/bundled-skills/gbp-local-seo-intent/references/common-pitfalls.md +40 -0
- package/bundled-skills/gbp-local-seo-intent/references/related-skills.md +17 -0
- package/bundled-skills/gbp-local-seo-intent/references/reusable-prompt.md +11 -0
- package/bundled-skills/gbp-local-seo-intent/references/security-safety-notes.md +27 -0
- package/bundled-skills/git-commit-message/SKILL.md +76 -0
- package/bundled-skills/health-wellness/SKILL.md +334 -0
- package/bundled-skills/idea-to-blueprint/LICENSE +21 -0
- package/bundled-skills/idea-to-blueprint/SKILL.md +260 -0
- package/bundled-skills/idea-to-blueprint/assets/AGENTS.md.template +38 -0
- package/bundled-skills/idea-to-blueprint/assets/CLAUDE.md.template +38 -0
- package/bundled-skills/idea-to-blueprint/assets/DECISIONS.md.template +14 -0
- package/bundled-skills/idea-to-blueprint/assets/PROGRESS.md.template +47 -0
- package/bundled-skills/idea-to-blueprint/assets/session-prompt.md +23 -0
- package/bundled-skills/idea-to-blueprint/references/architecture-decisions.md +202 -0
- package/bundled-skills/idea-to-blueprint/references/blueprint-template.md +259 -0
- package/bundled-skills/idea-to-blueprint/references/code-quality-and-review.md +216 -0
- package/bundled-skills/idea-to-blueprint/references/design-minimal.md +96 -0
- package/bundled-skills/idea-to-blueprint/references/edge-case-catalog.md +239 -0
- package/bundled-skills/idea-to-blueprint/references/intake-questions.md +105 -0
- package/bundled-skills/idea-to-blueprint/references/research-protocol.md +196 -0
- package/bundled-skills/idea-to-blueprint/references/session-protocol.md +133 -0
- package/bundled-skills/idea-to-blueprint/references/stories-ac-tests.md +194 -0
- package/bundled-skills/idea-to-blueprint/references/ux-writing-guide.md +128 -0
- package/bundled-skills/idea-to-blueprint/scripts/lint_blueprint.py +460 -0
- package/bundled-skills/intern-program/SKILL.md +354 -0
- package/bundled-skills/internal-communication/SKILL.md +318 -0
- package/bundled-skills/inventory-stock-reconciliation/SKILL.md +470 -0
- package/bundled-skills/inventory-stock-reconciliation/references/best-practices.md +18 -0
- package/bundled-skills/inventory-stock-reconciliation/references/common-pitfalls.md +42 -0
- package/bundled-skills/inventory-stock-reconciliation/references/related-skills.md +13 -0
- package/bundled-skills/inventory-stock-reconciliation/references/relations.md +8 -0
- package/bundled-skills/inventory-stock-reconciliation/references/reusable-prompt.md +31 -0
- package/bundled-skills/inventory-stock-reconciliation/references/security-safety-notes.md +17 -0
- package/bundled-skills/inventory-stock-reconciliation/references/select-options.md +45 -0
- package/bundled-skills/invoices-billing/SKILL.md +385 -0
- package/bundled-skills/issue-grievance-tracker/SKILL.md +400 -0
- package/bundled-skills/json-schema-manual/SKILL.md +309 -0
- package/bundled-skills/json-schema-manual/references/best-practices.md +9 -0
- package/bundled-skills/json-schema-manual/references/common-pitfalls.md +25 -0
- package/bundled-skills/json-schema-manual/references/input-modules.md +203 -0
- package/bundled-skills/json-schema-manual/references/related-skills.md +8 -0
- package/bundled-skills/json-schema-manual/references/reusable-prompt.md +10 -0
- package/bundled-skills/json-schema-manual/references/security-safety-notes.md +13 -0
- package/bundled-skills/knowledge-base/SKILL.md +318 -0
- package/bundled-skills/kpi-tracker/SKILL.md +345 -0
- package/bundled-skills/learning-career-development/SKILL.md +349 -0
- package/bundled-skills/leave-management/SKILL.md +341 -0
- package/bundled-skills/legal-compliance-vault/SKILL.md +324 -0
- package/bundled-skills/linktree-link-hub/SKILL.md +453 -0
- package/bundled-skills/liuguang-banlan-ui/SKILL.md +22 -18
- package/bundled-skills/liuguang-banlan-ui/assets/starter/launcher.css +2 -2
- package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/index.html +5 -4
- package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme-config.js +3 -10
- package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme.css +48 -62
- package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/index.html +5 -4
- package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme-config.js +27 -35
- package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme.css +48 -62
- package/bundled-skills/liuguang-banlan-ui/assets/starter/shared/spectral-field.js +135 -148
- package/bundled-skills/liuguang-banlan-ui/references/style-contract.md +26 -7
- package/bundled-skills/liuguang-banlan-ui/references/verification.md +19 -7
- package/bundled-skills/liuguang-banlan-ui/scripts/measure_preview.py +78 -47
- package/bundled-skills/liuguang-banlan-ui/scripts/serve_preview.py +35 -0
- package/bundled-skills/liuguang-banlan-ui/scripts/validate_manifest.py +34 -3
- package/bundled-skills/liuguang-banlan-ui/tests/test_measure_preview.py +119 -0
- package/bundled-skills/liuguang-banlan-ui/tests/test_serve_preview.py +38 -0
- package/bundled-skills/liuguang-banlan-ui/tests/test_validate_manifest.py +73 -0
- package/bundled-skills/logo-image-design/SKILL.md +459 -0
- package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +12 -12
- package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +1 -1
- package/bundled-skills/meeting-notes/SKILL.md +80 -0
- package/bundled-skills/mentorship-program/SKILL.md +332 -0
- package/bundled-skills/mirrord/SKILL.md +90 -0
- package/bundled-skills/monthly-closing-statements/SKILL.md +488 -0
- package/bundled-skills/monthly-closing-statements/references/related-skills.md +17 -0
- package/bundled-skills/monthly-closing-statements/references/reusable-prompt.md +8 -0
- package/bundled-skills/music-generation-studio/SKILL.md +259 -0
- package/bundled-skills/notification-reminder-hub/SKILL.md +324 -0
- package/bundled-skills/notion-manual-import/SKILL.md +376 -0
- package/bundled-skills/notion-manual-import/references/best-practices.md +10 -0
- package/bundled-skills/notion-manual-import/references/common-pitfalls.md +26 -0
- package/bundled-skills/notion-manual-import/references/input-modules.md +203 -0
- package/bundled-skills/notion-manual-import/references/related-skills.md +7 -0
- package/bundled-skills/notion-manual-import/references/reusable-prompt.md +8 -0
- package/bundled-skills/notion-manual-import/references/security-safety-notes.md +15 -0
- package/bundled-skills/observability-cloud-planning/SKILL.md +487 -0
- package/bundled-skills/observability-cloud-planning/references/common-pitfalls.md +39 -0
- package/bundled-skills/observability-cloud-planning/references/related-skills.md +20 -0
- package/bundled-skills/observability-cloud-planning/references/reusable-prompt.md +12 -0
- package/bundled-skills/observability-cloud-planning/references/security-safety-notes.md +30 -0
- package/bundled-skills/offboarding-exit/SKILL.md +343 -0
- package/bundled-skills/offer-appointment/SKILL.md +351 -0
- package/bundled-skills/okr-system/SKILL.md +355 -0
- package/bundled-skills/onboarding-playbook/SKILL.md +292 -0
- package/bundled-skills/organization-design/SKILL.md +324 -0
- package/bundled-skills/party-ledger-reconciliation/SKILL.md +411 -0
- package/bundled-skills/payment-accounting/SKILL.md +421 -0
- package/bundled-skills/payments-received/SKILL.md +315 -0
- package/bundled-skills/payroll-finance/SKILL.md +351 -0
- package/bundled-skills/people-directory/SKILL.md +459 -0
- package/bundled-skills/performance-management/SKILL.md +367 -0
- package/bundled-skills/petty-cash-management/SKILL.md +448 -0
- package/bundled-skills/policy-acknowledgement/SKILL.md +314 -0
- package/bundled-skills/policy-library/SKILL.md +327 -0
- package/bundled-skills/poster-design-studio/SKILL.md +259 -0
- package/bundled-skills/pre-boarding/SKILL.md +326 -0
- package/bundled-skills/presentation-deck/SKILL.md +491 -0
- package/bundled-skills/presentation-deck/references/related-skills.md +19 -0
- package/bundled-skills/presentation-deck/references/reusable-prompt.md +11 -0
- package/bundled-skills/probation-tracker/SKILL.md +347 -0
- package/bundled-skills/product-photo-studio/SKILL.md +259 -0
- package/bundled-skills/project-based-performance/SKILL.md +323 -0
- package/bundled-skills/projects-work-management/SKILL.md +368 -0
- package/bundled-skills/promotion-upgrade-requests/SKILL.md +377 -0
- package/bundled-skills/purchase-accounting/SKILL.md +414 -0
- package/bundled-skills/receipt-accounting/SKILL.md +424 -0
- package/bundled-skills/recognition-rewards/SKILL.md +324 -0
- package/bundled-skills/recruitment-pipeline/SKILL.md +399 -0
- package/bundled-skills/remote-work-tracker/SKILL.md +306 -0
- package/bundled-skills/repo-foundation/SKILL.md +166 -0
- package/bundled-skills/repo-foundation/references/LICENSE.md +21 -0
- package/bundled-skills/repo-foundation/references/bootstrap.md +63 -0
- package/bundled-skills/repo-foundation/references/companion.md +9 -0
- package/bundled-skills/repo-foundation/references/continuity.md +50 -0
- package/bundled-skills/repo-foundation/references/evolution.md +60 -0
- package/bundled-skills/repo-foundation/references/migration-examples.md +53 -0
- package/bundled-skills/repo-foundation/references/shared-contracts.md +48 -0
- package/bundled-skills/repo-foundation/references/verification.md +50 -0
- package/bundled-skills/repo-native-refactor/SKILL.md +225 -0
- package/bundled-skills/repo-native-refactor/references/LICENSE.md +21 -0
- package/bundled-skills/repo-native-refactor/references/deterministic-tooling.md +101 -0
- package/bundled-skills/repo-native-refactor/references/error-reliability.md +117 -0
- package/bundled-skills/repo-native-refactor/references/finding-taxonomy.md +181 -0
- package/bundled-skills/repo-native-refactor/references/refactor-examples.md +77 -0
- package/bundled-skills/repo-native-refactor/references/repository-prose.md +157 -0
- package/bundled-skills/repo-native-refactor/references/repository-rehabilitation.md +94 -0
- package/bundled-skills/repo-native-refactor/references/semantic-risk.md +187 -0
- package/bundled-skills/repo-native-refactor/references/shared-contracts.md +48 -0
- package/bundled-skills/repo-native-refactor/references/testing-integrity.md +103 -0
- package/bundled-skills/reports-analytics/SKILL.md +320 -0
- package/bundled-skills/salary-benchmarking/SKILL.md +318 -0
- package/bundled-skills/salary-wage-accounting/SKILL.md +412 -0
- package/bundled-skills/sales-accounting/SKILL.md +494 -0
- package/bundled-skills/sales-accounting/references/common-pitfalls.md +28 -0
- package/bundled-skills/sales-accounting/references/related-skills.md +13 -0
- package/bundled-skills/sales-accounting/references/reusable-prompt.md +8 -0
- package/bundled-skills/seo-directory-backlinks/SKILL.md +484 -0
- package/bundled-skills/seo-directory-backlinks/references/common-pitfalls.md +36 -0
- package/bundled-skills/seo-directory-backlinks/references/related-skills.md +15 -0
- package/bundled-skills/seo-directory-backlinks/references/reusable-prompt.md +11 -0
- package/bundled-skills/skill-gap-analysis/SKILL.md +338 -0
- package/bundled-skills/social-media-setup/SKILL.md +477 -0
- package/bundled-skills/social-media-setup/references/common-pitfalls.md +34 -0
- package/bundled-skills/social-media-setup/references/related-skills.md +19 -0
- package/bundled-skills/social-media-setup/references/reusable-prompt.md +11 -0
- package/bundled-skills/social-media-setup/references/security-safety-notes.md +27 -0
- package/bundled-skills/sop-company-wiki/SKILL.md +324 -0
- package/bundled-skills/source-document-filing/SKILL.md +368 -0
- package/bundled-skills/spreadsheet-manual-build/SKILL.md +493 -0
- package/bundled-skills/spreadsheet-manual-build/references/best-practices.md +9 -0
- package/bundled-skills/spreadsheet-manual-build/references/common-pitfalls.md +22 -0
- package/bundled-skills/spreadsheet-manual-build/references/related-skills.md +8 -0
- package/bundled-skills/spreadsheet-manual-build/references/reusable-prompt.md +9 -0
- package/bundled-skills/spreadsheet-manual-build/references/security-safety-notes.md +16 -0
- package/bundled-skills/stakeholder-investor-reports/SKILL.md +303 -0
- package/bundled-skills/suno-lyrics-to-song/SKILL.md +259 -0
- package/bundled-skills/talking-avatar-video/SKILL.md +260 -0
- package/bundled-skills/tax-register/SKILL.md +363 -0
- package/bundled-skills/tds-booking-payment/SKILL.md +405 -0
- package/bundled-skills/team-calendar/SKILL.md +312 -0
- package/bundled-skills/tech-writing-proofread/SKILL.md +78 -0
- package/bundled-skills/telegram/assets/boilerplate/nodejs/package-lock.json +3 -3
- package/bundled-skills/telegram/assets/boilerplate/nodejs/package.json +1 -0
- package/bundled-skills/template-library/SKILL.md +304 -0
- package/bundled-skills/time-tracking/SKILL.md +353 -0
- package/bundled-skills/typescript-expert/scripts/ts_diagnostic.py +40 -19
- package/bundled-skills/vendor-contractor-management/SKILL.md +327 -0
- package/bundled-skills/viral-video-teardown-remake/SKILL.md +260 -0
- package/bundled-skills/voice-cloning-studio/SKILL.md +260 -0
- package/bundled-skills/voiceover-narration-studio/SKILL.md +259 -0
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package-lock.json +3 -3
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package.json +1 -0
- package/bundled-skills/youtube-summarizer/SKILL.md +3 -3
- package/package.json +1 -1
- package/skills_index.json +254 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lint a generated blueprint (.md) for the structural guarantees the build sessions rely on.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 lint_blueprint.py path/to/blueprint.md [--strict]
|
|
6
|
+
|
|
7
|
+
Exit code 0 = no errors (warnings may remain), 1 = errors found (or warnings with --strict).
|
|
8
|
+
|
|
9
|
+
What it checks (E = error, W = warning):
|
|
10
|
+
E required top-level sections present (by English anchor in a `## ` heading)
|
|
11
|
+
E no placeholder text (TODO, TBD, TBA, FIXME, XXX, lorem, ???, [insert, to be defined/decided)
|
|
12
|
+
E at least one `## Epic nn — Title` and Epic 00 present
|
|
13
|
+
E each epic has **Definition of Done**; W each epic has **Session handoff checklist**
|
|
14
|
+
E story headings `### Enn-Snn — Title` are unique and live inside the matching epic
|
|
15
|
+
E each story has **Acceptance Criteria**, **Edge cases**, **Tests**, at least one `AC-n`, and names a persona `P<n>`
|
|
16
|
+
W each story has **Story**, **Context**, **Scope**, **Tasks**, **Story DoD**
|
|
17
|
+
E no "As a user" stories
|
|
18
|
+
E every `[UNKNOWN → OQ-nn]` has a matching OQ-nn row in the Open Questions section; bare `[UNKNOWN]` without an OQ is an error
|
|
19
|
+
E every table body row in the Tech Stack section carries [VERIFIED / [ASSUMED / [STATED (or [UNVERIFIED], which also raises W)
|
|
20
|
+
E every S-nn cited in a [VERIFIED …] tag exists in the Sources section; W fewer than 5 URLs in Sources
|
|
21
|
+
E every defined persona P<n> is used by at least one story
|
|
22
|
+
W copy IDs (CP-…) referenced in stories are defined in the Copy section
|
|
23
|
+
W dangling story references (Enn-Snn mentioned but not defined)
|
|
24
|
+
W `## ` headings with non-ASCII text carry an ASCII anchor in parentheses
|
|
25
|
+
W vague adjectives inside AC lines (properly, appropriately, fast, quickly, easily, seamless, user-friendly, intuitive)
|
|
26
|
+
W Session Protocol section mentions an Amendments log; W an appendix with session prompts exists
|
|
27
|
+
E Architecture section defines drivers AD-n and mentions fitness functions; W <3 drivers, drivers
|
|
28
|
+
with no number in them, no explicit non-drivers
|
|
29
|
+
E every AD-n referenced outside the Architecture section is defined inside it
|
|
30
|
+
W Tech Stack table has a runner-up column (a choice with no stated cost was not a decision)
|
|
31
|
+
E Quality section names a `check` command and embeds >=10 review-checklist items of the form `[ ] …`
|
|
32
|
+
W Quality section names critical e2e paths, says something about mutation testing, has M-nn metric
|
|
33
|
+
rows, and lists anti-metrics
|
|
34
|
+
E no metric row defines an output-per-person metric (lines of code, commits/PRs/diffs per engineer,
|
|
35
|
+
story points as productivity); W coverage appearing as a metric without the word "floor"
|
|
36
|
+
|
|
37
|
+
Add `<!-- lint-ignore -->` at the end of a line to suppress placeholder/adjective findings on that line
|
|
38
|
+
(e.g., the rule sentence "No TODO or TBD anywhere" in the How-to-use section).
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import re
|
|
43
|
+
import sys
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
|
|
46
|
+
REQUIRED_SECTIONS_ERROR = [
|
|
47
|
+
"How to use", "Executive summary", "Scope", "Research", "Assumptions", "Personas",
|
|
48
|
+
"Goals", "Architecture", "Tech Stack", "Quality", "Edge Cases", "Delivery Plan",
|
|
49
|
+
"Epics", "Session Protocol", "Risks", "Open Questions", "Glossary", "Sources",
|
|
50
|
+
]
|
|
51
|
+
REQUIRED_SECTIONS_WARN = ["Flows", "Screens", "Design", "UX Writing", "Copy"]
|
|
52
|
+
|
|
53
|
+
PLACEHOLDER_RE = re.compile(
|
|
54
|
+
r"\bTODO\b|\bTBD\b|\bTBA\b|\bFIXME\b|\bXXX\b|lorem ipsum|\?\?\?|\[insert|to be defined|to be decided",
|
|
55
|
+
re.IGNORECASE,
|
|
56
|
+
)
|
|
57
|
+
EPIC_HEADING_RE = re.compile(r"^## Epic (\d{2})\s*[—–-]\s*(.+?)\s*$")
|
|
58
|
+
STORY_HEADING_RE = re.compile(r"^### (E(\d{2})-S(\d{2}))\s*[—–-]\s*(.+?)\s*$")
|
|
59
|
+
STORY_REF_RE = re.compile(r"\bE\d{2}-S\d{2}\b")
|
|
60
|
+
AC_LINE_RE = re.compile(r"^\s*[-*]\s*AC-\d+", re.IGNORECASE)
|
|
61
|
+
PERSONA_REF_RE = re.compile(r"(?<![A-Za-z])P(\d+)\b")
|
|
62
|
+
UNKNOWN_RE = re.compile(r"\[UNKNOWN([^\]]*)\]")
|
|
63
|
+
OQ_IN_UNKNOWN_RE = re.compile(r"OQ-(\d+)")
|
|
64
|
+
VERIFIED_SOURCE_RE = re.compile(r"\[VERIFIED[^\]]*?\bS-(\d+)")
|
|
65
|
+
SOURCE_ID_RE = re.compile(r"\bS-(\d+)\b")
|
|
66
|
+
URL_RE = re.compile(r"https?://\S+")
|
|
67
|
+
CP_ID_RE = re.compile(r"\bCP-[A-Za-z0-9]+-\d+\b")
|
|
68
|
+
EVIDENCE_TAG_RE = re.compile(r"\[(VERIFIED|ASSUMED|STATED|UNVERIFIED)\b")
|
|
69
|
+
UNVERIFIED_RE = re.compile(r"\[UNVERIFIED\]")
|
|
70
|
+
VAGUE_RE = re.compile(
|
|
71
|
+
r"\b(properly|appropriately|fast|quickly|easily|seamless(?:ly)?|user-friendly|intuitive(?:ly)?)\b",
|
|
72
|
+
re.IGNORECASE,
|
|
73
|
+
)
|
|
74
|
+
AS_A_USER_RE = re.compile(r"\bAs an? user\b", re.IGNORECASE)
|
|
75
|
+
AD_ID_RE = re.compile(r"\bAD-(\d+)\b")
|
|
76
|
+
DIGIT_RE = re.compile(r"\d")
|
|
77
|
+
FITNESS_RE = re.compile(r"fitness function", re.IGNORECASE)
|
|
78
|
+
CHECK_CMD_RE = re.compile(r"`[^`\n]*\bcheck\b[^`\n]*`")
|
|
79
|
+
CHECKLIST_ITEM_RE = re.compile(r"^\s*(?:[-*]\s*)?\[ \]\s+\S")
|
|
80
|
+
METRIC_ROW_RE = re.compile(r"\bM-\d+(?:\.\d+)?\b")
|
|
81
|
+
BANNED_METRIC_RE = re.compile(
|
|
82
|
+
r"lines of code|\bLOC\b|commits? per (?:engineer|developer|dev|person)"
|
|
83
|
+
r"|(?:pull requests?|PRs?|diffs?) per (?:engineer|developer|dev|person)"
|
|
84
|
+
r"|story points? (?:per|as a measure|completed per)|velocity per",
|
|
85
|
+
re.IGNORECASE,
|
|
86
|
+
)
|
|
87
|
+
COVERAGE_TARGET_RE = re.compile(r"coverage", re.IGNORECASE)
|
|
88
|
+
LINT_IGNORE = "<!-- lint-ignore -->"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Findings:
|
|
93
|
+
errors: list[str] = field(default_factory=list)
|
|
94
|
+
warnings: list[str] = field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
def err(self, line: int | None, msg: str) -> None:
|
|
97
|
+
self.errors.append(f"L{line}: {msg}" if line else msg)
|
|
98
|
+
|
|
99
|
+
def warn(self, line: int | None, msg: str) -> None:
|
|
100
|
+
self.warnings.append(f"L{line}: {msg}" if line else msg)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Section:
|
|
105
|
+
title: str
|
|
106
|
+
start: int # 0-based index of heading line
|
|
107
|
+
end: int # exclusive
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def level2_sections(lines: list[str]) -> list[Section]:
|
|
111
|
+
idx = [i for i, l in enumerate(lines) if l.startswith("## ")]
|
|
112
|
+
sections = []
|
|
113
|
+
for n, i in enumerate(idx):
|
|
114
|
+
end = idx[n + 1] if n + 1 < len(idx) else len(lines)
|
|
115
|
+
sections.append(Section(lines[i][3:].strip(), i, end))
|
|
116
|
+
return sections
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def find_section(sections: list[Section], anchor: str) -> Section | None:
|
|
120
|
+
a = anchor.lower()
|
|
121
|
+
for s in sections:
|
|
122
|
+
if EPIC_HEADING_RE.match("## " + s.title):
|
|
123
|
+
continue
|
|
124
|
+
if a in s.title.lower():
|
|
125
|
+
return s
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def block_text(lines: list[str], start: int, end: int) -> str:
|
|
130
|
+
return "\n".join(lines[start:end])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def table_body_rows(lines: list[str], start: int, end: int) -> list[tuple[int, str]]:
|
|
134
|
+
"""Return (line_no, row) for table rows that are neither header nor separator."""
|
|
135
|
+
rows = []
|
|
136
|
+
in_table = False
|
|
137
|
+
header_seen = False
|
|
138
|
+
for i in range(start, end):
|
|
139
|
+
l = lines[i].strip()
|
|
140
|
+
if l.startswith("|"):
|
|
141
|
+
if not in_table:
|
|
142
|
+
in_table, header_seen = True, True # first row of a table is its header
|
|
143
|
+
continue
|
|
144
|
+
if re.match(r"^\|[\s:\-|]+\|$", l):
|
|
145
|
+
continue # separator
|
|
146
|
+
rows.append((i + 1, l))
|
|
147
|
+
else:
|
|
148
|
+
in_table, header_seen = False, False
|
|
149
|
+
return rows
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main(argv: list[str]) -> int:
|
|
153
|
+
if len(argv) < 2 or argv[1] in {"-h", "--help"}:
|
|
154
|
+
print(__doc__)
|
|
155
|
+
return 0
|
|
156
|
+
path = argv[1]
|
|
157
|
+
strict = "--strict" in argv
|
|
158
|
+
with open(path, encoding="utf-8") as f:
|
|
159
|
+
lines = f.read().splitlines()
|
|
160
|
+
f_ = Findings()
|
|
161
|
+
sections = level2_sections(lines)
|
|
162
|
+
|
|
163
|
+
# 1. Required sections
|
|
164
|
+
for anchor in REQUIRED_SECTIONS_ERROR:
|
|
165
|
+
if not find_section(sections, anchor):
|
|
166
|
+
f_.err(None, f"Missing required section with anchor '{anchor}' in a '## ' heading")
|
|
167
|
+
for anchor in REQUIRED_SECTIONS_WARN:
|
|
168
|
+
if not find_section(sections, anchor):
|
|
169
|
+
f_.warn(None, f"No section with anchor '{anchor}' — fine only if it truly does not apply (say so in the doc)")
|
|
170
|
+
|
|
171
|
+
# 2. Placeholders + vague adjectives + "As a user"
|
|
172
|
+
for i, l in enumerate(lines, 1):
|
|
173
|
+
if LINT_IGNORE in l:
|
|
174
|
+
continue
|
|
175
|
+
m = PLACEHOLDER_RE.search(l)
|
|
176
|
+
if m:
|
|
177
|
+
f_.err(i, f"Placeholder text '{m.group(0)}' — decide it or route to an [UNKNOWN → OQ-nn]")
|
|
178
|
+
if AC_LINE_RE.match(l):
|
|
179
|
+
v = VAGUE_RE.search(l)
|
|
180
|
+
if v:
|
|
181
|
+
f_.warn(i, f"Vague word '{v.group(0)}' in an acceptance criterion — replace with a number or observable result")
|
|
182
|
+
if AS_A_USER_RE.search(l):
|
|
183
|
+
f_.err(i, "'As a user' — stories must name a persona ID and name (e.g. 'As **P2 (Owner)**')")
|
|
184
|
+
|
|
185
|
+
# 3. Epics and stories
|
|
186
|
+
epics: list[tuple[int, str, Section]] = [] # (epic_no, title, section)
|
|
187
|
+
for s in sections:
|
|
188
|
+
m = EPIC_HEADING_RE.match("## " + s.title)
|
|
189
|
+
if m:
|
|
190
|
+
epics.append((int(m.group(1)), m.group(2), s))
|
|
191
|
+
if not epics:
|
|
192
|
+
f_.err(None, "No epics found — expected headings like '## Epic 00 — Foundation'")
|
|
193
|
+
elif not any(n == 0 for n, _, _ in epics):
|
|
194
|
+
f_.err(None, "Epic 00 (Foundation) is missing")
|
|
195
|
+
|
|
196
|
+
story_ids: dict[str, int] = {}
|
|
197
|
+
story_blocks: list[tuple[str, int, int, int]] = [] # (id, epic_no, start, end)
|
|
198
|
+
for epic_no, epic_title, sec in epics:
|
|
199
|
+
text = block_text(lines, sec.start, sec.end)
|
|
200
|
+
if "**Definition of Done**" not in text:
|
|
201
|
+
f_.err(sec.start + 1, f"Epic {epic_no:02d} has no **Definition of Done**")
|
|
202
|
+
if "**Session handoff checklist**" not in text:
|
|
203
|
+
f_.warn(sec.start + 1, f"Epic {epic_no:02d} has no **Session handoff checklist**")
|
|
204
|
+
# stories inside this epic
|
|
205
|
+
heads = [i for i in range(sec.start + 1, sec.end) if lines[i].startswith("### ")]
|
|
206
|
+
if not heads:
|
|
207
|
+
f_.err(sec.start + 1, f"Epic {epic_no:02d} has no stories ('### Enn-Snn — Title')")
|
|
208
|
+
for n, i in enumerate(heads):
|
|
209
|
+
end = heads[n + 1] if n + 1 < len(heads) else sec.end
|
|
210
|
+
m = STORY_HEADING_RE.match(lines[i])
|
|
211
|
+
if not m:
|
|
212
|
+
f_.warn(i + 1, f"Level-3 heading inside Epic {epic_no:02d} is not a story heading: '{lines[i][4:60]}'")
|
|
213
|
+
continue
|
|
214
|
+
sid, e_no = m.group(1), int(m.group(2))
|
|
215
|
+
if sid in story_ids:
|
|
216
|
+
f_.err(i + 1, f"Duplicate story ID {sid} (first at L{story_ids[sid]})")
|
|
217
|
+
story_ids[sid] = i + 1
|
|
218
|
+
if e_no != epic_no:
|
|
219
|
+
f_.err(i + 1, f"Story {sid} is inside Epic {epic_no:02d} but its ID says E{e_no:02d}")
|
|
220
|
+
story_blocks.append((sid, epic_no, i, end))
|
|
221
|
+
|
|
222
|
+
all_story_text = ""
|
|
223
|
+
for sid, _, start, end in story_blocks:
|
|
224
|
+
text = block_text(lines, start, end)
|
|
225
|
+
all_story_text += "\n" + text
|
|
226
|
+
for label in ("**Acceptance Criteria**", "**Edge cases**", "**Tests**"):
|
|
227
|
+
if label not in text:
|
|
228
|
+
f_.err(start + 1, f"{sid} is missing {label}")
|
|
229
|
+
for label in ("**Story**", "**Context**", "**Scope**", "**Tasks**", "**Story DoD**"):
|
|
230
|
+
if label not in text:
|
|
231
|
+
f_.warn(start + 1, f"{sid} is missing {label}")
|
|
232
|
+
if not any(AC_LINE_RE.match(l) for l in lines[start:end]):
|
|
233
|
+
f_.err(start + 1, f"{sid} has no 'AC-n' lines")
|
|
234
|
+
if not PERSONA_REF_RE.search(text):
|
|
235
|
+
f_.err(start + 1, f"{sid} does not reference a persona ID (P1, P2, … or P0 for the builder)")
|
|
236
|
+
|
|
237
|
+
# dangling story references
|
|
238
|
+
defined = set(story_ids)
|
|
239
|
+
for i, l in enumerate(lines, 1):
|
|
240
|
+
for ref in STORY_REF_RE.findall(l):
|
|
241
|
+
if ref not in defined:
|
|
242
|
+
f_.warn(i, f"Reference to undefined story {ref}")
|
|
243
|
+
|
|
244
|
+
# 4. UNKNOWN → OQ
|
|
245
|
+
oq_sec = find_section(sections, "Open Questions")
|
|
246
|
+
oq_text = block_text(lines, oq_sec.start, oq_sec.end) if oq_sec else ""
|
|
247
|
+
for i, l in enumerate(lines, 1):
|
|
248
|
+
for m in UNKNOWN_RE.finditer(l):
|
|
249
|
+
oqs = OQ_IN_UNKNOWN_RE.findall(m.group(1))
|
|
250
|
+
if not oqs:
|
|
251
|
+
f_.err(i, "[UNKNOWN] without an OQ reference — use '[UNKNOWN → OQ-nn]'")
|
|
252
|
+
for q in oqs:
|
|
253
|
+
if oq_sec and not re.search(rf"\bOQ-{q}\b", oq_text):
|
|
254
|
+
f_.err(i, f"OQ-{q} referenced but not present in the Open Questions section")
|
|
255
|
+
|
|
256
|
+
# 5. Tech stack evidence
|
|
257
|
+
ts = find_section(sections, "Tech Stack")
|
|
258
|
+
if ts:
|
|
259
|
+
rows = table_body_rows(lines, ts.start, ts.end)
|
|
260
|
+
if not rows:
|
|
261
|
+
f_.err(ts.start + 1, "Tech Stack section has no table rows")
|
|
262
|
+
for ln, row in rows:
|
|
263
|
+
if not EVIDENCE_TAG_RE.search(row):
|
|
264
|
+
f_.err(ln, "Tech Stack table row without an evidence tag ([VERIFIED …] / [ASSUMED …] / [STATED])")
|
|
265
|
+
|
|
266
|
+
# 6. Sources
|
|
267
|
+
src = find_section(sections, "Sources")
|
|
268
|
+
if src:
|
|
269
|
+
src_text = block_text(lines, src.start, src.end)
|
|
270
|
+
defined_sources = set(SOURCE_ID_RE.findall(src_text))
|
|
271
|
+
urls = URL_RE.findall(src_text)
|
|
272
|
+
if len(urls) < 5:
|
|
273
|
+
f_.warn(src.start + 1, f"Only {len(urls)} URL(s) in Sources — deep research normally yields far more")
|
|
274
|
+
for i, l in enumerate(lines, 1):
|
|
275
|
+
if src.start < i - 1 < src.end:
|
|
276
|
+
continue
|
|
277
|
+
for s_id in VERIFIED_SOURCE_RE.findall(l):
|
|
278
|
+
if s_id not in defined_sources:
|
|
279
|
+
f_.err(i, f"[VERIFIED …] cites S-{s_id}, which is not in the Sources section")
|
|
280
|
+
|
|
281
|
+
# 7. Personas used
|
|
282
|
+
per = find_section(sections, "Personas")
|
|
283
|
+
if per:
|
|
284
|
+
per_text = block_text(lines, per.start, per.end)
|
|
285
|
+
defined_personas = sorted({int(x) for x in PERSONA_REF_RE.findall(per_text)})
|
|
286
|
+
if not defined_personas:
|
|
287
|
+
f_.err(per.start + 1, "Personas section defines no persona IDs (P1, P2, …)")
|
|
288
|
+
used = {int(x) for x in PERSONA_REF_RE.findall(all_story_text)}
|
|
289
|
+
for p in defined_personas:
|
|
290
|
+
if p not in used:
|
|
291
|
+
f_.err(per.start + 1, f"Persona P{p} is never referenced by any story — remove it or write its stories")
|
|
292
|
+
|
|
293
|
+
# 8. Copy IDs referenced vs defined
|
|
294
|
+
cp = find_section(sections, "Copy")
|
|
295
|
+
if cp:
|
|
296
|
+
cp_defined = set(CP_ID_RE.findall(block_text(lines, cp.start, cp.end)))
|
|
297
|
+
for sid, _, start, end in story_blocks:
|
|
298
|
+
for ref in set(CP_ID_RE.findall(block_text(lines, start, end))):
|
|
299
|
+
if ref not in cp_defined:
|
|
300
|
+
f_.warn(start + 1, f"{sid} references copy ID {ref} that is not defined in the Copy section")
|
|
301
|
+
|
|
302
|
+
# 9. Heading anchors
|
|
303
|
+
for s in sections:
|
|
304
|
+
if any(ord(ch) > 127 and ch.isalpha() for ch in s.title) and not re.search(r"\([A-Za-z][A-Za-z0-9 &/\-]*\)", s.title):
|
|
305
|
+
f_.warn(s.start + 1, f"Heading '{s.title[:40]}' has no ASCII anchor in parentheses")
|
|
306
|
+
|
|
307
|
+
# 10. Session protocol / appendix
|
|
308
|
+
sp = find_section(sections, "Session Protocol")
|
|
309
|
+
if sp and "amendment" not in block_text(lines, sp.start, sp.end).lower():
|
|
310
|
+
f_.warn(sp.start + 1, "Session Protocol section does not mention the Amendments log")
|
|
311
|
+
if not find_section(sections, "Session Prompts"):
|
|
312
|
+
f_.warn(None, "No appendix with 'Session Prompts' — the user needs one copy-paste prompt per epic")
|
|
313
|
+
|
|
314
|
+
# 11. UNVERIFIED tags (only legitimate when web tools were unavailable)
|
|
315
|
+
unverified = sum(len(UNVERIFIED_RE.findall(l)) for l in lines)
|
|
316
|
+
if unverified:
|
|
317
|
+
f_.warn(None, f"{unverified} [UNVERIFIED] tag(s) — acceptable only with the no-web-access warning on the cover; the first build session must re-verify each")
|
|
318
|
+
|
|
319
|
+
# 12. Architecture drivers and the style decision
|
|
320
|
+
arch = find_section(sections, "Architecture")
|
|
321
|
+
ad_defined: set[str] = set()
|
|
322
|
+
if arch:
|
|
323
|
+
arch_text = block_text(lines, arch.start, arch.end)
|
|
324
|
+
# A driver is *defined* where its ID opens the line (optionally after a list marker, bold
|
|
325
|
+
# marker or heading hashes) or opens a table row's first cell. Everywhere else is a reference.
|
|
326
|
+
for i in range(arch.start, min(arch.end, len(lines))):
|
|
327
|
+
l = lines[i]
|
|
328
|
+
head = l.lstrip().lstrip("#*->|").lstrip().lstrip("*_ ")
|
|
329
|
+
m = AD_ID_RE.match(head)
|
|
330
|
+
if m:
|
|
331
|
+
ad_defined.add(m.group(1))
|
|
332
|
+
if not ad_defined:
|
|
333
|
+
f_.err(
|
|
334
|
+
arch.start + 1,
|
|
335
|
+
"Architecture section defines no drivers (AD-1, AD-2, …) — the style decision has "
|
|
336
|
+
"nothing to be scored against, which is how a stack gets chosen first and justified after",
|
|
337
|
+
)
|
|
338
|
+
elif len(ad_defined) < 3:
|
|
339
|
+
f_.warn(arch.start + 1, f"Only {len(ad_defined)} architecture driver(s) — 5–8 ranked drivers is the target")
|
|
340
|
+
# Each driver definition line must carry a measurable target. Strip the AD-n IDs first:
|
|
341
|
+
# the digit in the ID itself does not make the driver measurable.
|
|
342
|
+
for i in range(arch.start, min(arch.end, len(lines))):
|
|
343
|
+
l = lines[i]
|
|
344
|
+
head = l.lstrip().lstrip("#*->|").lstrip().lstrip("*_ ")
|
|
345
|
+
if LINT_IGNORE in l or not AD_ID_RE.match(head):
|
|
346
|
+
continue
|
|
347
|
+
if not DIGIT_RE.search(AD_ID_RE.sub("", l)):
|
|
348
|
+
f_.warn(
|
|
349
|
+
i + 1,
|
|
350
|
+
"Architecture driver with no number in it — a driver without a measurable target "
|
|
351
|
+
"cannot decide anything ('scalable' is not a driver)",
|
|
352
|
+
)
|
|
353
|
+
if not FITNESS_RE.search(arch_text):
|
|
354
|
+
f_.err(
|
|
355
|
+
arch.start + 1,
|
|
356
|
+
"Architecture section never mentions fitness functions — boundaries that are described "
|
|
357
|
+
"but not enforced are gone within a few sessions",
|
|
358
|
+
)
|
|
359
|
+
if "non-driver" not in arch_text.lower() and "not optimis" not in arch_text.lower() and "not optimiz" not in arch_text.lower():
|
|
360
|
+
f_.warn(arch.start + 1, "No explicit non-drivers ('what we are NOT optimising for') in the Architecture section")
|
|
361
|
+
|
|
362
|
+
# Every AD-n referenced anywhere must be defined by a driver line in the Architecture section
|
|
363
|
+
if ad_defined:
|
|
364
|
+
for i, l in enumerate(lines, 1):
|
|
365
|
+
head = l.lstrip().lstrip("#*->|").lstrip().lstrip("*_ ")
|
|
366
|
+
if arch and arch.start < i - 1 < arch.end and AD_ID_RE.match(head):
|
|
367
|
+
continue # this is the definition line itself
|
|
368
|
+
for ad in AD_ID_RE.findall(l):
|
|
369
|
+
if ad not in ad_defined:
|
|
370
|
+
f_.err(i, f"AD-{ad} is referenced but never defined as a driver in the Architecture section")
|
|
371
|
+
|
|
372
|
+
# 13. Tech stack table shape — every row must name what the runner-up would have bought
|
|
373
|
+
if ts:
|
|
374
|
+
header = ""
|
|
375
|
+
for i in range(ts.start, min(ts.end, len(lines))):
|
|
376
|
+
if lines[i].lstrip().startswith("|") and "---" not in lines[i]:
|
|
377
|
+
header = lines[i].lower()
|
|
378
|
+
break
|
|
379
|
+
if header and "runner" not in header:
|
|
380
|
+
f_.warn(
|
|
381
|
+
ts.start + 1,
|
|
382
|
+
"Tech Stack table has no runner-up column — a choice with no stated cost was not a decision",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# 14. Quality section: the gate, the checklist, the critical paths
|
|
386
|
+
q = find_section(sections, "Quality")
|
|
387
|
+
if q:
|
|
388
|
+
q_lines = lines[q.start : q.end]
|
|
389
|
+
q_text = "\n".join(q_lines)
|
|
390
|
+
q_lower = q_text.lower()
|
|
391
|
+
if not CHECK_CMD_RE.search(q_text):
|
|
392
|
+
f_.err(
|
|
393
|
+
q.start + 1,
|
|
394
|
+
"Quality section names no `check` command in backticks — the build sessions have no "
|
|
395
|
+
"definition of green",
|
|
396
|
+
)
|
|
397
|
+
checklist_items = sum(1 for l in q_lines if CHECKLIST_ITEM_RE.match(l))
|
|
398
|
+
if checklist_items < 10:
|
|
399
|
+
f_.err(
|
|
400
|
+
q.start + 1,
|
|
401
|
+
f"Quality section contains {checklist_items} review-checklist item(s) of the form '[ ] …' — "
|
|
402
|
+
"embed the review checklist verbatim (code-quality-and-review.md §11) or the reviewer "
|
|
403
|
+
"invents a different standard every session",
|
|
404
|
+
)
|
|
405
|
+
if "e2e" not in q_lower and "end-to-end" not in q_lower:
|
|
406
|
+
f_.warn(q.start + 1, "Quality section does not name the critical e2e paths")
|
|
407
|
+
if "mutation" not in q_lower:
|
|
408
|
+
f_.warn(
|
|
409
|
+
q.start + 1,
|
|
410
|
+
"Quality section says nothing about mutation testing — name the tool for this stack, "
|
|
411
|
+
"or state that no maintained one exists",
|
|
412
|
+
)
|
|
413
|
+
if not METRIC_ROW_RE.search(q_text):
|
|
414
|
+
f_.warn(q.start + 1, "Quality section has no metrics table rows (M-nn) — nothing will be measured")
|
|
415
|
+
if "anti-metric" not in q_lower and "will not measure" not in q_lower and "not measure" not in q_lower:
|
|
416
|
+
f_.warn(
|
|
417
|
+
q.start + 1,
|
|
418
|
+
"Quality section lists no anti-metrics — say explicitly what will not be measured and why",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# 15. Banned productivity metrics inside metric definition rows
|
|
422
|
+
for i, l in enumerate(lines, 1):
|
|
423
|
+
if LINT_IGNORE in l or not METRIC_ROW_RE.search(l):
|
|
424
|
+
continue
|
|
425
|
+
m = BANNED_METRIC_RE.search(l)
|
|
426
|
+
if m:
|
|
427
|
+
f_.err(
|
|
428
|
+
i,
|
|
429
|
+
f"Metric row defines '{m.group(0)}' — output-per-person metrics are gamed the moment they "
|
|
430
|
+
"become targets; measure the system, not the author",
|
|
431
|
+
)
|
|
432
|
+
if COVERAGE_TARGET_RE.search(l) and "floor" not in l.lower():
|
|
433
|
+
f_.warn(
|
|
434
|
+
i,
|
|
435
|
+
"Coverage appears as a metric without the word 'floor' — state it as a regression floor, "
|
|
436
|
+
"never as a target to raise",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# 16. Report
|
|
440
|
+
print(f"Blueprint lint — {path}")
|
|
441
|
+
print(f" epics: {len(epics)} stories: {len(story_ids)} sections: {len(sections)}")
|
|
442
|
+
if f_.errors:
|
|
443
|
+
print(f"\nERRORS ({len(f_.errors)}):")
|
|
444
|
+
for e in f_.errors:
|
|
445
|
+
print(" ✘ " + e)
|
|
446
|
+
if f_.warnings:
|
|
447
|
+
print(f"\nWARNINGS ({len(f_.warnings)}):")
|
|
448
|
+
for w in f_.warnings:
|
|
449
|
+
print(" ! " + w)
|
|
450
|
+
if not f_.errors and not f_.warnings:
|
|
451
|
+
print("\nPASS — no findings.")
|
|
452
|
+
elif not f_.errors:
|
|
453
|
+
print("\nPASS with warnings — resolve or justify each warning in the document.")
|
|
454
|
+
else:
|
|
455
|
+
print("\nFAIL — fix every error before delivering the blueprint.")
|
|
456
|
+
return 1 if f_.errors or (strict and f_.warnings) else 0
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
if __name__ == "__main__":
|
|
460
|
+
sys.exit(main(sys.argv))
|