godpowers 0.15.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/AGENTS.md +37 -0
- package/CHANGELOG.md +639 -0
- package/INSPIRATION.md +52 -0
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/SKILL.md +500 -0
- package/agents/god-archaeologist.md +139 -0
- package/agents/god-architect.md +92 -0
- package/agents/god-auditor.md +150 -0
- package/agents/god-browser-tester.md +144 -0
- package/agents/god-context-writer.md +137 -0
- package/agents/god-coordinator.md +138 -0
- package/agents/god-debt-assessor.md +132 -0
- package/agents/god-debugger.md +77 -0
- package/agents/god-deploy-engineer.md +87 -0
- package/agents/god-deps-auditor.md +111 -0
- package/agents/god-design-reviewer.md +137 -0
- package/agents/god-designer.md +171 -0
- package/agents/god-docs-writer.md +102 -0
- package/agents/god-executor.md +76 -0
- package/agents/god-explorer.md +110 -0
- package/agents/god-harden-auditor.md +163 -0
- package/agents/god-incident-investigator.md +144 -0
- package/agents/god-launch-strategist.md +103 -0
- package/agents/god-migration-strategist.md +126 -0
- package/agents/god-observability-engineer.md +76 -0
- package/agents/god-orchestrator.md +728 -0
- package/agents/god-org-context-loader.md +124 -0
- package/agents/god-planner.md +73 -0
- package/agents/god-pm.md +105 -0
- package/agents/god-quality-reviewer.md +74 -0
- package/agents/god-reconciler.md +230 -0
- package/agents/god-reconstructor.md +124 -0
- package/agents/god-repo-scaffolder.md +60 -0
- package/agents/god-retrospective.md +109 -0
- package/agents/god-roadmap-reconciler.md +123 -0
- package/agents/god-roadmap-updater.md +89 -0
- package/agents/god-roadmapper.md +82 -0
- package/agents/god-spec-reviewer.md +70 -0
- package/agents/god-spike-runner.md +119 -0
- package/agents/god-stack-selector.md +93 -0
- package/agents/god-standards-check.md +132 -0
- package/agents/god-storyteller.md +116 -0
- package/agents/god-updater.md +174 -0
- package/bin/install.js +514 -0
- package/extensions/data-pack/README.md +33 -0
- package/extensions/data-pack/agents/god-dashboard-builder.md +66 -0
- package/extensions/data-pack/agents/god-etl-engineer.md +64 -0
- package/extensions/data-pack/agents/god-ml-feature-engineer.md +66 -0
- package/extensions/data-pack/manifest.yaml +39 -0
- package/extensions/data-pack/package.json +42 -0
- package/extensions/data-pack/skills/god-dashboard.md +28 -0
- package/extensions/data-pack/skills/god-etl.md +28 -0
- package/extensions/data-pack/skills/god-ml-feature.md +28 -0
- package/extensions/data-pack/workflows/dashboard-arc.yaml +13 -0
- package/extensions/data-pack/workflows/etl-arc.yaml +13 -0
- package/extensions/data-pack/workflows/ml-feature-arc.yaml +13 -0
- package/extensions/launch-pack/README.md +36 -0
- package/extensions/launch-pack/agents/god-indie-hackers-strategist.md +128 -0
- package/extensions/launch-pack/agents/god-oss-release-strategist.md +125 -0
- package/extensions/launch-pack/agents/god-product-hunt-strategist.md +118 -0
- package/extensions/launch-pack/agents/god-show-hn-strategist.md +113 -0
- package/extensions/launch-pack/manifest.yaml +45 -0
- package/extensions/launch-pack/package.json +41 -0
- package/extensions/launch-pack/skills/god-indie-hackers.md +39 -0
- package/extensions/launch-pack/skills/god-oss-release.md +43 -0
- package/extensions/launch-pack/skills/god-product-hunt.md +41 -0
- package/extensions/launch-pack/skills/god-show-hn.md +40 -0
- package/extensions/launch-pack/workflows/indie-hackers.yaml +13 -0
- package/extensions/launch-pack/workflows/oss-release.yaml +13 -0
- package/extensions/launch-pack/workflows/product-hunt.yaml +13 -0
- package/extensions/launch-pack/workflows/show-hn.yaml +13 -0
- package/extensions/security-pack/README.md +48 -0
- package/extensions/security-pack/agents/god-hipaa-auditor.md +117 -0
- package/extensions/security-pack/agents/god-pci-auditor.md +100 -0
- package/extensions/security-pack/agents/god-soc2-auditor.md +107 -0
- package/extensions/security-pack/manifest.yaml +39 -0
- package/extensions/security-pack/package.json +42 -0
- package/extensions/security-pack/skills/god-hipaa-audit.md +41 -0
- package/extensions/security-pack/skills/god-pci-audit.md +40 -0
- package/extensions/security-pack/skills/god-soc2-audit.md +42 -0
- package/extensions/security-pack/workflows/hipaa-arc.yaml +15 -0
- package/extensions/security-pack/workflows/pci-arc.yaml +15 -0
- package/extensions/security-pack/workflows/soc2-arc.yaml +15 -0
- package/hooks/pre-tool-use.sh +40 -0
- package/hooks/session-start.sh +74 -0
- package/lib/README.md +28 -0
- package/lib/agent-browser-driver.js +215 -0
- package/lib/agent-cache.js +194 -0
- package/lib/agent-validator.js +275 -0
- package/lib/artifact-diff.js +168 -0
- package/lib/artifact-linter.js +142 -0
- package/lib/awesome-design.js +312 -0
- package/lib/browser-bridge.js +209 -0
- package/lib/budget.js +215 -0
- package/lib/checkpoint.js +390 -0
- package/lib/code-scanner.js +262 -0
- package/lib/context-budget.js +170 -0
- package/lib/context-writer.js +348 -0
- package/lib/cost-tracker.js +325 -0
- package/lib/cross-artifact-impact.js +162 -0
- package/lib/cross-repo-linkage.js +150 -0
- package/lib/design-detector.js +167 -0
- package/lib/design-spec.js +348 -0
- package/lib/drift-detector.js +212 -0
- package/lib/event-reader.js +174 -0
- package/lib/events.js +183 -0
- package/lib/extensions.js +257 -0
- package/lib/have-nots-validator.js +647 -0
- package/lib/impact.js +314 -0
- package/lib/impeccable-bridge.js +139 -0
- package/lib/intent.js +177 -0
- package/lib/linkage.js +232 -0
- package/lib/meta-linter.js +263 -0
- package/lib/multi-repo-detector.js +182 -0
- package/lib/otel-exporter.js +308 -0
- package/lib/recipes.js +186 -0
- package/lib/reverse-sync.js +332 -0
- package/lib/review-required.js +224 -0
- package/lib/router.js +278 -0
- package/lib/runtime-audit.js +455 -0
- package/lib/runtime-test.js +309 -0
- package/lib/skillui-bridge.js +216 -0
- package/lib/state-lock.js +201 -0
- package/lib/state.js +142 -0
- package/lib/story-validator.js +301 -0
- package/lib/suite-state.js +220 -0
- package/lib/workflow-parser.js +109 -0
- package/lib/workflow-runner.js +221 -0
- package/package.json +63 -0
- package/references/HAVE-NOTS.md +573 -0
- package/references/building/BUILD-ANTIPATTERNS.md +102 -0
- package/references/building/BUILD-VERTICAL-SLICES.md +75 -0
- package/references/building/BUILD-WAVES.md +61 -0
- package/references/building/README.md +17 -0
- package/references/design/COLOR.md +122 -0
- package/references/design/DESIGN-ANATOMY.md +121 -0
- package/references/design/DESIGN-ANTIPATTERNS.md +108 -0
- package/references/design/INTERACTION.md +148 -0
- package/references/design/MOTION.md +120 -0
- package/references/design/RESPONSIVE.md +157 -0
- package/references/design/SPATIAL.md +109 -0
- package/references/design/TYPOGRAPHY.md +121 -0
- package/references/design/UX-WRITING.md +135 -0
- package/references/orchestration/MODE-DETECTION.md +74 -0
- package/references/orchestration/README.md +18 -0
- package/references/orchestration/SCALE-DETECTION.md +81 -0
- package/references/planning/ARCH-ANATOMY.md +143 -0
- package/references/planning/ARCH-ANTIPATTERNS.md +52 -0
- package/references/planning/PRD-ANATOMY.md +117 -0
- package/references/planning/PRD-ANTIPATTERNS.md +138 -0
- package/references/planning/README.md +16 -0
- package/references/planning/ROADMAP-ANATOMY.md +43 -0
- package/references/planning/ROADMAP-ANTIPATTERNS.md +94 -0
- package/references/planning/STACK-ANATOMY.md +60 -0
- package/references/planning/STACK-ANTIPATTERNS.md +95 -0
- package/references/shared/GLOSSARY.md +80 -0
- package/references/shared/ORCHESTRATORS.md +76 -0
- package/references/shared/README.md +14 -0
- package/references/shipping/DEPLOY-ANTIPATTERNS.md +64 -0
- package/references/shipping/DEPLOY-PATTERNS.md +110 -0
- package/references/shipping/HARDEN-ANTIPATTERNS.md +66 -0
- package/references/shipping/HARDEN-OWASP-WORKSHEETS.md +89 -0
- package/references/shipping/LAUNCH-ANTIPATTERNS.md +68 -0
- package/references/shipping/OBSERVE-ANTIPATTERNS.md +62 -0
- package/references/shipping/OBSERVE-SLO-EXAMPLES.md +107 -0
- package/references/shipping/README.md +18 -0
- package/routing/god-add-backlog.yaml +24 -0
- package/routing/god-add-tests.yaml +27 -0
- package/routing/god-add-todo.yaml +24 -0
- package/routing/god-agent-audit.yaml +24 -0
- package/routing/god-arch.yaml +46 -0
- package/routing/god-archaeology.yaml +28 -0
- package/routing/god-audit.yaml +32 -0
- package/routing/god-budget.yaml +24 -0
- package/routing/god-build-agent.yaml +24 -0
- package/routing/god-build.yaml +46 -0
- package/routing/god-cache-clear.yaml +24 -0
- package/routing/god-check-todos.yaml +24 -0
- package/routing/god-context-scan.yaml +24 -0
- package/routing/god-context.yaml +44 -0
- package/routing/god-cost.yaml +24 -0
- package/routing/god-debug.yaml +28 -0
- package/routing/god-deploy.yaml +34 -0
- package/routing/god-design-impact.yaml +25 -0
- package/routing/god-design.yaml +67 -0
- package/routing/god-discuss.yaml +27 -0
- package/routing/god-docs.yaml +33 -0
- package/routing/god-doctor.yaml +27 -0
- package/routing/god-explore.yaml +27 -0
- package/routing/god-extension-add.yaml +24 -0
- package/routing/god-extension-info.yaml +24 -0
- package/routing/god-extension-list.yaml +24 -0
- package/routing/god-extension-remove.yaml +24 -0
- package/routing/god-extract-learnings.yaml +24 -0
- package/routing/god-fast.yaml +27 -0
- package/routing/god-feature.yaml +34 -0
- package/routing/god-graph.yaml +24 -0
- package/routing/god-harden.yaml +41 -0
- package/routing/god-help.yaml +27 -0
- package/routing/god-hotfix.yaml +34 -0
- package/routing/god-hygiene.yaml +28 -0
- package/routing/god-init.yaml +37 -0
- package/routing/god-intel.yaml +24 -0
- package/routing/god-launch.yaml +41 -0
- package/routing/god-lifecycle.yaml +27 -0
- package/routing/god-link.yaml +24 -0
- package/routing/god-lint.yaml +24 -0
- package/routing/god-list-assumptions.yaml +27 -0
- package/routing/god-locate.yaml +24 -0
- package/routing/god-logs.yaml +24 -0
- package/routing/god-map-codebase.yaml +24 -0
- package/routing/god-metrics.yaml +24 -0
- package/routing/god-mode.yaml +31 -0
- package/routing/god-next.yaml +27 -0
- package/routing/god-note.yaml +24 -0
- package/routing/god-observe.yaml +34 -0
- package/routing/god-org-context.yaml +28 -0
- package/routing/god-party.yaml +24 -0
- package/routing/god-pause-work.yaml +27 -0
- package/routing/god-plant-seed.yaml +24 -0
- package/routing/god-postmortem.yaml +34 -0
- package/routing/god-pr-branch.yaml +25 -0
- package/routing/god-prd.yaml +49 -0
- package/routing/god-quick.yaml +28 -0
- package/routing/god-reconcile.yaml +48 -0
- package/routing/god-reconstruct.yaml +36 -0
- package/routing/god-redo.yaml +27 -0
- package/routing/god-refactor.yaml +36 -0
- package/routing/god-repair.yaml +27 -0
- package/routing/god-repo.yaml +35 -0
- package/routing/god-restore.yaml +27 -0
- package/routing/god-resume-work.yaml +27 -0
- package/routing/god-review-changes.yaml +25 -0
- package/routing/god-review.yaml +28 -0
- package/routing/god-roadmap-check.yaml +39 -0
- package/routing/god-roadmap-update.yaml +37 -0
- package/routing/god-roadmap.yaml +42 -0
- package/routing/god-rollback.yaml +27 -0
- package/routing/god-scan.yaml +24 -0
- package/routing/god-set-profile.yaml +24 -0
- package/routing/god-settings.yaml +24 -0
- package/routing/god-skip.yaml +27 -0
- package/routing/god-smite.yaml +29 -0
- package/routing/god-spike.yaml +35 -0
- package/routing/god-sprint.yaml +25 -0
- package/routing/god-stack.yaml +41 -0
- package/routing/god-standards.yaml +24 -0
- package/routing/god-status.yaml +27 -0
- package/routing/god-stories.yaml +24 -0
- package/routing/god-story-build.yaml +25 -0
- package/routing/god-story-close.yaml +25 -0
- package/routing/god-story-verify.yaml +25 -0
- package/routing/god-story.yaml +24 -0
- package/routing/god-suite-init.yaml +24 -0
- package/routing/god-suite-patch.yaml +25 -0
- package/routing/god-suite-release.yaml +25 -0
- package/routing/god-suite-status.yaml +25 -0
- package/routing/god-suite-sync.yaml +25 -0
- package/routing/god-sync.yaml +33 -0
- package/routing/god-tech-debt.yaml +32 -0
- package/routing/god-test-extension.yaml +24 -0
- package/routing/god-test-runtime.yaml +25 -0
- package/routing/god-thread.yaml +24 -0
- package/routing/god-trace.yaml +24 -0
- package/routing/god-undo.yaml +27 -0
- package/routing/god-update-deps.yaml +39 -0
- package/routing/god-upgrade.yaml +33 -0
- package/routing/god-version.yaml +24 -0
- package/routing/god-workstream.yaml +24 -0
- package/routing/god.yaml +24 -0
- package/routing/recipes/add-feature-defer-current-milestone.yaml +21 -0
- package/routing/recipes/add-feature-future-conditional.yaml +21 -0
- package/routing/recipes/add-feature-mid-arc-pause.yaml +33 -0
- package/routing/recipes/add-feature-next-milestone.yaml +23 -0
- package/routing/recipes/add-feature-parallel.yaml +29 -0
- package/routing/recipes/add-feature-prd-update.yaml +21 -0
- package/routing/recipes/add-feature-small.yaml +24 -0
- package/routing/recipes/add-feature-tiny.yaml +24 -0
- package/routing/recipes/bluefield-org-aware.yaml +27 -0
- package/routing/recipes/broken-install.yaml +22 -0
- package/routing/recipes/brownfield-onboarding.yaml +32 -0
- package/routing/recipes/bug-no-urgency.yaml +21 -0
- package/routing/recipes/capture-idea.yaml +22 -0
- package/routing/recipes/capture-todo.yaml +21 -0
- package/routing/recipes/clean-pr.yaml +21 -0
- package/routing/recipes/code-cleanup.yaml +23 -0
- package/routing/recipes/docs-drift.yaml +21 -0
- package/routing/recipes/existing-codebase-onboarding.yaml +32 -0
- package/routing/recipes/extract-learnings.yaml +22 -0
- package/routing/recipes/greenfield-fast.yaml +25 -0
- package/routing/recipes/greenfield-manual.yaml +32 -0
- package/routing/recipes/greenfield-with-ideation.yaml +29 -0
- package/routing/recipes/incident-postmortem.yaml +24 -0
- package/routing/recipes/major-framework-upgrade.yaml +23 -0
- package/routing/recipes/monthly-deps.yaml +22 -0
- package/routing/recipes/multi-repo-suite.yaml +56 -0
- package/routing/recipes/parallel-engineers.yaml +26 -0
- package/routing/recipes/pause-handoff.yaml +21 -0
- package/routing/recipes/production-broken.yaml +26 -0
- package/routing/recipes/rerun-tier.yaml +21 -0
- package/routing/recipes/returning-after-break.yaml +31 -0
- package/routing/recipes/state-drift.yaml +21 -0
- package/routing/recipes/undo-last.yaml +21 -0
- package/routing/recipes/weekly-health-check.yaml +24 -0
- package/routing/recipes/whats-next.yaml +22 -0
- package/routing/recipes/where-am-i.yaml +21 -0
- package/schema/events.v1.json +63 -0
- package/schema/extension-manifest.v1.json +84 -0
- package/schema/intent.v1.yaml.json +116 -0
- package/schema/recipe.v1.json +120 -0
- package/schema/routing.v1.json +163 -0
- package/schema/state.v1.json +146 -0
- package/schema/workflow.v1.json +96 -0
- package/skills/god-add-backlog.md +40 -0
- package/skills/god-add-tests.md +53 -0
- package/skills/god-add-todo.md +32 -0
- package/skills/god-agent-audit.md +87 -0
- package/skills/god-arch.md +81 -0
- package/skills/god-archaeology.md +48 -0
- package/skills/god-audit.md +65 -0
- package/skills/god-budget.md +103 -0
- package/skills/god-build-agent.md +91 -0
- package/skills/god-build.md +90 -0
- package/skills/god-cache-clear.md +75 -0
- package/skills/god-check-todos.md +42 -0
- package/skills/god-context-scan.md +125 -0
- package/skills/god-context.md +147 -0
- package/skills/god-cost.md +118 -0
- package/skills/god-debug.md +30 -0
- package/skills/god-deploy.md +76 -0
- package/skills/god-design-impact.md +86 -0
- package/skills/god-design.md +275 -0
- package/skills/god-discuss.md +46 -0
- package/skills/god-docs.md +81 -0
- package/skills/god-doctor.md +94 -0
- package/skills/god-explore.md +50 -0
- package/skills/god-export-otel.md +87 -0
- package/skills/god-extension-add.md +79 -0
- package/skills/god-extension-info.md +75 -0
- package/skills/god-extension-list.md +55 -0
- package/skills/god-extension-remove.md +66 -0
- package/skills/god-extract-learnings.md +60 -0
- package/skills/god-fast.md +47 -0
- package/skills/god-feature.md +114 -0
- package/skills/god-graph.md +56 -0
- package/skills/god-harden.md +106 -0
- package/skills/god-help.md +66 -0
- package/skills/god-hotfix.md +139 -0
- package/skills/god-hygiene.md +104 -0
- package/skills/god-init.md +161 -0
- package/skills/god-intel.md +36 -0
- package/skills/god-launch.md +86 -0
- package/skills/god-lifecycle.md +119 -0
- package/skills/god-link.md +90 -0
- package/skills/god-lint.md +128 -0
- package/skills/god-list-assumptions.md +56 -0
- package/skills/god-locate.md +97 -0
- package/skills/god-logs.md +57 -0
- package/skills/god-map-codebase.md +45 -0
- package/skills/god-metrics.md +51 -0
- package/skills/god-mode.md +159 -0
- package/skills/god-next.md +257 -0
- package/skills/god-note.md +39 -0
- package/skills/god-observe.md +76 -0
- package/skills/god-org-context.md +81 -0
- package/skills/god-party.md +87 -0
- package/skills/god-pause-work.md +64 -0
- package/skills/god-plant-seed.md +59 -0
- package/skills/god-postmortem.md +103 -0
- package/skills/god-pr-branch.md +50 -0
- package/skills/god-prd.md +90 -0
- package/skills/god-quick.md +50 -0
- package/skills/god-reconcile.md +90 -0
- package/skills/god-reconstruct.md +72 -0
- package/skills/god-redo.md +73 -0
- package/skills/god-refactor.md +137 -0
- package/skills/god-repair.md +82 -0
- package/skills/god-repo.md +49 -0
- package/skills/god-restore.md +91 -0
- package/skills/god-resume-work.md +42 -0
- package/skills/god-review-changes.md +93 -0
- package/skills/god-review.md +52 -0
- package/skills/god-roadmap-check.md +66 -0
- package/skills/god-roadmap-update.md +64 -0
- package/skills/god-roadmap.md +77 -0
- package/skills/god-rollback.md +88 -0
- package/skills/god-scan.md +106 -0
- package/skills/god-set-profile.md +58 -0
- package/skills/god-settings.md +44 -0
- package/skills/god-skip.md +78 -0
- package/skills/god-smite.md +86 -0
- package/skills/god-spike.md +120 -0
- package/skills/god-sprint.md +77 -0
- package/skills/god-stack.md +74 -0
- package/skills/god-standards.md +62 -0
- package/skills/god-status.md +99 -0
- package/skills/god-stories.md +60 -0
- package/skills/god-story-build.md +76 -0
- package/skills/god-story-close.md +82 -0
- package/skills/god-story-verify.md +71 -0
- package/skills/god-story.md +55 -0
- package/skills/god-suite-init.md +75 -0
- package/skills/god-suite-patch.md +64 -0
- package/skills/god-suite-release.md +58 -0
- package/skills/god-suite-status.md +63 -0
- package/skills/god-suite-sync.md +49 -0
- package/skills/god-sync.md +102 -0
- package/skills/god-tech-debt.md +56 -0
- package/skills/god-test-extension.md +87 -0
- package/skills/god-test-runtime.md +144 -0
- package/skills/god-thread.md +39 -0
- package/skills/god-trace.md +50 -0
- package/skills/god-undo.md +68 -0
- package/skills/god-update-deps.md +134 -0
- package/skills/god-upgrade.md +139 -0
- package/skills/god-version.md +37 -0
- package/skills/god-workstream.md +61 -0
- package/skills/god.md +207 -0
- package/templates/ARCH.md +99 -0
- package/templates/DEPS-AUDIT.md +66 -0
- package/templates/DESIGN.md +71 -0
- package/templates/DOCS-UPDATE-LOG.md +64 -0
- package/templates/HARDEN-FINDINGS.md +69 -0
- package/templates/MIGRATION.md +86 -0
- package/templates/POSTMORTEM.md +88 -0
- package/templates/PRD.md +80 -0
- package/templates/PROGRESS.md +49 -0
- package/templates/ROADMAP.md +47 -0
- package/templates/SPIKE.md +72 -0
- package/templates/STACK-DECISION.md +61 -0
- package/workflows/audit-only.yaml +22 -0
- package/workflows/bluefield-arc.yaml +87 -0
- package/workflows/brownfield-arc.yaml +44 -0
- package/workflows/deps-audit.yaml +56 -0
- package/workflows/docs-arc.yaml +22 -0
- package/workflows/feature-arc.yaml +59 -0
- package/workflows/full-arc.yaml +84 -0
- package/workflows/hotfix-arc.yaml +59 -0
- package/workflows/hygiene.yaml +43 -0
- package/workflows/migration-arc.yaml +73 -0
- package/workflows/postmortem.yaml +31 -0
- package/workflows/refactor-arc.yaml +59 -0
- package/workflows/spike.yaml +23 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Have-Nots Validator
|
|
3
|
+
*
|
|
4
|
+
* Registry of mechanical checks against the 99 named have-nots from
|
|
5
|
+
* references/HAVE-NOTS.md. Each check returns structured findings:
|
|
6
|
+
*
|
|
7
|
+
* { code, severity: 'error'|'warning'|'info', line, column, message, suggestion }
|
|
8
|
+
*
|
|
9
|
+
* Universal checks apply to all artifacts. Per-artifact checks apply only
|
|
10
|
+
* when the linter is given a typed artifact (e.g., 'prd', 'arch').
|
|
11
|
+
*
|
|
12
|
+
* Source of truth for which have-nots are mechanical:
|
|
13
|
+
*
|
|
14
|
+
* U-08 em/en dash mechanical (regex)
|
|
15
|
+
* U-09 emoji mechanical (unicode regex)
|
|
16
|
+
* U-02 unlabeled sentence mechanical (sentence scan + label check)
|
|
17
|
+
* U-10 phantom reference mechanical (link scan + filesystem check)
|
|
18
|
+
* U-11 future-dated timestamp mechanical (date parse vs today)
|
|
19
|
+
* P-04 metric without timeline mechanical (metric + time-word scan)
|
|
20
|
+
* P-05 metric without method mechanical (metric + measurement-word)
|
|
21
|
+
* P-07 no-gos empty mechanical (section presence)
|
|
22
|
+
* P-08 open-q without owner mechanical (table column scan)
|
|
23
|
+
* P-09 open-q without due date mechanical (table column scan)
|
|
24
|
+
* A-04 NFR not mapped mechanical (PRD NFR vs ARCH map)
|
|
25
|
+
*
|
|
26
|
+
* Substitution test (U-01) is a partial mechanical check: flags sentences
|
|
27
|
+
* containing only generic nouns ('users', 'developers', 'scalable',
|
|
28
|
+
* 'robust', 'modern', 'intuitive', 'seamless') without specific
|
|
29
|
+
* quantifiers or proper nouns.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
|
|
35
|
+
const GENERIC_NOUNS = [
|
|
36
|
+
'users', 'developers', 'teams', 'people', 'customers',
|
|
37
|
+
'scalable', 'robust', 'modern', 'intuitive', 'seamless',
|
|
38
|
+
'best-in-class', 'world-class', 'next-generation', 'cutting-edge',
|
|
39
|
+
'simple', 'easy', 'fast', 'powerful'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const LABEL_TAGS = ['DECISION', 'HYPOTHESIS', 'OPEN QUESTION', 'OPEN-QUESTION'];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find all line/column positions for a given regex match in content.
|
|
46
|
+
* Returns array of { line, column } objects (1-indexed).
|
|
47
|
+
*/
|
|
48
|
+
function findPositions(content, regex) {
|
|
49
|
+
const positions = [];
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
let match;
|
|
53
|
+
const localRegex = new RegExp(regex.source, regex.flags.includes('g') ? regex.flags : regex.flags + 'g');
|
|
54
|
+
while ((match = localRegex.exec(lines[i])) !== null) {
|
|
55
|
+
positions.push({ line: i + 1, column: match.index + 1, matched: match[0] });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return positions;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Universal checks (apply to all artifacts)
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
/** U-08: em or en dash present */
|
|
66
|
+
function checkEmEnDash(content) {
|
|
67
|
+
const findings = [];
|
|
68
|
+
const positions = findPositions(content, /[–—]/g);
|
|
69
|
+
for (const p of positions) {
|
|
70
|
+
findings.push({
|
|
71
|
+
code: 'U-08',
|
|
72
|
+
severity: 'error',
|
|
73
|
+
line: p.line,
|
|
74
|
+
column: p.column,
|
|
75
|
+
message: `Em or en dash detected ("${p.matched}"). Use comma, colon, semicolon, parentheses, or hyphen instead.`,
|
|
76
|
+
suggestion: 'Replace with appropriate ASCII punctuation.'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** U-09: emoji present */
|
|
83
|
+
function checkEmoji(content) {
|
|
84
|
+
const findings = [];
|
|
85
|
+
// Common emoji ranges. Excludes basic punctuation and arrows that have
|
|
86
|
+
// legitimate documentation use.
|
|
87
|
+
const emojiRanges = /[\u{1F300}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu;
|
|
88
|
+
const positions = findPositions(content, emojiRanges);
|
|
89
|
+
for (const p of positions) {
|
|
90
|
+
findings.push({
|
|
91
|
+
code: 'U-09',
|
|
92
|
+
severity: 'error',
|
|
93
|
+
line: p.line,
|
|
94
|
+
column: p.column,
|
|
95
|
+
message: `Decorative emoji "${p.matched}" detected.`,
|
|
96
|
+
suggestion: 'Use words or icon library symbols instead.'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return findings;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** U-02: unlabeled paragraph (paragraph-aware) */
|
|
103
|
+
function checkUnlabeled(content, opts = {}) {
|
|
104
|
+
const findings = [];
|
|
105
|
+
const lines = content.split('\n');
|
|
106
|
+
let inCodeBlock = false;
|
|
107
|
+
let paragraph = [];
|
|
108
|
+
let paragraphStartLine = -1;
|
|
109
|
+
|
|
110
|
+
function flushParagraph() {
|
|
111
|
+
if (paragraph.length === 0) return;
|
|
112
|
+
const text = paragraph.join(' ').trim();
|
|
113
|
+
paragraph = [];
|
|
114
|
+
if (!text || text.length < 50) return;
|
|
115
|
+
// Skip intro lines that end with a colon (likely list/section preamble)
|
|
116
|
+
if (text.endsWith(':')) return;
|
|
117
|
+
// If the paragraph contains ANY label tag, accept the whole paragraph
|
|
118
|
+
const hasLabel = LABEL_TAGS.some(tag => text.includes(`[${tag}]`));
|
|
119
|
+
if (hasLabel) return;
|
|
120
|
+
findings.push({
|
|
121
|
+
code: 'U-02',
|
|
122
|
+
severity: 'warning',
|
|
123
|
+
line: paragraphStartLine,
|
|
124
|
+
column: 1,
|
|
125
|
+
message: `Unlabeled paragraph: "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"`,
|
|
126
|
+
suggestion: 'Tag with [DECISION], [HYPOTHESIS], or [OPEN QUESTION].'
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let inBulletContext = false;
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < lines.length; i++) {
|
|
133
|
+
const line = lines[i];
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
|
|
136
|
+
if (trimmed.startsWith('```')) {
|
|
137
|
+
flushParagraph();
|
|
138
|
+
inCodeBlock = !inCodeBlock;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (inCodeBlock) continue;
|
|
142
|
+
|
|
143
|
+
// Bullet starts a bullet context (which absorbs indented continuation)
|
|
144
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('* ') || trimmed.startsWith('- [')) {
|
|
145
|
+
flushParagraph();
|
|
146
|
+
inBulletContext = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Indented continuation under a bullet stays in the bullet
|
|
151
|
+
const isIndented = /^\s+\S/.test(line);
|
|
152
|
+
if (inBulletContext && isIndented) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Lines that break paragraphs
|
|
157
|
+
const isBreak =
|
|
158
|
+
!trimmed ||
|
|
159
|
+
trimmed.startsWith('#') ||
|
|
160
|
+
trimmed.startsWith('>') ||
|
|
161
|
+
trimmed.startsWith('|') ||
|
|
162
|
+
trimmed.startsWith('---') ||
|
|
163
|
+
trimmed.startsWith('===') ||
|
|
164
|
+
trimmed.startsWith('<') ||
|
|
165
|
+
/^\d+\.\s/.test(trimmed);
|
|
166
|
+
|
|
167
|
+
if (isBreak) {
|
|
168
|
+
flushParagraph();
|
|
169
|
+
inBulletContext = false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Non-indented content line ends bullet context
|
|
174
|
+
inBulletContext = false;
|
|
175
|
+
|
|
176
|
+
// Continuation or start of a paragraph
|
|
177
|
+
if (paragraph.length === 0) paragraphStartLine = i + 1;
|
|
178
|
+
paragraph.push(trimmed);
|
|
179
|
+
}
|
|
180
|
+
flushParagraph();
|
|
181
|
+
return findings;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** U-10: phantom reference (link to file that does not exist) */
|
|
185
|
+
function checkPhantomRef(content, opts = {}) {
|
|
186
|
+
const findings = [];
|
|
187
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
188
|
+
const docDir = opts.docDir || projectRoot;
|
|
189
|
+
// Match markdown links: [text](path) where path doesn't start with http
|
|
190
|
+
const linkRegex = /\[[^\]]+\]\(([^)]+)\)/g;
|
|
191
|
+
const lines = content.split('\n');
|
|
192
|
+
for (let i = 0; i < lines.length; i++) {
|
|
193
|
+
let match;
|
|
194
|
+
const localRegex = new RegExp(linkRegex.source, 'g');
|
|
195
|
+
while ((match = localRegex.exec(lines[i])) !== null) {
|
|
196
|
+
const ref = match[1].split('#')[0]; // strip anchor
|
|
197
|
+
if (!ref || ref.startsWith('http') || ref.startsWith('mailto:')) continue;
|
|
198
|
+
const resolved = path.isAbsolute(ref) ? ref : path.resolve(docDir, ref);
|
|
199
|
+
if (!fs.existsSync(resolved)) {
|
|
200
|
+
// Try project root resolution
|
|
201
|
+
const altResolved = path.resolve(projectRoot, ref);
|
|
202
|
+
if (!fs.existsSync(altResolved)) {
|
|
203
|
+
findings.push({
|
|
204
|
+
code: 'U-10',
|
|
205
|
+
severity: 'warning',
|
|
206
|
+
line: i + 1,
|
|
207
|
+
column: match.index + 1,
|
|
208
|
+
message: `Phantom reference: link target "${ref}" does not exist.`,
|
|
209
|
+
suggestion: 'Fix the link or create the referenced file.'
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return findings;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** U-11: future-dated timestamp */
|
|
219
|
+
function checkFutureDate(content, opts = {}) {
|
|
220
|
+
const findings = [];
|
|
221
|
+
const today = opts.today ? new Date(opts.today) : new Date();
|
|
222
|
+
// Match ISO-like dates: 2026-05-10, 2026-12-31
|
|
223
|
+
const dateRegex = /\b(20\d{2})-(\d{2})-(\d{2})\b/g;
|
|
224
|
+
const lines = content.split('\n');
|
|
225
|
+
for (let i = 0; i < lines.length; i++) {
|
|
226
|
+
let match;
|
|
227
|
+
while ((match = dateRegex.exec(lines[i])) !== null) {
|
|
228
|
+
const [full, year, month, day] = match;
|
|
229
|
+
const parsed = new Date(`${year}-${month}-${day}`);
|
|
230
|
+
if (isNaN(parsed.getTime())) continue;
|
|
231
|
+
// Allow due-date columns to be future (those are intentional)
|
|
232
|
+
// Heuristic: if the line contains "Due:" or "Owner:" or is in a table, skip
|
|
233
|
+
if (lines[i].includes('Due:') || lines[i].includes('Owner:') || lines[i].trim().startsWith('|')) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Future timestamps in body content are suspicious
|
|
237
|
+
const oneYearOut = new Date(today);
|
|
238
|
+
oneYearOut.setFullYear(today.getFullYear() + 1);
|
|
239
|
+
if (parsed > oneYearOut) {
|
|
240
|
+
findings.push({
|
|
241
|
+
code: 'U-11',
|
|
242
|
+
severity: 'warning',
|
|
243
|
+
line: i + 1,
|
|
244
|
+
column: match.index + 1,
|
|
245
|
+
message: `Future-dated timestamp "${full}" (more than a year out) in non-deadline context.`,
|
|
246
|
+
suggestion: 'Verify the date is correct or move to an Open Questions due-date.'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return findings;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** U-01 (partial): substitution test - flag generic nouns without quantifiers */
|
|
255
|
+
function checkSubstitution(content) {
|
|
256
|
+
const findings = [];
|
|
257
|
+
const lines = content.split('\n');
|
|
258
|
+
for (let i = 0; i < lines.length; i++) {
|
|
259
|
+
const line = lines[i].trim();
|
|
260
|
+
if (line.startsWith('#') || line.startsWith('|') || line.startsWith('- [') || !line) continue;
|
|
261
|
+
// Look for sentences containing only generic nouns and no numbers/proper nouns
|
|
262
|
+
const sentences = line.split(/(?<=[.?!])\s+/);
|
|
263
|
+
for (const s of sentences) {
|
|
264
|
+
if (s.length < 30) continue;
|
|
265
|
+
const lower = s.toLowerCase();
|
|
266
|
+
const hasGeneric = GENERIC_NOUNS.some(g => lower.includes(` ${g} `) || lower.includes(` ${g}.`) || lower.includes(`${g} `));
|
|
267
|
+
const hasNumber = /\d/.test(s);
|
|
268
|
+
const hasProperNoun = /[A-Z][a-z]+ [A-Z]/.test(s); // Two consecutive capitalized words
|
|
269
|
+
if (hasGeneric && !hasNumber && !hasProperNoun) {
|
|
270
|
+
findings.push({
|
|
271
|
+
code: 'U-01',
|
|
272
|
+
severity: 'warning',
|
|
273
|
+
line: i + 1,
|
|
274
|
+
column: 1,
|
|
275
|
+
message: `Possibly generic claim (substitution test risk): "${s.slice(0, 80)}${s.length > 80 ? '...' : ''}"`,
|
|
276
|
+
suggestion: 'Add specific numbers, named users, or proper nouns. Could a competitor say this verbatim?'
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return findings;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// Per-artifact checks
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Collect bullet items from a section body. Multi-line bullets are joined
|
|
290
|
+
* into a single string. Returns [{ text, startLine }].
|
|
291
|
+
*/
|
|
292
|
+
function collectBullets(body, sectionStartLine) {
|
|
293
|
+
const lines = body.split('\n');
|
|
294
|
+
const bullets = [];
|
|
295
|
+
let current = null;
|
|
296
|
+
for (let i = 0; i < lines.length; i++) {
|
|
297
|
+
const line = lines[i];
|
|
298
|
+
const trimmed = line.trim();
|
|
299
|
+
if (trimmed.startsWith('-') || trimmed.startsWith('*')) {
|
|
300
|
+
if (current) bullets.push(current);
|
|
301
|
+
current = { text: trimmed.replace(/^[-*]\s*/, ''), startLine: sectionStartLine + i };
|
|
302
|
+
} else if (current && trimmed && !/^#/.test(trimmed) && !trimmed.startsWith('|')) {
|
|
303
|
+
// Continuation line (indented or just wrapping)
|
|
304
|
+
current.text += ' ' + trimmed;
|
|
305
|
+
} else if (!trimmed) {
|
|
306
|
+
// Blank line ends the current bullet
|
|
307
|
+
if (current) {
|
|
308
|
+
bullets.push(current);
|
|
309
|
+
current = null;
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Heading or non-list content ends the current bullet
|
|
313
|
+
if (current) {
|
|
314
|
+
bullets.push(current);
|
|
315
|
+
current = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (current) bullets.push(current);
|
|
320
|
+
return bullets;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** P-04: success metric without timeline */
|
|
324
|
+
function checkPrdMetricTimeline(content) {
|
|
325
|
+
const findings = [];
|
|
326
|
+
const successSection = extractSection(content, /^##\s*Success Metrics/im);
|
|
327
|
+
if (!successSection) return findings;
|
|
328
|
+
|
|
329
|
+
const timeWords = /(within|by|in|over)\s+\d+\s*(day|days|week|weeks|month|months|year|years)|by\s+(week|day|month|year|Q[1-4])\s*\d+|by\s+\d{4}-\d{2}-\d{2}|by\s+(Q[1-4])/i;
|
|
330
|
+
const bullets = collectBullets(successSection.body, successSection.startLine);
|
|
331
|
+
for (const b of bullets) {
|
|
332
|
+
if (!timeWords.test(b.text)) {
|
|
333
|
+
findings.push({
|
|
334
|
+
code: 'P-04',
|
|
335
|
+
severity: 'error',
|
|
336
|
+
line: b.startLine,
|
|
337
|
+
column: 1,
|
|
338
|
+
message: 'Success metric without timeline (no "within N days/weeks/months" or "by YYYY-MM-DD").',
|
|
339
|
+
suggestion: 'Add a time bound to make the metric measurable.'
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return findings;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** P-05: success metric without measurement method */
|
|
347
|
+
function checkPrdMetricMethod(content) {
|
|
348
|
+
const findings = [];
|
|
349
|
+
const successSection = extractSection(content, /^##\s*Success Metrics/im);
|
|
350
|
+
if (!successSection) return findings;
|
|
351
|
+
|
|
352
|
+
const methodWords = /(measured|tracked|monitored|via|using)\s+/i;
|
|
353
|
+
const bullets = collectBullets(successSection.body, successSection.startLine);
|
|
354
|
+
for (const b of bullets) {
|
|
355
|
+
if (!methodWords.test(b.text)) {
|
|
356
|
+
findings.push({
|
|
357
|
+
code: 'P-05',
|
|
358
|
+
severity: 'warning',
|
|
359
|
+
line: b.startLine,
|
|
360
|
+
column: 1,
|
|
361
|
+
message: 'Success metric without measurement method.',
|
|
362
|
+
suggestion: 'Specify how it will be measured (e.g., "measured via analytics").'
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return findings;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** P-07: No-Gos section empty */
|
|
370
|
+
function checkPrdNoGos(content) {
|
|
371
|
+
const findings = [];
|
|
372
|
+
const noGoSection = extractSection(content, /^##\s*Scope and No-Gos|^##\s*No.?Gos/im);
|
|
373
|
+
if (!noGoSection) {
|
|
374
|
+
findings.push({
|
|
375
|
+
code: 'P-07',
|
|
376
|
+
severity: 'error',
|
|
377
|
+
line: 1,
|
|
378
|
+
column: 1,
|
|
379
|
+
message: 'Missing "Scope and No-Gos" section.',
|
|
380
|
+
suggestion: 'Add a section listing what is explicitly NOT being built.'
|
|
381
|
+
});
|
|
382
|
+
return findings;
|
|
383
|
+
}
|
|
384
|
+
// Check for "explicitly NOT" subsection content. Locate the heading and
|
|
385
|
+
// scan the lines immediately following for at least one bullet item.
|
|
386
|
+
const body = noGoSection.body;
|
|
387
|
+
const lines = body.split('\n');
|
|
388
|
+
let headingIdx = -1;
|
|
389
|
+
for (let i = 0; i < lines.length; i++) {
|
|
390
|
+
if (/^###\s.*not\s+in\s+scope/i.test(lines[i])) {
|
|
391
|
+
headingIdx = i;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
let hasItem = false;
|
|
396
|
+
if (headingIdx !== -1) {
|
|
397
|
+
for (let i = headingIdx + 1; i < lines.length; i++) {
|
|
398
|
+
if (/^###\s/.test(lines[i])) break; // next subsection
|
|
399
|
+
if (/^-\s+\S/.test(lines[i].trim()) || /^\*\s+\S/.test(lines[i].trim())) {
|
|
400
|
+
hasItem = true;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
// No subsection found, accept any list items in body as no-gos
|
|
406
|
+
hasItem = /^[-*]\s+\S/m.test(body);
|
|
407
|
+
}
|
|
408
|
+
if (!hasItem) {
|
|
409
|
+
findings.push({
|
|
410
|
+
code: 'P-07',
|
|
411
|
+
severity: 'error',
|
|
412
|
+
line: noGoSection.startLine,
|
|
413
|
+
column: 1,
|
|
414
|
+
message: 'No-Gos list is empty or missing.',
|
|
415
|
+
suggestion: 'List at least one thing explicitly not being built.'
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return findings;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** P-08, P-09: open questions missing owner or due date */
|
|
422
|
+
function checkPrdOpenQuestions(content) {
|
|
423
|
+
const findings = [];
|
|
424
|
+
const oqSection = extractSection(content, /^##\s*Open Questions/im);
|
|
425
|
+
if (!oqSection) return findings;
|
|
426
|
+
|
|
427
|
+
const lines = oqSection.body.split('\n');
|
|
428
|
+
// Look for table rows: | Question | Owner | Due Date | Resolution |
|
|
429
|
+
let inTable = false;
|
|
430
|
+
let headerProcessed = false;
|
|
431
|
+
for (let i = 0; i < lines.length; i++) {
|
|
432
|
+
const line = lines[i].trim();
|
|
433
|
+
if (line.startsWith('|') && line.includes('|')) {
|
|
434
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
435
|
+
// Skip header and separator rows
|
|
436
|
+
if (cells.some(c => /^-+$/.test(c))) continue;
|
|
437
|
+
if (!headerProcessed) {
|
|
438
|
+
if (cells.some(c => /question/i.test(c))) {
|
|
439
|
+
headerProcessed = true;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (headerProcessed && cells.length >= 3) {
|
|
444
|
+
const question = cells[0];
|
|
445
|
+
const owner = cells[1] || '';
|
|
446
|
+
const dueDate = cells[2] || '';
|
|
447
|
+
if (question && /^\[?[A-Z]/.test(question) && question.length > 5) {
|
|
448
|
+
if (!owner || /TBD|^\[/.test(owner)) {
|
|
449
|
+
findings.push({
|
|
450
|
+
code: 'P-08',
|
|
451
|
+
severity: 'error',
|
|
452
|
+
line: oqSection.startLine + i,
|
|
453
|
+
column: 1,
|
|
454
|
+
message: `Open question "${question.slice(0, 50)}" has no named owner.`,
|
|
455
|
+
suggestion: 'Assign a named owner.'
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (!dueDate || /TBD|^\[/.test(dueDate)) {
|
|
459
|
+
findings.push({
|
|
460
|
+
code: 'P-09',
|
|
461
|
+
severity: 'error',
|
|
462
|
+
line: oqSection.startLine + i,
|
|
463
|
+
column: 1,
|
|
464
|
+
message: `Open question "${question.slice(0, 50)}" has no due date.`,
|
|
465
|
+
suggestion: 'Set a due date. "TBD" is not a date.'
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return findings;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** A-04: NFR not mapped to architectural choice */
|
|
476
|
+
function checkArchNfrMap(content, opts = {}) {
|
|
477
|
+
const findings = [];
|
|
478
|
+
const prdContent = opts.prdContent;
|
|
479
|
+
if (!prdContent) return findings; // Cannot cross-check without PRD
|
|
480
|
+
|
|
481
|
+
// Find NFR section in PRD
|
|
482
|
+
const prdNfrSection = extractSection(prdContent, /^##\s*Non-Functional Requirements/im);
|
|
483
|
+
if (!prdNfrSection) return findings;
|
|
484
|
+
|
|
485
|
+
const archMapSection = extractSection(content, /^##\s*NFR-to-Architecture Map/im);
|
|
486
|
+
if (!archMapSection) {
|
|
487
|
+
findings.push({
|
|
488
|
+
code: 'A-04',
|
|
489
|
+
severity: 'error',
|
|
490
|
+
line: 1,
|
|
491
|
+
column: 1,
|
|
492
|
+
message: 'ARCH missing "NFR-to-Architecture Map" section.',
|
|
493
|
+
suggestion: 'Add a section mapping each PRD NFR to an architectural choice.'
|
|
494
|
+
});
|
|
495
|
+
return findings;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Extract category names from PRD NFR table
|
|
499
|
+
const prdLines = prdNfrSection.body.split('\n');
|
|
500
|
+
const nfrCategories = [];
|
|
501
|
+
for (const line of prdLines) {
|
|
502
|
+
if (line.startsWith('|')) {
|
|
503
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
504
|
+
if (cells.length >= 2 && !cells.some(c => /^-+$/.test(c)) && !/category/i.test(cells[0])) {
|
|
505
|
+
nfrCategories.push(cells[0]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Check ARCH map mentions each
|
|
511
|
+
const mapBody = archMapSection.body.toLowerCase();
|
|
512
|
+
for (const cat of nfrCategories) {
|
|
513
|
+
if (cat && cat.length > 1 && !mapBody.includes(cat.toLowerCase())) {
|
|
514
|
+
findings.push({
|
|
515
|
+
code: 'A-04',
|
|
516
|
+
severity: 'warning',
|
|
517
|
+
line: archMapSection.startLine,
|
|
518
|
+
column: 1,
|
|
519
|
+
message: `PRD NFR "${cat}" not mapped in ARCH NFR-to-Architecture Map.`,
|
|
520
|
+
suggestion: `Add a row for "${cat}" with the architectural choice that delivers it.`
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return findings;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ============================================================================
|
|
528
|
+
// Helpers
|
|
529
|
+
// ============================================================================
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Extract a markdown section starting at a heading regex.
|
|
533
|
+
* Returns { body, startLine, endLine } or null.
|
|
534
|
+
*/
|
|
535
|
+
function extractSection(content, headingRegex) {
|
|
536
|
+
const lines = content.split('\n');
|
|
537
|
+
let startIdx = -1;
|
|
538
|
+
for (let i = 0; i < lines.length; i++) {
|
|
539
|
+
if (headingRegex.test(lines[i])) {
|
|
540
|
+
startIdx = i;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (startIdx === -1) return null;
|
|
545
|
+
let endIdx = lines.length;
|
|
546
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
547
|
+
if (/^##\s/.test(lines[i])) {
|
|
548
|
+
endIdx = i;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
body: lines.slice(startIdx + 1, endIdx).join('\n'),
|
|
554
|
+
startLine: startIdx + 1,
|
|
555
|
+
endLine: endIdx
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ============================================================================
|
|
560
|
+
// Public API
|
|
561
|
+
// ============================================================================
|
|
562
|
+
|
|
563
|
+
const UNIVERSAL_CHECKS = [
|
|
564
|
+
{ code: 'U-08', fn: checkEmEnDash },
|
|
565
|
+
{ code: 'U-09', fn: checkEmoji },
|
|
566
|
+
{ code: 'U-02', fn: checkUnlabeled },
|
|
567
|
+
{ code: 'U-10', fn: checkPhantomRef },
|
|
568
|
+
{ code: 'U-11', fn: checkFutureDate },
|
|
569
|
+
{ code: 'U-01', fn: checkSubstitution }
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
const ARTIFACT_CHECKS = {
|
|
573
|
+
prd: [
|
|
574
|
+
{ code: 'P-04', fn: checkPrdMetricTimeline },
|
|
575
|
+
{ code: 'P-05', fn: checkPrdMetricMethod },
|
|
576
|
+
{ code: 'P-07', fn: checkPrdNoGos },
|
|
577
|
+
{ code: 'P-08', fn: checkPrdOpenQuestions },
|
|
578
|
+
{ code: 'P-09', fn: checkPrdOpenQuestions }
|
|
579
|
+
],
|
|
580
|
+
arch: [
|
|
581
|
+
{ code: 'A-04', fn: checkArchNfrMap }
|
|
582
|
+
]
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Run all checks for a given artifact.
|
|
587
|
+
* Returns a deduplicated, line-sorted list of findings.
|
|
588
|
+
*/
|
|
589
|
+
function runChecks(content, artifactType, opts = {}) {
|
|
590
|
+
const findings = [];
|
|
591
|
+
for (const c of UNIVERSAL_CHECKS) {
|
|
592
|
+
findings.push(...c.fn(content, opts));
|
|
593
|
+
}
|
|
594
|
+
if (artifactType && ARTIFACT_CHECKS[artifactType]) {
|
|
595
|
+
const seen = new Set();
|
|
596
|
+
for (const c of ARTIFACT_CHECKS[artifactType]) {
|
|
597
|
+
// Avoid running the same fn twice (P-08 and P-09 share fn)
|
|
598
|
+
if (seen.has(c.fn)) continue;
|
|
599
|
+
seen.add(c.fn);
|
|
600
|
+
findings.push(...c.fn(content, opts));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Sort by line number
|
|
604
|
+
findings.sort((a, b) => a.line - b.line);
|
|
605
|
+
// Dedupe identical findings
|
|
606
|
+
const dedup = [];
|
|
607
|
+
const seenKeys = new Set();
|
|
608
|
+
for (const f of findings) {
|
|
609
|
+
const key = `${f.code}:${f.line}:${f.message}`;
|
|
610
|
+
if (seenKeys.has(key)) continue;
|
|
611
|
+
seenKeys.add(key);
|
|
612
|
+
dedup.push(f);
|
|
613
|
+
}
|
|
614
|
+
return dedup;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Summary of findings: counts by severity and code.
|
|
619
|
+
*/
|
|
620
|
+
function summarize(findings) {
|
|
621
|
+
const summary = { errors: 0, warnings: 0, infos: 0, byCode: {} };
|
|
622
|
+
for (const f of findings) {
|
|
623
|
+
summary[f.severity + 's']++;
|
|
624
|
+
summary.byCode[f.code] = (summary.byCode[f.code] || 0) + 1;
|
|
625
|
+
}
|
|
626
|
+
return summary;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
module.exports = {
|
|
630
|
+
runChecks,
|
|
631
|
+
summarize,
|
|
632
|
+
UNIVERSAL_CHECKS,
|
|
633
|
+
ARTIFACT_CHECKS,
|
|
634
|
+
// Exposed for testing
|
|
635
|
+
checkEmEnDash,
|
|
636
|
+
checkEmoji,
|
|
637
|
+
checkUnlabeled,
|
|
638
|
+
checkPhantomRef,
|
|
639
|
+
checkFutureDate,
|
|
640
|
+
checkSubstitution,
|
|
641
|
+
checkPrdMetricTimeline,
|
|
642
|
+
checkPrdMetricMethod,
|
|
643
|
+
checkPrdNoGos,
|
|
644
|
+
checkPrdOpenQuestions,
|
|
645
|
+
checkArchNfrMap,
|
|
646
|
+
extractSection
|
|
647
|
+
};
|