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
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* doc-gate enforcement gate.
|
|
5
|
+
*
|
|
6
|
+
* Built ON TOP of the validated repo-structure detector
|
|
7
|
+
* (lib/doc-gate/detect.js). The detector resolves a repo's source surface and
|
|
8
|
+
* emits a `verdict` (CODE-RESOLVED | MANUAL-CONFIG | ESCALATE-TO-AGENT). This
|
|
9
|
+
* module turns that surface into a PR gate that enforces the rule:
|
|
10
|
+
*
|
|
11
|
+
* "a code change must be accompanied by a doc update."
|
|
12
|
+
*
|
|
13
|
+
* Design (do not regress):
|
|
14
|
+
* - ABSTAIN-first: if the detector could not confidently resolve the source
|
|
15
|
+
* surface (verdict MANUAL-CONFIG or ESCALATE-TO-AGENT) we NEVER hard-fail.
|
|
16
|
+
* This naturally exempts monorepos (e.g. forge itself, whose npm-workspaces
|
|
17
|
+
* layout escalates), so the gate is safe to make a required check.
|
|
18
|
+
* - Doc-path exclusion is mandatory: a flat-root `source:["."]` repo spans the
|
|
19
|
+
* whole tree (including docs), so doc-only edits must never be counted as a
|
|
20
|
+
* "code change".
|
|
21
|
+
* - Only Added/Modified files matter (the changelog-enforcer A/M pattern);
|
|
22
|
+
* deletions never require a doc update.
|
|
23
|
+
* - Dependency-free: Node built-ins + the tracked-git helper style from
|
|
24
|
+
* detect.js.
|
|
25
|
+
*
|
|
26
|
+
* @module doc-gate/gate
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const cp = require('node:child_process');
|
|
30
|
+
const { detect } = require('./detect');
|
|
31
|
+
|
|
32
|
+
// Strict git: THROWS on any failure. The gate must NEVER treat a git error (a bad
|
|
33
|
+
// ref, a diff failure) as "no changes" — that would fail the gate OPEN and defeat
|
|
34
|
+
// the "safe to require" design. Callers turn a throw into an explicit fail-closed.
|
|
35
|
+
function gitStrict(root, args) {
|
|
36
|
+
const res = cp.spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); // NOSONAR S4036 - hardcoded CLI command, no user input.
|
|
37
|
+
if (res.error) throw new Error(`git ${args.join(' ')}: ${res.error.message}`);
|
|
38
|
+
if (res.status !== 0) throw new Error(`git ${args.join(' ')} exited ${res.status}: ${String(res.stderr || '').trim()}`);
|
|
39
|
+
return res.stdout;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const toPosix = p => String(p).replaceAll('\\', '/').replace(/^\.\//, '');
|
|
43
|
+
const baseName = p => toPosix(p).split('/').pop();
|
|
44
|
+
const extOf = p => {
|
|
45
|
+
const b = baseName(p);
|
|
46
|
+
const i = b.lastIndexOf('.');
|
|
47
|
+
return i > 0 ? b.slice(i).toLowerCase() : '';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// --- declaration glob matching (excludeFromGate / rules) ---------------------
|
|
51
|
+
// Deterministic, ReDoS-safe glob → RegExp. Only linear tokens are emitted
|
|
52
|
+
// (`[^/]*`, `[^/]`, `.*`, `(?:.*/)?`), anchored ^…$ — no nested/overlapping
|
|
53
|
+
// quantifiers, so committed (trusted) globs cannot cause catastrophic backtracking.
|
|
54
|
+
const GLOB_SPECIALS = new Set(['.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\']);
|
|
55
|
+
function globToRegExp(glob) {
|
|
56
|
+
const g = toPosix(glob);
|
|
57
|
+
let re = '^';
|
|
58
|
+
let i = 0;
|
|
59
|
+
while (i < g.length) {
|
|
60
|
+
const c = g[i];
|
|
61
|
+
if (c === '*' && g[i + 1] === '*') {
|
|
62
|
+
if (g[i + 2] === '/') { re += '(?:.*/)?'; i += 3; } // '**/' — zero or more dirs
|
|
63
|
+
else { re += '.*'; i += 2; } // trailing '**' — anything, including '/'
|
|
64
|
+
} else if (c === '*') {
|
|
65
|
+
re += '[^/]*'; i += 1; // single-segment wildcard
|
|
66
|
+
} else if (c === '?') {
|
|
67
|
+
re += '[^/]'; i += 1;
|
|
68
|
+
} else if (GLOB_SPECIALS.has(c)) {
|
|
69
|
+
re += `\\${c}`; i += 1;
|
|
70
|
+
} else {
|
|
71
|
+
re += c; i += 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return new RegExp(`${re}$`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* True when repo-relative path `rel` matches ANY of `globs`. A bare directory
|
|
79
|
+
* glob (no wildcard) also matches everything beneath it (prefix semantics).
|
|
80
|
+
*
|
|
81
|
+
* @param {string} rel - Repo-relative POSIX path.
|
|
82
|
+
* @param {string[]} globs - Declaration globs.
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
function matchesAnyGlob(rel, globs) {
|
|
86
|
+
for (const raw of globs) {
|
|
87
|
+
const glob = toPosix(raw).replace(/\/+$/, '');
|
|
88
|
+
if (!glob) continue;
|
|
89
|
+
if (rel === glob || rel.startsWith(`${glob}/`)) return true; // exact or dir prefix
|
|
90
|
+
if (globToRegExp(glob).test(rel)) return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- doc-path classification (mandatory exclusion) ---------------------------
|
|
96
|
+
const DOC_EXTS = new Set(['.md', '.mdx', '.rst']);
|
|
97
|
+
// Anchored so NEWSLETTER.js / LICENSEMANAGER.go (real code) are NOT treated as
|
|
98
|
+
// docs: the base name must be exactly the word, or word + a [._-] separator.
|
|
99
|
+
const DOC_BASENAME_RE = /^(README|CHANGELOG|HISTORY|NEWS|LICENSE)([._-].*)?$/i;
|
|
100
|
+
const DOC_DIR_PREFIXES = ['docs/', '.changeset/'];
|
|
101
|
+
const DOC_EXACT = new Set(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* True when a repo-relative path is documentation (never counted as "code").
|
|
105
|
+
* Covers markdown/rst family, README/CHANGELOG/HISTORY/NEWS/LICENSE files,
|
|
106
|
+
* `docs/**`, `.changeset/**`, and the agent-instruction docs.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} p - Repo-relative path (any OS separator).
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
function isDocPath(p) {
|
|
112
|
+
const rel = toPosix(p);
|
|
113
|
+
if (DOC_EXTS.has(extOf(rel))) return true;
|
|
114
|
+
if (DOC_DIR_PREFIXES.some(d => rel.startsWith(d))) return true;
|
|
115
|
+
const base = baseName(rel);
|
|
116
|
+
if (DOC_EXACT.has(base)) return true;
|
|
117
|
+
return DOC_BASENAME_RE.test(base);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- config classification (used only for a flat-root `.` source) ------------
|
|
121
|
+
const CONFIG_BASENAMES = new Set([
|
|
122
|
+
'package.json', 'package-lock.json', 'npm-shrinkwrap.json', 'bun.lockb', 'bun.lock',
|
|
123
|
+
'yarn.lock', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'turbo.json', 'lerna.json',
|
|
124
|
+
'tsconfig.json', 'jsconfig.json', 'go.mod', 'go.sum', 'go.work', 'go.work.sum',
|
|
125
|
+
'cargo.toml', 'cargo.lock', 'pyproject.toml', 'setup.py', 'setup.cfg', 'tox.ini',
|
|
126
|
+
'requirements.txt', 'pipfile', 'pipfile.lock', 'poetry.lock', 'uv.lock',
|
|
127
|
+
'gemfile', 'gemfile.lock', 'composer.json', 'composer.lock', 'makefile', 'dockerfile',
|
|
128
|
+
'.gitignore', '.gitattributes', '.editorconfig', '.npmrc', '.nvmrc', '.dockerignore',
|
|
129
|
+
'lefthook.yml',
|
|
130
|
+
]);
|
|
131
|
+
const CONFIG_EXTS = new Set(['.yml', '.yaml', '.toml', '.ini', '.cfg', '.lock']);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* True when a path is project configuration rather than source. Applied only
|
|
135
|
+
* when the source surface is the whole tree (`["."]`), so config edits in a
|
|
136
|
+
* flat-root repo do not require a doc update.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} p - Repo-relative path.
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
function isConfigPath(p) {
|
|
142
|
+
const rel = toPosix(p);
|
|
143
|
+
// Dotfiles / dot-directories (.github, .circleci, .vscode, .config, ...) are config.
|
|
144
|
+
if (rel.split('/')[0].startsWith('.')) return true;
|
|
145
|
+
if (CONFIG_BASENAMES.has(baseName(rel).toLowerCase())) return true;
|
|
146
|
+
return CONFIG_EXTS.has(extOf(rel));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- change parsing ----------------------------------------------------------
|
|
150
|
+
/**
|
|
151
|
+
* Normalise a git status letter (or word) to ADDED / MODIFIED / DELETED.
|
|
152
|
+
* @param {string} s
|
|
153
|
+
* @returns {'ADDED'|'MODIFIED'|'DELETED'}
|
|
154
|
+
*/
|
|
155
|
+
function normalizeStatus(s) {
|
|
156
|
+
const v = String(s || 'M').toUpperCase();
|
|
157
|
+
if (v.startsWith('A')) return 'ADDED';
|
|
158
|
+
if (v.startsWith('D')) return 'DELETED';
|
|
159
|
+
return 'MODIFIED';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Parse `git diff --name-status` output into `{ status, path }` records.
|
|
164
|
+
* Renames/copies (R###/C###) resolve to the NEW path, classified as ADDED.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} out - Raw name-status output.
|
|
167
|
+
* @returns {Array<{status:string, path:string}>}
|
|
168
|
+
*/
|
|
169
|
+
function parseNameStatus(out) {
|
|
170
|
+
const changes = [];
|
|
171
|
+
for (const line of out.split('\n')) {
|
|
172
|
+
const trimmed = line.trim();
|
|
173
|
+
if (!trimmed) continue;
|
|
174
|
+
const parts = trimmed.split('\t');
|
|
175
|
+
const letter = parts[0][0];
|
|
176
|
+
if (letter === 'R' || letter === 'C') {
|
|
177
|
+
changes.push({ status: 'ADDED', path: parts[parts.length - 1] });
|
|
178
|
+
} else if (parts[1]) {
|
|
179
|
+
changes.push({ status: normalizeStatus(letter), path: parts[1] });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return changes;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Normalise caller-supplied `changedFiles` (strings or `{status,path}` objects)
|
|
187
|
+
* into `{ status, path }` records. Bare strings default to MODIFIED.
|
|
188
|
+
*
|
|
189
|
+
* @param {Array<string|{status?:string, path:string}>} changedFiles
|
|
190
|
+
* @returns {Array<{status:string, path:string}>}
|
|
191
|
+
*/
|
|
192
|
+
function normalizeChangedFiles(changedFiles) {
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const entry of changedFiles) {
|
|
195
|
+
if (typeof entry === 'string') {
|
|
196
|
+
out.push({ status: 'MODIFIED', path: entry });
|
|
197
|
+
} else if (entry?.path) {
|
|
198
|
+
out.push({ status: normalizeStatus(entry.status), path: entry.path });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* From the Added/Modified changes, return the ones that count as CODE under the
|
|
206
|
+
* resolved source surface. Docs are always excluded; paths matching a declared
|
|
207
|
+
* `excludeFromGate` glob are excluded; for a flat-root `["."]` surface only
|
|
208
|
+
* top-level, non-config files count.
|
|
209
|
+
*
|
|
210
|
+
* @param {Array<{status:string, path:string}>} changes
|
|
211
|
+
* @param {string[]|null} sourceDirs
|
|
212
|
+
* @param {string[]} [excludeGlobs] - Declared `excludeFromGate` globs.
|
|
213
|
+
* @returns {string[]} repo-relative code paths
|
|
214
|
+
*/
|
|
215
|
+
function codeChangesUnderSource(changes, sourceDirs, excludeGlobs = []) {
|
|
216
|
+
// Strip trailing slashes so a declared `packages/a/` matches `packages/a/index.js`.
|
|
217
|
+
const dirs = Array.isArray(sourceDirs) ? sourceDirs.map(p => toPosix(p).replace(/\/+$/, '')) : [];
|
|
218
|
+
const flatRoot = dirs.includes('.');
|
|
219
|
+
const code = [];
|
|
220
|
+
for (const change of changes) {
|
|
221
|
+
if (change.status === 'DELETED') continue;
|
|
222
|
+
const rel = toPosix(change.path);
|
|
223
|
+
if (isDocPath(rel)) continue;
|
|
224
|
+
if (excludeGlobs.length > 0 && matchesAnyGlob(rel, excludeGlobs)) continue; // declared exclusion
|
|
225
|
+
if (flatRoot) {
|
|
226
|
+
if (!rel.includes('/') && !isConfigPath(rel)) code.push(rel);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (dirs.some(d => rel === d || rel.startsWith(`${d}/`))) code.push(rel);
|
|
230
|
+
}
|
|
231
|
+
return code;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Apply declared `rules`: a changed non-doc file matching `rule.when` REQUIRES
|
|
236
|
+
* the `rule.requires` path to be Added/Modified in the same change set. Returns
|
|
237
|
+
* a precise message for each violated rule (empty when all satisfied).
|
|
238
|
+
*
|
|
239
|
+
* @param {Array<{status:string, path:string}>} changes
|
|
240
|
+
* @param {Array<{when:string, requires:string}>} rules
|
|
241
|
+
* @returns {string[]} violation messages
|
|
242
|
+
*/
|
|
243
|
+
function checkDeclaredRules(changes, rules) {
|
|
244
|
+
if (!Array.isArray(rules) || rules.length === 0) return [];
|
|
245
|
+
const touched = changes.filter(c => c.status !== 'DELETED').map(c => toPosix(c.path));
|
|
246
|
+
const violations = [];
|
|
247
|
+
for (const rule of rules) {
|
|
248
|
+
const triggered = touched.filter(p => !isDocPath(p) && matchesAnyGlob(p, [rule.when]));
|
|
249
|
+
if (triggered.length === 0) continue;
|
|
250
|
+
const requires = toPosix(rule.requires);
|
|
251
|
+
const satisfied = touched.some(p => p === requires || matchesAnyGlob(p, [rule.requires]));
|
|
252
|
+
if (!satisfied) {
|
|
253
|
+
violations.push(`change to ${triggered[0]} requires an update to "${rule.requires}" (rule when="${rule.when}")`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return violations;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolve the changed-file set: caller-supplied `changedFiles` when present,
|
|
261
|
+
* otherwise the tracked `git diff --name-status <base>...<head>` (three-dot, PR
|
|
262
|
+
* semantics).
|
|
263
|
+
*
|
|
264
|
+
* @param {{root:string, base?:string, head?:string, changedFiles?:Array}} opts
|
|
265
|
+
* @returns {Array<{status:string, path:string}>}
|
|
266
|
+
*/
|
|
267
|
+
function resolveChanges({ root, base, head, changedFiles }) {
|
|
268
|
+
if (Array.isArray(changedFiles)) return normalizeChangedFiles(changedFiles);
|
|
269
|
+
if (!base || !head) return [];
|
|
270
|
+
// Validate BOTH refs resolve to a commit before diffing — a bad ref throws
|
|
271
|
+
// (fail-closed) rather than yielding an empty, gate-passing diff.
|
|
272
|
+
gitStrict(root, ['rev-parse', '--verify', '--quiet', `${base}^{commit}`]);
|
|
273
|
+
gitStrict(root, ['rev-parse', '--verify', '--quiet', `${head}^{commit}`]);
|
|
274
|
+
return parseNameStatus(gitStrict(root, ['diff', '--name-status', `${base}...${head}`]));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Evaluate the doc-gate for a pull request.
|
|
279
|
+
*
|
|
280
|
+
* @param {Object} opts
|
|
281
|
+
* @param {string} opts.root - Absolute repo root (a git working tree).
|
|
282
|
+
* @param {string} [opts.base] - Base ref/SHA (required unless `changedFiles`).
|
|
283
|
+
* @param {string} [opts.head] - Head ref/SHA (required unless `changedFiles`).
|
|
284
|
+
* @param {Array<string|{status?:string, path:string}>} [opts.changedFiles] -
|
|
285
|
+
* Pre-computed change set; bypasses `git diff`.
|
|
286
|
+
* @param {boolean} [opts.skip] - Force a pass (e.g. a `no-docs-needed` label).
|
|
287
|
+
* @returns {{ decision:'pass'|'fail'|'abstain', reason:string,
|
|
288
|
+
* offendingCodeFiles:string[], docChangesSeen:string[],
|
|
289
|
+
* sourceSurface:(string[]|null), verdict:string }}
|
|
290
|
+
*/
|
|
291
|
+
function evaluateGate({ root, base, head, changedFiles, skip } = {}) {
|
|
292
|
+
const result = detect(root);
|
|
293
|
+
const sourceSurface = result.source ? result.source.value : null;
|
|
294
|
+
const verdict = result.verdict;
|
|
295
|
+
const summary = { sourceSurface, verdict };
|
|
296
|
+
|
|
297
|
+
// FAIL-CLOSED on an INVALID committed `.docgate.json`: a malformed declaration
|
|
298
|
+
// is a config error that must block a required check, never silently pass —
|
|
299
|
+
// checked BEFORE `skip` so a broken declaration can't be waved through.
|
|
300
|
+
if (Array.isArray(result.declarationErrors) && result.declarationErrors.length > 0) {
|
|
301
|
+
return {
|
|
302
|
+
decision: 'fail',
|
|
303
|
+
reason: `invalid .docgate.json declaration: ${result.declarationErrors.join('; ')}`,
|
|
304
|
+
offendingCodeFiles: [],
|
|
305
|
+
docChangesSeen: [],
|
|
306
|
+
...summary,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (skip) {
|
|
311
|
+
return { decision: 'pass', reason: 'skipped', offendingCodeFiles: [], docChangesSeen: [], ...summary };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (verdict === 'ESCALATE-TO-AGENT' || verdict === 'MANUAL-CONFIG') {
|
|
315
|
+
return {
|
|
316
|
+
decision: 'abstain',
|
|
317
|
+
reason: `detector verdict ${verdict}: source surface not confidently resolved; not enforcing`,
|
|
318
|
+
offendingCodeFiles: [],
|
|
319
|
+
docChangesSeen: [],
|
|
320
|
+
...summary,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// CODE-RESOLVED or DECLARED (a committed declaration is enforced exactly like
|
|
325
|
+
// CODE-RESOLVED): the source surface is a concrete set of dirs (or ".").
|
|
326
|
+
let changes;
|
|
327
|
+
try {
|
|
328
|
+
changes = resolveChanges({ root, base, head, changedFiles });
|
|
329
|
+
} catch (err) {
|
|
330
|
+
// FAIL-CLOSED: a git error (bad refs / diff failure) must not silently pass a
|
|
331
|
+
// required check — surface it as a failure so the PR is investigated.
|
|
332
|
+
return {
|
|
333
|
+
decision: 'fail',
|
|
334
|
+
reason: `could not compute the PR diff (${err.message}); failing closed`,
|
|
335
|
+
offendingCodeFiles: [],
|
|
336
|
+
docChangesSeen: [],
|
|
337
|
+
...summary,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const excludeGlobs = result.declaration?.excludeFromGate ?? [];
|
|
341
|
+
const docChangesSeen = changes
|
|
342
|
+
.filter(c => c.status !== 'DELETED' && isDocPath(c.path))
|
|
343
|
+
.map(c => toPosix(c.path));
|
|
344
|
+
const offending = codeChangesUnderSource(changes, sourceSurface, excludeGlobs);
|
|
345
|
+
|
|
346
|
+
if (offending.length > 0 && docChangesSeen.length === 0) {
|
|
347
|
+
return {
|
|
348
|
+
decision: 'fail',
|
|
349
|
+
reason: `${offending.length} code change(s) under source surface [${(sourceSurface || []).join(', ')}] with no accompanying doc update`,
|
|
350
|
+
offendingCodeFiles: offending,
|
|
351
|
+
docChangesSeen,
|
|
352
|
+
...summary,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Declared `rules` are enforced independently of the doc-companion check: a
|
|
357
|
+
// triggered rule fails even when a doc update accompanied the code change.
|
|
358
|
+
const ruleViolations = checkDeclaredRules(changes, result.declaration?.rules ?? []);
|
|
359
|
+
if (ruleViolations.length > 0) {
|
|
360
|
+
return {
|
|
361
|
+
decision: 'fail',
|
|
362
|
+
reason: ruleViolations.join('; '),
|
|
363
|
+
offendingCodeFiles: offending,
|
|
364
|
+
docChangesSeen,
|
|
365
|
+
...summary,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const reason = offending.length === 0
|
|
370
|
+
? 'no code change under the source surface'
|
|
371
|
+
: 'code change accompanied by a doc update';
|
|
372
|
+
return { decision: 'pass', reason, offendingCodeFiles: [], docChangesSeen, ...summary };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = { evaluateGate, isDocPath, isConfigPath, parseNameStatus };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* doc-gate OKF feature toggle — `.forge/doc-gate.json`.
|
|
5
|
+
*
|
|
6
|
+
* OKF (Google's Open Knowledge Format) support is an OPT-IN, user-toggleable
|
|
7
|
+
* knowledge-base feature. It is DISABLED by default: OKF v0.1 is a DRAFT and
|
|
8
|
+
* explicitly "not an official Google product", so nothing generates a bundle or
|
|
9
|
+
* touches AGENTS.md until a user turns it on.
|
|
10
|
+
*
|
|
11
|
+
* This module is the toggle store. It mirrors the `.forge/*.json` config pattern
|
|
12
|
+
* used by `lib/adapter-cli.js` (see `setAdapterEnabled`) but writes a dedicated
|
|
13
|
+
* `.forge/doc-gate.json` shaped `{ okf: { enabled: boolean } }`.
|
|
14
|
+
*
|
|
15
|
+
* IMPORTANT: this file is NOT the repo-root `.docgate.json` declaration
|
|
16
|
+
* (lib/doc-gate/declaration.js). Those two are deliberately separate — the
|
|
17
|
+
* declaration authoritatively describes repo structure and flips the detector
|
|
18
|
+
* verdict to DECLARED; this toggle ONLY gates OKF bundle generation. Neither
|
|
19
|
+
* reads or writes the other's file.
|
|
20
|
+
*
|
|
21
|
+
* Fail-safe: a missing OR malformed config resolves to DISABLED, never an error.
|
|
22
|
+
*
|
|
23
|
+
* @module doc-gate/okf-config
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
|
|
29
|
+
const CONFIG_DIR = '.forge';
|
|
30
|
+
const CONFIG_FILE = 'doc-gate.json';
|
|
31
|
+
|
|
32
|
+
/** Absolute path to a repo's `.forge/doc-gate.json`. */
|
|
33
|
+
function configPath(root) {
|
|
34
|
+
return path.join(root, CONFIG_DIR, CONFIG_FILE);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** True only for a plain (non-array) object. */
|
|
38
|
+
function isPlainObject(value) {
|
|
39
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize any parsed value into a well-formed `{ okf: { enabled: boolean } }`,
|
|
44
|
+
* preserving unrelated top-level keys. `enabled` is `true` ONLY when it is
|
|
45
|
+
* strictly the boolean `true`, so any junk (missing, string, number, null) is a
|
|
46
|
+
* safe `false`.
|
|
47
|
+
*/
|
|
48
|
+
function normalizeConfig(parsed) {
|
|
49
|
+
const base = isPlainObject(parsed) ? parsed : {};
|
|
50
|
+
const okf = isPlainObject(base.okf) ? base.okf : {};
|
|
51
|
+
return { ...base, okf: { ...okf, enabled: okf.enabled === true } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load + normalize a repo's `.forge/doc-gate.json`.
|
|
56
|
+
*
|
|
57
|
+
* A missing file, an unreadable file, or invalid JSON all resolve to the DISABLED
|
|
58
|
+
* default — this must never throw, so the toggle can be queried anywhere.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} root - Repository root.
|
|
61
|
+
* @returns {{ okf: { enabled: boolean } }}
|
|
62
|
+
*/
|
|
63
|
+
function loadOkfConfig(root) {
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = fs.readFileSync(configPath(root), 'utf8');
|
|
67
|
+
} catch (_err) {
|
|
68
|
+
// Missing / unreadable config => disabled (fail-safe). NOSONAR S2486
|
|
69
|
+
return normalizeConfig(null);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return normalizeConfig(JSON.parse(raw));
|
|
73
|
+
} catch (_err) {
|
|
74
|
+
// Malformed JSON => disabled (fail-safe), never surfaced as an error. NOSONAR S2486
|
|
75
|
+
return normalizeConfig(null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** True when OKF generation is enabled for `root`. */
|
|
80
|
+
function isOkfEnabled(root) {
|
|
81
|
+
return loadOkfConfig(root).okf.enabled === true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Symlink-safe config write: refuse to write THROUGH a symlink (a checked-in
|
|
86
|
+
* symlink could clobber a file outside the repo), matching `doc-gate init`.
|
|
87
|
+
*/
|
|
88
|
+
function writeConfig(root, config) {
|
|
89
|
+
const dir = path.join(root, CONFIG_DIR);
|
|
90
|
+
// Refuse a symlinked CONFIG_DIR (.forge) BEFORE mkdir/write — a checked-in
|
|
91
|
+
// symlink there could redirect the write to a file OUTSIDE the repo.
|
|
92
|
+
let dirStat = null;
|
|
93
|
+
try { dirStat = fs.lstatSync(dir); } catch { /* absent: will be created */ }
|
|
94
|
+
if (dirStat?.isSymbolicLink()) {
|
|
95
|
+
throw new Error(`${CONFIG_DIR} is a symlink; refusing to write through it.`);
|
|
96
|
+
}
|
|
97
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
98
|
+
const file = configPath(root);
|
|
99
|
+
let stat = null;
|
|
100
|
+
try { stat = fs.lstatSync(file); } catch { /* absent: stat stays null */ }
|
|
101
|
+
if (stat?.isSymbolicLink()) {
|
|
102
|
+
throw new Error(`${CONFIG_DIR}/${CONFIG_FILE} is a symlink; refusing to write through it.`);
|
|
103
|
+
}
|
|
104
|
+
fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set the OKF `enabled` flag, preserving any unrelated config keys.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} root - Repository root.
|
|
111
|
+
* @param {boolean} enabled - Desired state.
|
|
112
|
+
* @returns {{ okf: { enabled: boolean } }} The written config.
|
|
113
|
+
*/
|
|
114
|
+
function setOkfEnabled(root, enabled) {
|
|
115
|
+
const current = loadOkfConfig(root);
|
|
116
|
+
const next = { ...current, okf: { ...current.okf, enabled: enabled === true } };
|
|
117
|
+
writeConfig(root, next);
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
loadOkfConfig,
|
|
123
|
+
isOkfEnabled,
|
|
124
|
+
setOkfEnabled,
|
|
125
|
+
configPath,
|
|
126
|
+
CONFIG_DIR,
|
|
127
|
+
CONFIG_FILE,
|
|
128
|
+
};
|