forge-workflow 0.0.10 → 0.1.0-beta.2
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/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +150 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +461 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/forge/TOOLCHAIN.md +670 -0
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +741 -77
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +838 -972
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +307 -100
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +241 -20
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +296 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard.sh +22 -3
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +49 -84
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +50 -83
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -281
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
package/lib/docs-command.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
+
const { parse } = require('@babel/parser');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Allowlist mapping topic names to filenames in docs/.
|
|
@@ -13,6 +14,10 @@ const TOPICS = {
|
|
|
13
14
|
roadmap: 'ROADMAP.md',
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
const TOPIC_DIRS = {
|
|
18
|
+
setup: ['guides', 'reference', ''],
|
|
19
|
+
};
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
22
|
* List all available topic names.
|
|
18
23
|
* @returns {string[]}
|
|
@@ -38,14 +43,1164 @@ function getTopicContent(topic, packageDir) {
|
|
|
38
43
|
return { error: `Unknown topic: "${topic}". Available topics: ${availableList}` };
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
const
|
|
46
|
+
const searchDirs = TOPIC_DIRS[topic] || ['reference', 'guides', ''];
|
|
47
|
+
|
|
48
|
+
for (const dir of searchDirs) {
|
|
49
|
+
const filePath = path.join(packageDir, 'docs', dir, filename);
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
52
|
+
return { content };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code === 'ENOENT') {
|
|
55
|
+
// Try the next allowed documentation directory.
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
return { error: `Failed to read documentation file "${filePath}": ${error.message}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const searchedPaths = searchDirs
|
|
63
|
+
.map((dir) => path.join(packageDir, 'docs', dir, filename))
|
|
64
|
+
.join(', ');
|
|
65
|
+
return { error: `Documentation file "${filename}" not found at ${searchedPaths}` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function walkFiles(rootDir, predicate, results = []) {
|
|
69
|
+
if (!fs.existsSync(rootDir)) {
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const stack = [rootDir];
|
|
74
|
+
while (stack.length > 0) {
|
|
75
|
+
const dir = stack.pop();
|
|
76
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
77
|
+
const fullPath = path.join(dir, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
if (['node_modules', '.git', 'coverage', 'test-results', 'dist', 'build', '.next', 'out'].includes(entry.name)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
stack.push(fullPath);
|
|
83
|
+
} else if (predicate(fullPath)) {
|
|
84
|
+
results.push(fullPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getMarkdownFiles(packageDir) {
|
|
93
|
+
const rootDocs = ['README.md', 'CHANGELOG.md', 'AGENTS.md', 'CLAUDE.md']
|
|
94
|
+
.map((file) => path.join(packageDir, file))
|
|
95
|
+
.filter((file) => fs.existsSync(file));
|
|
96
|
+
const docsFiles = walkFiles(path.join(packageDir, 'docs'), (file) => file.endsWith('.md'));
|
|
97
|
+
const packageDocsFiles = walkFiles(path.join(packageDir, 'packages'), (file) => file.endsWith('.md'));
|
|
98
|
+
return [...rootDocs, ...docsFiles, ...packageDocsFiles].sort((a, b) => a.localeCompare(b));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isExternalLink(target) {
|
|
102
|
+
const trimmed = target.trim();
|
|
103
|
+
return trimmed.startsWith('//') || (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !/^[a-z]:[\\/]/i.test(trimmed));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stripLinkDecorators(target) {
|
|
107
|
+
return target
|
|
108
|
+
.trim()
|
|
109
|
+
.replaceAll(/^<|>$/g, '')
|
|
110
|
+
.split(/[?#]/)[0];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function slugHeading(heading) {
|
|
114
|
+
return stripMarkdownReferenceLabels(stripMarkdownLinkDestinations(heading))
|
|
115
|
+
.trim()
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.replaceAll(/\s+/g, '-')
|
|
118
|
+
.replaceAll(/[`*_~[\](){}:;'"!?,./\\|+=<>@#$%^&]/g, '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stripMarkdownLinkDestinations(text) {
|
|
122
|
+
let result = '';
|
|
123
|
+
let cursor = 0;
|
|
124
|
+
while (cursor < text.length) {
|
|
125
|
+
const marker = text.indexOf('](', cursor);
|
|
126
|
+
if (marker === -1) {
|
|
127
|
+
result += text.slice(cursor);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const labelStart = text.lastIndexOf('[', marker);
|
|
132
|
+
if (labelStart < cursor) {
|
|
133
|
+
result += text.slice(cursor, marker + 2);
|
|
134
|
+
cursor = marker + 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const destinationEnd = findMarkdownDestinationEnd(text, marker + 2);
|
|
139
|
+
if (destinationEnd === -1) {
|
|
140
|
+
result += text.slice(cursor);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const replacementStart = labelStart > 0 && text[labelStart - 1] === '!' ? labelStart - 1 : labelStart;
|
|
145
|
+
result += text.slice(cursor, replacementStart);
|
|
146
|
+
result += text.slice(labelStart + 1, marker);
|
|
147
|
+
cursor = destinationEnd + 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stripMarkdownReferenceLabels(text) {
|
|
154
|
+
let result = '';
|
|
155
|
+
let cursor = 0;
|
|
156
|
+
while (cursor < text.length) {
|
|
157
|
+
const marker = text.indexOf('][', cursor);
|
|
158
|
+
if (marker === -1) {
|
|
159
|
+
result += text.slice(cursor);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const labelStart = text.lastIndexOf('[', marker);
|
|
164
|
+
const referenceEnd = text.indexOf(']', marker + 2);
|
|
165
|
+
if (labelStart < cursor || referenceEnd === -1) {
|
|
166
|
+
result += text.slice(cursor, marker + 2);
|
|
167
|
+
cursor = marker + 2;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const replacementStart = labelStart > 0 && text[labelStart - 1] === '!' ? labelStart - 1 : labelStart;
|
|
172
|
+
result += text.slice(cursor, replacementStart);
|
|
173
|
+
result += text.slice(labelStart + 1, marker);
|
|
174
|
+
cursor = referenceEnd + 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function collectAnchors(content) {
|
|
181
|
+
const anchors = new Set();
|
|
182
|
+
const seen = new Map();
|
|
183
|
+
let activeFence = null;
|
|
184
|
+
let previousTextLine = null;
|
|
185
|
+
for (const line of content.split(/\r?\n/)) {
|
|
186
|
+
const fence = getMarkdownFence(line);
|
|
187
|
+
if (fence) {
|
|
188
|
+
activeFence = updateActiveMarkdownFence(activeFence, fence);
|
|
189
|
+
previousTextLine = null;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (activeFence) {
|
|
194
|
+
previousTextLine = null;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const heading = getMarkdownHeadingText(line);
|
|
199
|
+
if (heading) {
|
|
200
|
+
addAnchor(anchors, seen, heading);
|
|
201
|
+
previousTextLine = null;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (previousTextLine && isSetextHeadingUnderline(line)) {
|
|
206
|
+
addAnchor(anchors, seen, previousTextLine);
|
|
207
|
+
previousTextLine = null;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
previousTextLine = line.trim() ? line.trim() : null;
|
|
212
|
+
}
|
|
213
|
+
return anchors;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function addAnchor(anchors, seen, heading) {
|
|
217
|
+
const slug = slugHeading(heading);
|
|
218
|
+
const count = seen.get(slug) || 0;
|
|
219
|
+
anchors.add(count === 0 ? slug : `${slug}-${count}`);
|
|
220
|
+
seen.set(slug, count + 1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isSetextHeadingUnderline(line) {
|
|
224
|
+
const trimmed = line.trim();
|
|
225
|
+
if (!trimmed) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const marker = trimmed[0];
|
|
230
|
+
if (marker !== '=' && marker !== '-') {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const char of trimmed) {
|
|
235
|
+
if (char !== marker) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getMarkdownHeadingText(line) {
|
|
243
|
+
const trimmed = line.trimStart();
|
|
244
|
+
let level = 0;
|
|
245
|
+
while (trimmed[level] === '#' && level < 6) {
|
|
246
|
+
level++;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (level === 0 || (trimmed[level] !== ' ' && trimmed[level] !== '\t')) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return trimClosingHeadingHashes(trimmed.slice(level + 1).trim());
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function trimClosingHeadingHashes(text) {
|
|
257
|
+
let end = text.length;
|
|
258
|
+
while (end > 0 && text[end - 1] === '#') {
|
|
259
|
+
end--;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return text.slice(0, end).trimEnd();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function findMarkdownLinks(content) {
|
|
266
|
+
const links = [];
|
|
267
|
+
const referenceDefinitions = _collectReferenceDefinitions(content);
|
|
268
|
+
let offset = 0;
|
|
269
|
+
let activeFence = null;
|
|
270
|
+
let allowIndentedListContinuation = false;
|
|
271
|
+
const lines = content.split('\n');
|
|
272
|
+
for (let index = 0; index < lines.length; index++) {
|
|
273
|
+
const line = lines[index];
|
|
274
|
+
const fence = getMarkdownFence(line);
|
|
275
|
+
if (fence) {
|
|
276
|
+
activeFence = updateActiveMarkdownFence(activeFence, fence);
|
|
277
|
+
offset += line.length + 1;
|
|
278
|
+
allowIndentedListContinuation = false;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const isListContinuation = allowIndentedListContinuation && isIndentedMarkdownCodeLine(line);
|
|
283
|
+
if (!activeFence && !isIndentedMarkdownCodeLine(line, { isListContinuation })) {
|
|
284
|
+
links.push(...findMarkdownLinksInLine(line, offset, referenceDefinitions, lines[index + 1]));
|
|
285
|
+
}
|
|
286
|
+
allowIndentedListContinuation = shouldAllowIndentedListContinuation(line, isListContinuation);
|
|
287
|
+
offset += line.length + 1;
|
|
288
|
+
}
|
|
289
|
+
return links;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function _collectReferenceDefinitions(content) {
|
|
293
|
+
const definitions = new Map();
|
|
294
|
+
let activeFence = null;
|
|
295
|
+
let allowIndentedListContinuation = false;
|
|
296
|
+
const lines = content.split('\n');
|
|
297
|
+
for (let index = 0; index < lines.length; index++) {
|
|
298
|
+
const line = lines[index];
|
|
299
|
+
const fence = getMarkdownFence(line);
|
|
300
|
+
if (fence) {
|
|
301
|
+
activeFence = updateActiveMarkdownFence(activeFence, fence);
|
|
302
|
+
allowIndentedListContinuation = false;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const isListContinuation = allowIndentedListContinuation && isIndentedMarkdownCodeLine(line);
|
|
306
|
+
if (activeFence || isIndentedMarkdownCodeLine(line, { isListContinuation })) {
|
|
307
|
+
allowIndentedListContinuation = shouldAllowIndentedListContinuation(line, isListContinuation);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const definition = _getReferenceLinkDefinition(line, lines[index + 1]);
|
|
312
|
+
const label = definition ? _normalizeReferenceLabel(definition.label) : null;
|
|
313
|
+
if (definition && !definitions.has(label)) {
|
|
314
|
+
definitions.set(label, definition.target);
|
|
315
|
+
}
|
|
316
|
+
allowIndentedListContinuation = shouldAllowIndentedListContinuation(line, isListContinuation);
|
|
317
|
+
}
|
|
318
|
+
return definitions;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function findMarkdownLinksInLine(line, offset, referenceDefinitions = new Map(), nextLine = '') {
|
|
322
|
+
const links = [];
|
|
323
|
+
const referenceDefinition = _getReferenceLinkDefinition(line, nextLine);
|
|
324
|
+
if (referenceDefinition) {
|
|
325
|
+
const label = _normalizeReferenceLabel(referenceDefinition.label);
|
|
326
|
+
if (referenceDefinitions.get(label) === referenceDefinition.target) {
|
|
327
|
+
links.push({ target: referenceDefinition.target, index: offset });
|
|
328
|
+
}
|
|
329
|
+
return links;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let cursor = 0;
|
|
333
|
+
while (cursor < line.length) {
|
|
334
|
+
const start = line.indexOf('](', cursor);
|
|
335
|
+
if (start === -1) {
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const labelStart = line.lastIndexOf('[', start);
|
|
340
|
+
if (
|
|
341
|
+
isInsideInlineCode(line, start) ||
|
|
342
|
+
(labelStart !== -1 && isEscapedMarkdownCharacter(line, labelStart))
|
|
343
|
+
) {
|
|
344
|
+
cursor = start + 2;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const end = findMarkdownDestinationEnd(line, start + 2);
|
|
349
|
+
if (end === -1) {
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const target = getMarkdownLinkTarget(line.slice(start + 2, end));
|
|
354
|
+
if (target) {
|
|
355
|
+
links.push({ target, index: offset + start });
|
|
356
|
+
}
|
|
357
|
+
cursor = end + 1;
|
|
358
|
+
}
|
|
359
|
+
links.push(..._findReferenceLinkUsagesInLine(line, offset, referenceDefinitions));
|
|
360
|
+
return links;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function isIndentedMarkdownCodeLine(line, { isListContinuation = false } = {}) {
|
|
364
|
+
if (!line.startsWith(' ') && !line.startsWith('\t')) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
return !(isListContinuation && line.startsWith(' ') && !line.startsWith(' '));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function shouldAllowIndentedListContinuation(line, isListContinuation) {
|
|
371
|
+
if (isMarkdownListItemLine(line)) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
return isListContinuation && line.trim() !== '';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isMarkdownListItemLine(line) {
|
|
378
|
+
return /^\s{0,3}(?:[-+*]|\d{1,9}[.)])\s+/.test(line);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function getMarkdownFence(line) {
|
|
382
|
+
let leadingSpaces = 0;
|
|
383
|
+
while (line[leadingSpaces] === ' ') {
|
|
384
|
+
leadingSpaces += 1;
|
|
385
|
+
}
|
|
386
|
+
if (leadingSpaces > 3) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const trimmed = line.slice(leadingSpaces);
|
|
391
|
+
const marker = trimmed[0];
|
|
392
|
+
if (marker !== '`' && marker !== '~') {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let length = 0;
|
|
397
|
+
while (trimmed[length] === marker) {
|
|
398
|
+
length += 1;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (length < 3) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return { marker, length, hasInfo: trimmed.slice(length).trim() !== '' };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function updateActiveMarkdownFence(activeFence, fence) {
|
|
409
|
+
if (!activeFence) {
|
|
410
|
+
return fence;
|
|
411
|
+
}
|
|
412
|
+
return fence.marker === activeFence.marker && fence.length >= activeFence.length && !fence.hasInfo
|
|
413
|
+
? null
|
|
414
|
+
: activeFence;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function _findReferenceLinkUsagesInLine(line, offset, referenceDefinitions) {
|
|
418
|
+
const links = [];
|
|
419
|
+
let cursor = 0;
|
|
420
|
+
while (cursor < line.length) {
|
|
421
|
+
const usage = findNextReferenceLinkUsage(line, cursor);
|
|
422
|
+
if (!usage) {
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const label = _normalizeReferenceLabel(usage.rawLabel);
|
|
427
|
+
const target = referenceDefinitions.get(label);
|
|
428
|
+
if (target) {
|
|
429
|
+
links.push({ target, index: offset + usage.labelStart });
|
|
430
|
+
} else if (usage.hasFullReference) {
|
|
431
|
+
const missingReference = usage.rawLabel.trim();
|
|
432
|
+
if (missingReference) {
|
|
433
|
+
links.push({ missingReference, index: offset + usage.labelStart });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
cursor = usage.nextCursor;
|
|
437
|
+
}
|
|
438
|
+
return links;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function findNextReferenceLinkUsage(line, cursor) {
|
|
442
|
+
while (cursor < line.length) {
|
|
443
|
+
const labelStart = line.indexOf('[', cursor);
|
|
444
|
+
if (labelStart === -1) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const skippedCursor = getSkippedReferenceLabelCursor(line, labelStart);
|
|
449
|
+
if (skippedCursor !== null) {
|
|
450
|
+
cursor = skippedCursor;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const labelEnd = line.indexOf(']', labelStart + 1);
|
|
455
|
+
if (labelEnd === -1) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const reference = parseReferenceSuffix(line, labelStart, labelEnd);
|
|
460
|
+
if (reference) {
|
|
461
|
+
return reference;
|
|
462
|
+
}
|
|
463
|
+
cursor = labelEnd + 1;
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function getSkippedReferenceLabelCursor(line, labelStart) {
|
|
469
|
+
if (
|
|
470
|
+
(labelStart > 0 && line[labelStart - 1] === '!') ||
|
|
471
|
+
isEscapedMarkdownCharacter(line, labelStart) ||
|
|
472
|
+
isInsideInlineCode(line, labelStart)
|
|
473
|
+
) {
|
|
474
|
+
return labelStart + 1;
|
|
475
|
+
}
|
|
476
|
+
if (line[labelStart + 1] === '^') {
|
|
477
|
+
return labelStart + 1;
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function isEscapedMarkdownCharacter(line, index) {
|
|
483
|
+
let slashCount = 0;
|
|
484
|
+
for (let cursor = index - 1; cursor >= 0 && line[cursor] === '\\'; cursor--) {
|
|
485
|
+
slashCount++;
|
|
486
|
+
}
|
|
487
|
+
return slashCount % 2 === 1;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function parseReferenceSuffix(line, labelStart, labelEnd) {
|
|
491
|
+
if (line[labelEnd + 1] === '(') {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const hasFullReference = line[labelEnd + 1] === '[';
|
|
496
|
+
const referenceEnd = hasFullReference ? line.indexOf(']', labelEnd + 2) : labelEnd;
|
|
497
|
+
if (referenceEnd === -1) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const rawLabel = hasFullReference
|
|
502
|
+
? line.slice(labelEnd + 2, referenceEnd) || line.slice(labelStart + 1, labelEnd)
|
|
503
|
+
: line.slice(labelStart + 1, labelEnd);
|
|
504
|
+
return { hasFullReference, labelStart, nextCursor: referenceEnd + 1, rawLabel };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function findMarkdownDestinationEnd(line, start) {
|
|
508
|
+
let depth = 0;
|
|
509
|
+
for (let index = start; index < line.length; index++) {
|
|
510
|
+
const char = line[index];
|
|
511
|
+
if (char === '\\') {
|
|
512
|
+
index++;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (char === '(') {
|
|
516
|
+
depth++;
|
|
517
|
+
} else if (char === ')') {
|
|
518
|
+
if (depth === 0) {
|
|
519
|
+
return index;
|
|
520
|
+
}
|
|
521
|
+
depth--;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return -1;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isInsideInlineCode(line, index) {
|
|
528
|
+
let cursor = 0;
|
|
529
|
+
while (cursor < index) {
|
|
530
|
+
if (line[cursor] !== '`') {
|
|
531
|
+
cursor++;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (cursor > 0 && line[cursor - 1] === '\\') {
|
|
535
|
+
cursor++;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const start = cursor;
|
|
540
|
+
while (cursor < line.length && line[cursor] === '`') {
|
|
541
|
+
cursor++;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const marker = line.slice(start, cursor);
|
|
545
|
+
const end = line.indexOf(marker, cursor);
|
|
546
|
+
if (end === -1) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
if (index > cursor && index < end) {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
cursor = end + marker.length;
|
|
553
|
+
}
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function getMarkdownLinkTarget(rawDestination) {
|
|
558
|
+
const trimmed = rawDestination.trim();
|
|
559
|
+
if (trimmed.startsWith('<')) {
|
|
560
|
+
const end = trimmed.indexOf('>');
|
|
561
|
+
return end > 1 ? trimmed.slice(0, end + 1) : trimmed;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const titleStart = _findUnescapedWhitespaceIndex(trimmed);
|
|
565
|
+
const target = titleStart === -1 ? trimmed : trimmed.slice(0, titleStart);
|
|
566
|
+
return target.replace(/\\([ \t])/g, '$1');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function _findUnescapedWhitespaceIndex(text) {
|
|
570
|
+
for (let index = 0; index < text.length; index++) {
|
|
571
|
+
if (text[index] === '\\') {
|
|
572
|
+
index++;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (/\s/.test(text[index])) {
|
|
576
|
+
return index;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return -1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function _getReferenceLinkDefinition(line, nextLine = '') {
|
|
583
|
+
const trimmed = line.trimStart();
|
|
584
|
+
if (!trimmed.startsWith('[')) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const labelEnd = trimmed.indexOf(']:');
|
|
589
|
+
if (labelEnd <= 1) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
if (trimmed.slice(1, labelEnd).startsWith('^')) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const rawTarget = trimmed.slice(labelEnd + 2);
|
|
597
|
+
const target = getMarkdownLinkTarget(rawTarget.trim() ? rawTarget : nextLine);
|
|
598
|
+
return target ? { label: trimmed.slice(1, labelEnd), target } : null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function _normalizeReferenceLabel(label) {
|
|
602
|
+
return label.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function lineForIndex(content, index) {
|
|
606
|
+
return content.slice(0, index).split(/\r?\n/).length;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function formatLinkTargetForReport(rawTarget) {
|
|
610
|
+
const normalized = rawTarget.trim().replaceAll('\\', '/');
|
|
611
|
+
if (
|
|
612
|
+
normalized.startsWith('/') ||
|
|
613
|
+
(!path.isAbsolute(normalized) && !/^[A-Za-z]:\//.test(normalized))
|
|
614
|
+
) {
|
|
615
|
+
return rawTarget;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const worktreeIndex = normalized.indexOf('/.worktrees/');
|
|
619
|
+
if (worktreeIndex >= 0) {
|
|
620
|
+
return `<repo>${normalized.slice(worktreeIndex)}`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const repoIndex = normalized.toLowerCase().lastIndexOf('/forge/');
|
|
624
|
+
if (repoIndex >= 0) {
|
|
625
|
+
return `<repo>/${normalized.slice(repoIndex + '/forge/'.length)}`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return `<absolute-path>/${path.posix.basename(normalized)}`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function isWithinRoot(rootDir, candidatePath) {
|
|
632
|
+
const relative = path.relative(
|
|
633
|
+
fs.realpathSync.native(path.resolve(rootDir)),
|
|
634
|
+
resolveRealPathForContainment(candidatePath)
|
|
635
|
+
);
|
|
636
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function resolveRealPathForContainment(candidatePath) {
|
|
640
|
+
const resolved = path.resolve(candidatePath);
|
|
641
|
+
if (fs.existsSync(resolved)) {
|
|
642
|
+
return fs.realpathSync.native(resolved);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const missingSegments = [];
|
|
646
|
+
let current = resolved;
|
|
647
|
+
while (!fs.existsSync(current)) {
|
|
648
|
+
const parent = path.dirname(current);
|
|
649
|
+
if (parent === current) {
|
|
650
|
+
return resolved;
|
|
651
|
+
}
|
|
652
|
+
missingSegments.unshift(path.basename(current));
|
|
653
|
+
current = parent;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return path.join(fs.realpathSync.native(current), ...missingSegments);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function resolveMarkdownLink(packageDir, file, rawTarget) {
|
|
660
|
+
const [filePart, anchorPart] = rawTarget.replaceAll(/^<|>$/g, '').split('#');
|
|
661
|
+
if (!filePart) {
|
|
662
|
+
return { anchorPart, resolved: file };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const targetPath = decodeLocalMarkdownPath(stripLinkDecorators(filePart));
|
|
666
|
+
const resolved = targetPath.startsWith('/') && !targetPath.startsWith('//')
|
|
667
|
+
? path.resolve(packageDir, targetPath.slice(1))
|
|
668
|
+
: path.resolve(path.dirname(file), targetPath);
|
|
669
|
+
return { anchorPart, resolved };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function decodeLocalMarkdownPath(targetPath) {
|
|
673
|
+
try {
|
|
674
|
+
return decodeURIComponent(targetPath);
|
|
675
|
+
} catch {
|
|
676
|
+
return targetPath;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function checkResolvedLink(packageDir, file, relFile, line, rawTarget) {
|
|
681
|
+
const { anchorPart, resolved } = resolveMarkdownLink(packageDir, file, rawTarget);
|
|
682
|
+
const target = formatLinkTargetForReport(rawTarget);
|
|
683
|
+
if (!isWithinRoot(packageDir, resolved)) {
|
|
684
|
+
return { file: relFile, line, target, reason: 'Link escapes project root' };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!fs.existsSync(resolved)) {
|
|
688
|
+
return { file: relFile, line, target, reason: 'Target file does not exist' };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (anchorPart && resolved.endsWith('.md')) {
|
|
692
|
+
const targetContent = fs.readFileSync(resolved, 'utf8');
|
|
693
|
+
const anchors = collectAnchors(targetContent);
|
|
694
|
+
if (!anchors.has(slugHeading(decodeMarkdownAnchor(anchorPart)))) {
|
|
695
|
+
return { file: relFile, line, target, reason: 'Target anchor does not exist' };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function decodeMarkdownAnchor(anchor) {
|
|
703
|
+
try {
|
|
704
|
+
return decodeURIComponent(anchor);
|
|
705
|
+
} catch {
|
|
706
|
+
return anchor;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function checkMarkdownFileLinks(packageDir, file) {
|
|
711
|
+
const brokenLinks = [];
|
|
712
|
+
let linksChecked = 0;
|
|
713
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
714
|
+
const relFile = path.relative(packageDir, file).replaceAll('\\', '/');
|
|
715
|
+
for (const link of findMarkdownLinks(content)) {
|
|
716
|
+
const line = lineForIndex(content, link.index);
|
|
717
|
+
if (link.missingReference) {
|
|
718
|
+
brokenLinks.push({
|
|
719
|
+
file: relFile,
|
|
720
|
+
line,
|
|
721
|
+
target: link.missingReference,
|
|
722
|
+
reason: 'Reference link definition does not exist',
|
|
723
|
+
});
|
|
724
|
+
linksChecked++;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const rawTarget = link.target.trim();
|
|
729
|
+
if (!rawTarget || isExternalLink(rawTarget)) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
linksChecked++;
|
|
734
|
+
const brokenLink = checkResolvedLink(packageDir, file, relFile, line, rawTarget);
|
|
735
|
+
if (brokenLink) {
|
|
736
|
+
brokenLinks.push(brokenLink);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return { linksChecked, brokenLinks };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function checkMarkdownLinks(packageDir) {
|
|
744
|
+
const files = getMarkdownFiles(packageDir);
|
|
745
|
+
const totals = { filesChecked: files.length, linksChecked: 0, brokenLinks: [] };
|
|
746
|
+
for (const file of files) {
|
|
747
|
+
const result = checkMarkdownFileLinks(packageDir, file);
|
|
748
|
+
totals.linksChecked += result.linksChecked;
|
|
749
|
+
totals.brokenLinks.push(...result.brokenLinks);
|
|
750
|
+
}
|
|
751
|
+
return totals;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function getSourceFiles(packageDir) {
|
|
755
|
+
const sourceRoots = ['lib', 'bin', 'scripts', 'src', 'apps', 'packages'];
|
|
756
|
+
const files = new Set();
|
|
757
|
+
for (const file of _getRootSourceEntryFiles(packageDir)) {
|
|
758
|
+
files.add(file);
|
|
759
|
+
}
|
|
760
|
+
for (const dir of sourceRoots) {
|
|
761
|
+
for (const file of walkFiles(path.join(packageDir, dir), isJavaScriptSourceFile)) {
|
|
762
|
+
files.add(file);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return Array.from(files)
|
|
767
|
+
.sort((a, b) => a.localeCompare(b));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function _getRootSourceEntryFiles(packageDir) {
|
|
771
|
+
const candidates = new Set([
|
|
772
|
+
'index.js',
|
|
773
|
+
'index.jsx',
|
|
774
|
+
'index.mjs',
|
|
775
|
+
'index.cjs',
|
|
776
|
+
'index.ts',
|
|
777
|
+
'index.tsx',
|
|
778
|
+
'index.mts',
|
|
779
|
+
'index.cts',
|
|
780
|
+
]);
|
|
781
|
+
for (const entry of _getPackageJsonSourceEntries(packageDir)) {
|
|
782
|
+
candidates.add(entry);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return Array.from(candidates)
|
|
786
|
+
.map((file) => path.resolve(packageDir, file))
|
|
787
|
+
.filter((file) => isWithinRoot(packageDir, file) && fs.existsSync(file) && isJavaScriptSourceFile(file));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function _getPackageJsonSourceEntries(packageDir) {
|
|
791
|
+
const packageJsonPath = path.join(packageDir, 'package.json');
|
|
792
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
793
|
+
return [];
|
|
794
|
+
}
|
|
42
795
|
|
|
43
796
|
try {
|
|
44
|
-
const
|
|
45
|
-
return
|
|
46
|
-
} catch
|
|
47
|
-
return
|
|
797
|
+
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
798
|
+
return _collectPackageEntryValues([manifest.main, manifest.module, manifest.browser, manifest.exports, manifest.bin]);
|
|
799
|
+
} catch {
|
|
800
|
+
return [];
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function _collectPackageEntryValues(values) {
|
|
805
|
+
const entries = [];
|
|
806
|
+
for (const value of Array.isArray(values) ? values : [values]) {
|
|
807
|
+
if (typeof value === 'string') {
|
|
808
|
+
entries.push(value);
|
|
809
|
+
} else if (value && typeof value === 'object') {
|
|
810
|
+
entries.push(..._collectPackageEntryValues(Object.values(value)));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return entries;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function isJavaScriptSourceFile(file) {
|
|
817
|
+
if (!['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'].includes(path.extname(file))) {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
if (hasTestPathSegment(file)) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
return !/(?:^|[.-])(test|spec)\.[cm]?[jt]sx?$/.test(path.basename(file));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function hasTestPathSegment(file) {
|
|
827
|
+
return file
|
|
828
|
+
.replaceAll('\\', '/')
|
|
829
|
+
.split('/')
|
|
830
|
+
.some((segment) => ['test', 'tests', '__tests__', 'spec', 'specs', '__specs__'].includes(segment));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function hasLeadingJsDoc(source, comments, node) {
|
|
834
|
+
const previous = comments
|
|
835
|
+
.filter((comment) => comment.end <= node.start)
|
|
836
|
+
.sort((a, b) => b.end - a.end)[0];
|
|
837
|
+
|
|
838
|
+
if (previous?.type !== 'CommentBlock' || !previous.value.trim().startsWith('*')) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return source.slice(previous.end, node.start).trim() === '';
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function getPublicDocTargets(_source, ast) {
|
|
846
|
+
const targets = [];
|
|
847
|
+
const declarations = collectTopLevelDocDeclarations(ast);
|
|
848
|
+
for (const node of ast.program.body) {
|
|
849
|
+
if (node.type === 'ExportNamedDeclaration') {
|
|
850
|
+
addDocTargetsFromDeclaration(targets, node.declaration, node);
|
|
851
|
+
addDocTargetsFromExportSpecifiers(targets, node, declarations);
|
|
852
|
+
} else if (node.type === 'ExportDefaultDeclaration') {
|
|
853
|
+
addDocTargetsFromDeclaration(targets, node.declaration, node);
|
|
854
|
+
addDocTargetFromDefaultExportIdentifier(targets, node, declarations);
|
|
855
|
+
} else if (node.type === 'ExpressionStatement') {
|
|
856
|
+
addDocTargetFromCommonJsExport(targets, node, declarations);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return targets.filter((target) => !getDocTargetPublicName(target.name).startsWith('_'));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function getDocTargetPublicName(name) {
|
|
863
|
+
const parts = String(name || '').split('.');
|
|
864
|
+
return parts[parts.length - 1] || '';
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function collectTopLevelDocDeclarations(ast) {
|
|
868
|
+
const declarations = new Map();
|
|
869
|
+
for (const node of ast.program.body) {
|
|
870
|
+
const targets = [];
|
|
871
|
+
addDocTargetsFromDeclaration(targets, node);
|
|
872
|
+
for (const target of targets) {
|
|
873
|
+
declarations.set(target.name, target);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return declarations;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function addDocTargetsFromExportSpecifiers(targets, node, declarations) {
|
|
880
|
+
for (const specifier of node.specifiers || []) {
|
|
881
|
+
if (specifier.type !== 'ExportSpecifier' || specifier.local?.type !== 'Identifier') {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const declaration = declarations.get(specifier.local.name);
|
|
886
|
+
if (declaration) {
|
|
887
|
+
targets.push({
|
|
888
|
+
name: getExportedSpecifierName(specifier) || declaration.name,
|
|
889
|
+
node: declaration.node,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
48
892
|
}
|
|
49
893
|
}
|
|
50
894
|
|
|
51
|
-
|
|
895
|
+
function addDocTargetFromDefaultExportIdentifier(targets, node, declarations) {
|
|
896
|
+
if (node.declaration?.type !== 'Identifier') {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const declaration = declarations.get(node.declaration.name);
|
|
901
|
+
if (declaration) {
|
|
902
|
+
targets.push({ name: 'default', node: declaration.node });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function getExportedSpecifierName(specifier) {
|
|
907
|
+
if (specifier.exported?.type === 'Identifier') {
|
|
908
|
+
return specifier.exported.name;
|
|
909
|
+
}
|
|
910
|
+
return specifier.exported?.type === 'StringLiteral' ? specifier.exported.value : null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function addDocTargetsFromDeclaration(targets, node, reportNode = node) {
|
|
914
|
+
if (!node) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (
|
|
919
|
+
reportNode?.type === 'ExportDefaultDeclaration' &&
|
|
920
|
+
[
|
|
921
|
+
'ArrowFunctionExpression',
|
|
922
|
+
'FunctionExpression',
|
|
923
|
+
'ClassExpression',
|
|
924
|
+
'FunctionDeclaration',
|
|
925
|
+
'ClassDeclaration',
|
|
926
|
+
].includes(node.type)
|
|
927
|
+
) {
|
|
928
|
+
targets.push({ name: 'default', node: reportNode });
|
|
929
|
+
} else if (node.type === 'FunctionDeclaration' && node.id?.name) {
|
|
930
|
+
targets.push({ name: node.id.name, node: reportNode });
|
|
931
|
+
} else if (node.type === 'ClassDeclaration' && node.id?.name) {
|
|
932
|
+
targets.push({ name: node.id.name, node: reportNode });
|
|
933
|
+
} else if (node.type === 'VariableDeclaration') {
|
|
934
|
+
for (const declaration of node.declarations) {
|
|
935
|
+
const initType = declaration.init?.type;
|
|
936
|
+
if (
|
|
937
|
+
declaration.id?.type === 'Identifier' &&
|
|
938
|
+
['ArrowFunctionExpression', 'FunctionExpression', 'ClassExpression'].includes(initType)
|
|
939
|
+
) {
|
|
940
|
+
targets.push({ name: declaration.id.name, node: reportNode });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function addDocTargetFromCommonJsExport(targets, node, declarations = new Map()) {
|
|
947
|
+
const expression = node.expression;
|
|
948
|
+
if (expression?.type !== 'AssignmentExpression') {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const initType = expression.right?.type;
|
|
953
|
+
const name = getCommonJsExportName(expression.left);
|
|
954
|
+
if (!name) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (['ArrowFunctionExpression', 'FunctionExpression', 'ClassExpression'].includes(initType)) {
|
|
959
|
+
targets.push({ name, node });
|
|
960
|
+
} else if (name === 'module.exports' && initType === 'ObjectExpression') {
|
|
961
|
+
addDocTargetsFromObjectExport(targets, expression.right, declarations);
|
|
962
|
+
} else if (initType === 'Identifier') {
|
|
963
|
+
const declaration = declarations.get(expression.right.name);
|
|
964
|
+
if (declaration) {
|
|
965
|
+
targets.push({ name, node: declaration.node });
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function addDocTargetsFromObjectExport(targets, node, declarations = new Map()) {
|
|
971
|
+
for (const property of node.properties || []) {
|
|
972
|
+
const hasExportedFunction = property.type === 'ObjectMethod' || (
|
|
973
|
+
property.type === 'ObjectProperty' &&
|
|
974
|
+
['ArrowFunctionExpression', 'FunctionExpression', 'ClassExpression'].includes(property.value?.type)
|
|
975
|
+
);
|
|
976
|
+
if (hasExportedFunction) {
|
|
977
|
+
const name = getObjectPropertyName(property);
|
|
978
|
+
if (name) {
|
|
979
|
+
targets.push({ name: `module.exports.${name}`, node: property });
|
|
980
|
+
}
|
|
981
|
+
} else if (property.type === 'ObjectProperty' && property.value?.type === 'Identifier') {
|
|
982
|
+
const name = getObjectPropertyName(property);
|
|
983
|
+
const declaration = declarations.get(property.value.name);
|
|
984
|
+
if (name && declaration) {
|
|
985
|
+
targets.push({ name: `module.exports.${name}`, node: declaration.node });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function getCommonJsExportName(node) {
|
|
992
|
+
if (node?.type !== 'MemberExpression') {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (node.object?.type === 'Identifier' && node.object.name === 'exports') {
|
|
997
|
+
return getMemberPropertyName(node);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (
|
|
1001
|
+
node.object?.type === 'MemberExpression' &&
|
|
1002
|
+
node.object.object?.type === 'Identifier' &&
|
|
1003
|
+
node.object.object.name === 'module' &&
|
|
1004
|
+
getMemberPropertyName(node.object) === 'exports'
|
|
1005
|
+
) {
|
|
1006
|
+
return getMemberPropertyName(node);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (node.object?.type === 'Identifier' && node.object.name === 'module' && getMemberPropertyName(node) === 'exports') {
|
|
1010
|
+
return 'module.exports';
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function getMemberPropertyName(node) {
|
|
1017
|
+
if (node.computed) {
|
|
1018
|
+
return node.property?.type === 'StringLiteral' ? node.property.value : null;
|
|
1019
|
+
}
|
|
1020
|
+
return node.property?.type === 'Identifier' ? node.property.name : null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function getObjectPropertyName(node) {
|
|
1024
|
+
if (node.computed) {
|
|
1025
|
+
return node.key?.type === 'StringLiteral' ? node.key.value : null;
|
|
1026
|
+
}
|
|
1027
|
+
if (node.key?.type === 'Identifier') {
|
|
1028
|
+
return node.key.name;
|
|
1029
|
+
}
|
|
1030
|
+
return node.key?.type === 'StringLiteral' ? node.key.value : null;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function checkDocstringCoverage(packageDir) {
|
|
1034
|
+
const missing = [];
|
|
1035
|
+
let total = 0;
|
|
1036
|
+
let documented = 0;
|
|
1037
|
+
const files = getSourceFiles(packageDir);
|
|
1038
|
+
|
|
1039
|
+
for (const file of files) {
|
|
1040
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
1041
|
+
let ast;
|
|
1042
|
+
try {
|
|
1043
|
+
ast = parse(source, {
|
|
1044
|
+
sourceType: 'unambiguous',
|
|
1045
|
+
plugins: getParserPlugins(file),
|
|
1046
|
+
attachComment: true,
|
|
1047
|
+
locations: true,
|
|
1048
|
+
ranges: false,
|
|
1049
|
+
});
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
missing.push({
|
|
1052
|
+
file: path.relative(packageDir, file).replaceAll('\\', '/'),
|
|
1053
|
+
name: '<parse-error>',
|
|
1054
|
+
line: 1,
|
|
1055
|
+
reason: error.message,
|
|
1056
|
+
});
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
for (const target of getPublicDocTargets(source, ast)) {
|
|
1061
|
+
total++;
|
|
1062
|
+
if (hasLeadingJsDoc(source, ast.comments || [], target.node)) {
|
|
1063
|
+
documented++;
|
|
1064
|
+
} else {
|
|
1065
|
+
missing.push({
|
|
1066
|
+
file: path.relative(packageDir, file).replaceAll('\\', '/'),
|
|
1067
|
+
name: target.name,
|
|
1068
|
+
line: target.node.loc?.start?.line || 1,
|
|
1069
|
+
reason: 'Missing leading JSDoc block',
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const percent = total === 0 ? 100 : Math.round((documented / total) * 10000) / 100;
|
|
1076
|
+
return { filesChecked: files.length, total, documented, percent, missing };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function getParserPlugins(file) {
|
|
1080
|
+
const ext = path.extname(file).toLowerCase();
|
|
1081
|
+
const plugins = ['topLevelAwait', ['decorators', { decoratorsBeforeExport: true }]];
|
|
1082
|
+
if (['.ts', '.tsx', '.mts', '.cts'].includes(ext)) {
|
|
1083
|
+
plugins.push('typescript');
|
|
1084
|
+
}
|
|
1085
|
+
if (['.js', '.mjs', '.jsx', '.tsx'].includes(ext)) {
|
|
1086
|
+
plugins.push('jsx');
|
|
1087
|
+
}
|
|
1088
|
+
return plugins;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function brokenLinkKey(item) {
|
|
1092
|
+
return `${item.file}\0${item.line}\0${item.target}\0${item.reason}`;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function loadDocsBaseline(packageDir, baselinePath) {
|
|
1096
|
+
if (!baselinePath) {
|
|
1097
|
+
return new Set();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const resolved = path.resolve(packageDir, baselinePath);
|
|
1101
|
+
if (!isWithinRoot(packageDir, resolved)) {
|
|
1102
|
+
throw new Error(`Baseline path escapes project root: "${baselinePath}"`);
|
|
1103
|
+
}
|
|
1104
|
+
if (!fs.existsSync(resolved)) {
|
|
1105
|
+
return new Set();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
try {
|
|
1109
|
+
const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
1110
|
+
return new Set((parsed.brokenLinks || []).map(brokenLinkKey));
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
throw new Error(`Invalid docs baseline JSON at "${resolved}": ${error.message}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function writeDocsBaseline(packageDir, baselinePath, result) {
|
|
1117
|
+
const resolved = path.resolve(packageDir, baselinePath);
|
|
1118
|
+
if (!isWithinRoot(packageDir, resolved)) {
|
|
1119
|
+
throw new Error(`Baseline path escapes project root: "${baselinePath}"`);
|
|
1120
|
+
}
|
|
1121
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
1122
|
+
fs.writeFileSync(
|
|
1123
|
+
resolved,
|
|
1124
|
+
`${JSON.stringify({
|
|
1125
|
+
generatedBy: 'forge docs verify --write-baseline',
|
|
1126
|
+
brokenLinks: result.links.allBrokenLinks || result.links.brokenLinks,
|
|
1127
|
+
}, null, 2)}\n`,
|
|
1128
|
+
'utf8'
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function validateDocs(packageDir, options = {}) {
|
|
1133
|
+
const minDocstringCoverage = Number.isFinite(options.minDocstringCoverage)
|
|
1134
|
+
? options.minDocstringCoverage
|
|
1135
|
+
: 0;
|
|
1136
|
+
const links = checkMarkdownLinks(packageDir);
|
|
1137
|
+
const baseline = loadDocsBaseline(packageDir, options.baselinePath);
|
|
1138
|
+
const allBrokenLinks = links.brokenLinks;
|
|
1139
|
+
const newBrokenLinks = allBrokenLinks.filter((item) => !baseline.has(brokenLinkKey(item)));
|
|
1140
|
+
links.allBrokenLinks = allBrokenLinks;
|
|
1141
|
+
links.knownBrokenLinks = allBrokenLinks.length - newBrokenLinks.length;
|
|
1142
|
+
links.brokenLinks = newBrokenLinks;
|
|
1143
|
+
const docstrings = checkDocstringCoverage(packageDir);
|
|
1144
|
+
const parseErrors = docstrings.missing
|
|
1145
|
+
.filter((item) => item.name === '<parse-error>')
|
|
1146
|
+
.map((item) => ({ type: 'docstring-parse-error', ...item }));
|
|
1147
|
+
const failures = [
|
|
1148
|
+
...links.brokenLinks.map((item) => ({ type: 'broken-link', ...item })),
|
|
1149
|
+
...parseErrors,
|
|
1150
|
+
...(docstrings.percent < minDocstringCoverage
|
|
1151
|
+
? [{
|
|
1152
|
+
type: 'docstring-coverage',
|
|
1153
|
+
reason: `Docstring coverage ${docstrings.percent}% is below ${minDocstringCoverage}%`,
|
|
1154
|
+
}]
|
|
1155
|
+
: []),
|
|
1156
|
+
];
|
|
1157
|
+
|
|
1158
|
+
return {
|
|
1159
|
+
ok: failures.length === 0,
|
|
1160
|
+
links,
|
|
1161
|
+
docstrings,
|
|
1162
|
+
failures,
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function formatDocsValidation(result) {
|
|
1167
|
+
const lines = [
|
|
1168
|
+
'Forge docs validation',
|
|
1169
|
+
'',
|
|
1170
|
+
`Markdown files checked: ${result.links.filesChecked}`,
|
|
1171
|
+
`Markdown links checked: ${result.links.linksChecked}`,
|
|
1172
|
+
`Broken links: ${result.links.brokenLinks.length}`,
|
|
1173
|
+
`Known broken links in baseline: ${result.links.knownBrokenLinks || 0}`,
|
|
1174
|
+
`Source files checked: ${result.docstrings.filesChecked}`,
|
|
1175
|
+
`Docstring coverage: ${result.docstrings.documented}/${result.docstrings.total} (${result.docstrings.percent}%)`,
|
|
1176
|
+
];
|
|
1177
|
+
|
|
1178
|
+
if (result.links.brokenLinks.length > 0) {
|
|
1179
|
+
lines.push('', 'Broken links:');
|
|
1180
|
+
for (const item of result.links.brokenLinks) {
|
|
1181
|
+
lines.push(` - ${item.file}:${item.line} ${item.target} (${item.reason})`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (result.docstrings.missing.length > 0) {
|
|
1186
|
+
lines.push('', 'Missing docstrings:');
|
|
1187
|
+
for (const item of result.docstrings.missing.slice(0, 50)) {
|
|
1188
|
+
lines.push(` - ${item.file}:${item.line} ${item.name} (${item.reason})`);
|
|
1189
|
+
}
|
|
1190
|
+
if (result.docstrings.missing.length > 50) {
|
|
1191
|
+
lines.push(` ... ${result.docstrings.missing.length - 50} more`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
lines.push('', result.ok ? 'Docs validation passed.' : 'Docs validation failed.');
|
|
1196
|
+
return lines.join('\n');
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
module.exports = {
|
|
1200
|
+
listTopics,
|
|
1201
|
+
getTopicContent,
|
|
1202
|
+
validateDocs,
|
|
1203
|
+
formatDocsValidation,
|
|
1204
|
+
writeDocsBaseline,
|
|
1205
|
+
TOPICS,
|
|
1206
|
+
};
|