@event4u/agent-config 16.3.0 → 16.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +178 -78
- package/README.md +2 -2
- package/agents/templates/.ai-council.yml.example +26 -8
- package/dist/agent-src/commands/analyze/inbox.md +10 -7
- package/dist/agent-src/commands/bug/investigate.md +1 -1
- package/dist/agent-src/commands/commit/in-chunks.md +40 -31
- package/dist/agent-src/commands/commit.md +72 -33
- package/dist/agent-src/commands/condense.md +2 -8
- package/dist/agent-src/commands/council/debate.md +11 -7
- package/dist/agent-src/commands/council/default.md +10 -9
- package/dist/agent-src/commands/council.md +1 -1
- package/dist/agent-src/commands/design.md +1 -1
- package/dist/agent-src/commands/estimate-ticket.md +1 -1
- package/dist/agent-src/commands/feature/explore.md +3 -3
- package/dist/agent-src/commands/feature/plan.md +1 -1
- package/dist/agent-src/commands/fix/ci.md +17 -3
- package/dist/agent-src/commands/fix/commit-messages.md +48 -23
- package/dist/agent-src/commands/fix/pr-comments.md +3 -6
- package/dist/agent-src/commands/implement-ticket.md +3 -3
- package/dist/agent-src/commands/memory/add.md +4 -16
- package/dist/agent-src/commands/memory/load.md +5 -6
- package/dist/agent-src/commands/memory/promote.md +3 -16
- package/dist/agent-src/commands/mission/upgrade.md +3 -2
- package/dist/agent-src/commands/optimize/agents-dir.md +2 -2
- package/dist/agent-src/commands/optimize/deep.md +4 -2
- package/dist/agent-src/commands/optimize/rtk.md +4 -26
- package/dist/agent-src/commands/pr/create/description-only.md +18 -15
- package/dist/agent-src/commands/pr/create.md +42 -34
- package/dist/agent-src/commands/pr/merge.md +136 -9
- package/dist/agent-src/commands/prepare-for-review.md +13 -11
- package/dist/agent-src/commands/project/analyze.md +7 -35
- package/dist/agent-src/commands/refine-ticket.md +1 -1
- package/dist/agent-src/commands/review/changes.md +14 -19
- package/dist/agent-src/commands/roadmap/next.md +10 -1
- package/dist/agent-src/commands/roadmap/process-full.md +52 -38
- package/dist/agent-src/commands/roadmap/resolve-blockers.md +64 -14
- package/dist/agent-src/commands/worktree/create.md +7 -3
- package/dist/agent-src/commands/worktree/verify.md +2 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +5 -3
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +7 -9
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +8 -6
- package/dist/agent-src/contexts/contracts/frugality-charter.md +8 -5
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +1 -1
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +0 -1
- package/dist/agent-src/contexts/execution/evidence-discipline.md +2 -2
- package/dist/agent-src/contexts/execution/non-interactive-contract.md +5 -8
- package/dist/agent-src/contexts/execution/rdp-gate.md +4 -4
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +9 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +67 -52
- package/dist/agent-src/contexts/execution/subagent-routing.md +8 -8
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -4
- package/dist/agent-src/contexts/execution/terminal-states.md +2 -2
- package/dist/agent-src/contexts/execution/toolchain-resolver.md +4 -10
- package/dist/agent-src/contexts/subagent-configuration.md +1 -1
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +39 -0
- package/dist/agent-src/guidelines/agent-infra/context-hygiene-mechanics.md +1 -1
- package/dist/agent-src/guidelines/agent-infra/cross-source-consistency-mechanics.md +8 -10
- package/dist/agent-src/guidelines/agent-infra/frontier-reasoning-operating-profile.md +3 -2
- package/dist/agent-src/guidelines/agent-infra/layered-settings.md +13 -1
- package/dist/agent-src/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/dist/agent-src/guidelines/agent-infra/self-improvement-pipeline.md +10 -3
- package/dist/agent-src/guidelines/agent-infra/size-and-scope.md +36 -0
- package/dist/agent-src/guidelines/design-antipatterns.md +1 -1
- package/dist/agent-src/guidelines/php/git.md +15 -2
- package/dist/agent-src/rules/active-remediation.md +0 -1
- package/dist/agent-src/rules/analysis-skill-routing.md +0 -1
- package/dist/agent-src/rules/architecture.md +0 -1
- package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -1
- package/dist/agent-src/rules/artifact-engagement-recording.md +0 -1
- package/dist/agent-src/rules/augment-edit-discipline.md +0 -1
- package/dist/agent-src/rules/autonomous-execution.md +0 -1
- package/dist/agent-src/rules/brand-consistency.md +0 -1
- package/dist/agent-src/rules/brand-source-of-truth.md +0 -1
- package/dist/agent-src/rules/broken-access-control.md +0 -1
- package/dist/agent-src/rules/cli-output-handling.md +0 -1
- package/dist/agent-src/rules/code-comment-discipline.md +0 -1
- package/dist/agent-src/rules/code-provenance.md +0 -1
- package/dist/agent-src/rules/command-suggestion-policy.md +0 -1
- package/dist/agent-src/rules/commit-conventions.md +1 -2
- package/dist/agent-src/rules/communication-through-line.md +1 -2
- package/dist/agent-src/rules/content-quoting-floor.md +1 -2
- package/dist/agent-src/rules/context-hygiene.md +5 -5
- package/dist/agent-src/rules/copilot-routing.md +1 -2
- package/dist/agent-src/rules/council-availability.md +4 -4
- package/dist/agent-src/rules/cross-source-consistency.md +4 -6
- package/dist/agent-src/rules/decision-revisit-gate.md +2 -3
- package/dist/agent-src/rules/delegation-policy.md +2 -3
- package/dist/agent-src/rules/design-fidelity.md +0 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +3 -3
- package/dist/agent-src/rules/devcontainer-routing.md +1 -2
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +0 -1
- package/dist/agent-src/rules/docker-commands.md +0 -1
- package/dist/agent-src/rules/domain-adoption-policy.md +0 -1
- package/dist/agent-src/rules/domain-safety-disclaimer.md +0 -1
- package/dist/agent-src/rules/domain-safety-pii.md +0 -1
- package/dist/agent-src/rules/domain-safety-retention.md +0 -1
- package/dist/agent-src/rules/downstream-changes.md +0 -1
- package/dist/agent-src/rules/engineering-safety-floor.md +0 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +0 -1
- package/dist/agent-src/rules/external-reference-deep-dive.md +0 -1
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -2
- package/dist/agent-src/rules/finance-safety-floor.md +0 -1
- package/dist/agent-src/rules/fix-what-you-see.md +6 -32
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +0 -1
- package/dist/agent-src/rules/git-history-discipline.md +1 -2
- package/dist/agent-src/rules/guidelines.md +0 -1
- package/dist/agent-src/rules/history-discipline.md +0 -1
- package/dist/agent-src/rules/icon-consistency.md +0 -1
- package/dist/agent-src/rules/image-likeness-and-rights.md +0 -1
- package/dist/agent-src/rules/improve-before-implement.md +0 -1
- package/dist/agent-src/rules/invite-challenge.md +0 -1
- package/dist/agent-src/rules/laravel-routing.md +1 -2
- package/dist/agent-src/rules/laravel-translations.md +0 -1
- package/dist/agent-src/rules/legal-safety-floor.md +0 -1
- package/dist/agent-src/rules/lethal-trifecta-guard.md +0 -1
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -1
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +0 -1
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
- package/dist/agent-src/rules/media-governance-routing.md +1 -2
- package/dist/agent-src/rules/media-sync-ground-truth.md +0 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +0 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +0 -1
- package/dist/agent-src/rules/missing-tool-handling.md +0 -1
- package/dist/agent-src/rules/model-recommendation.md +0 -1
- package/dist/agent-src/rules/neighbour-precedence.md +64 -0
- package/dist/agent-src/rules/no-attribution-footers.md +0 -1
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +0 -1
- package/dist/agent-src/rules/no-pr-progress-comments.md +0 -1
- package/dist/agent-src/rules/no-roadmap-references.md +0 -1
- package/dist/agent-src/rules/notes-first-reasoning.md +0 -1
- package/dist/agent-src/rules/onboarding-gate.md +0 -1
- package/dist/agent-src/rules/output-discipline.md +0 -1
- package/dist/agent-src/rules/package-ci-checks.md +0 -1
- package/dist/agent-src/rules/persona-governance.md +2 -3
- package/dist/agent-src/rules/php-coding.md +0 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -1
- package/dist/agent-src/rules/preservation-guard.md +0 -1
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -3
- package/dist/agent-src/rules/question-not-instruction.md +0 -1
- package/dist/agent-src/rules/reviewer-awareness.md +0 -1
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +0 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +3 -6
- package/dist/agent-src/rules/role-mode-adherence.md +0 -1
- package/dist/agent-src/rules/rule-type-governance.md +0 -1
- package/dist/agent-src/rules/runtime-safety.md +0 -1
- package/dist/agent-src/rules/scale-discipline.md +0 -1
- package/dist/agent-src/rules/secret-vcs-guard.md +0 -1
- package/dist/agent-src/rules/security-sensitive-stop.md +0 -1
- package/dist/agent-src/rules/self-repair-loop.md +0 -1
- package/dist/agent-src/rules/senior-engineering-discipline.md +0 -1
- package/dist/agent-src/rules/session-canary.md +0 -1
- package/dist/agent-src/rules/settings-ask-protocol.md +5 -6
- package/dist/agent-src/rules/size-enforcement.md +0 -1
- package/dist/agent-src/rules/skill-improvement-trigger.md +2 -3
- package/dist/agent-src/rules/skill-quality.md +0 -1
- package/dist/agent-src/rules/slash-command-routing-policy.md +0 -1
- package/dist/agent-src/rules/source-confidentiality.md +0 -1
- package/dist/agent-src/rules/source-discovery-gate.md +0 -1
- package/dist/agent-src/rules/source-of-truth.md +0 -1
- package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -2
- package/dist/agent-src/rules/strategy-safety-floor.md +0 -1
- package/dist/agent-src/rules/symfony-routing.md +1 -2
- package/dist/agent-src/rules/think-before-action.md +0 -1
- package/dist/agent-src/rules/token-budget-discipline.md +14 -48
- package/dist/agent-src/rules/token-efficiency.md +0 -1
- package/dist/agent-src/rules/token-optimizer-maintenance.md +0 -1
- package/dist/agent-src/rules/tool-safety.md +0 -1
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -1
- package/dist/agent-src/rules/upstream-proposal.md +0 -1
- package/dist/agent-src/rules/user-interaction.md +0 -1
- package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
- package/dist/agent-src/scripts/gate_budget.ts +47 -23
- package/dist/agent-src/scripts/gate_execute.ts +12 -4
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +2 -0
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +7 -4
- package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +21 -11
- package/dist/agent-src/skills/ai-council/references/procedure.md +12 -6
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/dist/agent-src/skills/bug-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +4 -2
- package/dist/agent-src/skills/command-routing/SKILL.md +3 -4
- package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +127 -139
- package/dist/agent-src/skills/corpus-grounding/ATTRIBUTION.md +39 -0
- package/dist/agent-src/skills/corpus-grounding/SKILL.md +8 -2
- package/dist/agent-src/skills/database/data/manifest.json +2 -2
- package/dist/agent-src/skills/database/data/query-tuning.csv +13 -13
- package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +13 -10
- package/dist/agent-src/skills/design-system-capture/SKILL.md +0 -3
- package/dist/agent-src/skills/estimate-ticket/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +49 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +1 -1
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +9 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +44 -44
- package/dist/agent-src/skills/git-workflow/references/branch-update.md +312 -0
- package/dist/agent-src/skills/git-workflow/references/commit-subject.md +205 -0
- package/dist/agent-src/skills/iconography/SKILL.md +4 -2
- package/dist/agent-src/skills/image-provider-routing/SKILL.md +16 -5
- package/dist/agent-src/skills/jira-integration/SKILL.md +3 -3
- package/dist/agent-src/skills/logo-generation/SKILL.md +12 -8
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +10 -4
- package/dist/agent-src/skills/po-discovery/SKILL.md +1 -1
- package/dist/agent-src/skills/refine-ticket/SKILL.md +4 -4
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +4 -2
- package/dist/agent-src/skills/source-discovery/SKILL.md +1 -1
- package/dist/agent-src/skills/svg-authoring/SKILL.md +318 -0
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +1 -1
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +2 -2
- package/dist/agent-src/skills/wireframe/SKILL.md +5 -2
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agent-settings.md +46 -129
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +3 -3
- package/dist/agent-src/templates/command.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +6 -1
- package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -6
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +11 -7
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +3 -2
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +2 -1
- package/dist/agent-src/templates/scripts/work_engine/hooks/settings.ts +0 -6
- package/dist/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +9 -12
- package/dist/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +2 -1
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/commands/uiRender.js +201 -4
- package/dist/cli/commands/uiRenderAsset.js +1308 -0
- package/dist/cli/commands/uiRenderAssetCompare.js +752 -0
- package/dist/cli/commands/uiRenderAssetGrid.js +265 -0
- package/dist/cli/commands/uiRenderAssetGrounds.js +323 -0
- package/dist/cli/commands/uiRenderAssetIsolation.js +85 -0
- package/dist/cli/commands/uiRenderAssetTypes.js +145 -0
- package/dist/cli/main.js +47 -1
- package/dist/cli/registry.js +2 -1
- package/dist/cli-delegate/check_update_banner.js +195 -119
- package/dist/cli-delegate/{chunk-S4UMZNES.js → chunk-4DBG5K7M.js} +5 -5
- package/dist/cli-delegate/{chunk-TRI6DQZ6.js → chunk-5GPULQBE.js} +60 -42
- package/dist/cli-delegate/{chunk-JQVYKIZA.js → chunk-6SRJTYJS.js} +33 -177
- package/dist/cli-delegate/chunk-6V5FMA3P.js +186 -0
- package/dist/cli-delegate/chunk-6YQC2HLF.js +415 -0
- package/dist/cli-delegate/{chunk-E4XIZDIB.js → chunk-7HKRE54K.js} +1 -64
- package/dist/cli-delegate/{chunk-7HQ52DHU.js → chunk-AL3RZ22D.js} +1 -1
- package/dist/cli-delegate/{chunk-DBH4AHDK.js → chunk-ED7KCMLB.js} +9 -11
- package/dist/cli-delegate/{chunk-NXEILF2K.js → chunk-F3QDG2GT.js} +481 -72
- package/dist/cli-delegate/{chunk-CQWX336L.js → chunk-G5VVIZRC.js} +3 -3
- package/dist/cli-delegate/{chunk-JY2I2DXV.js → chunk-H76PHZBI.js} +137 -60
- package/dist/cli-delegate/chunk-IAYTIIQW.js +67 -0
- package/dist/cli-delegate/{chunk-LIYTLH6O.js → chunk-KXXWJTDO.js} +5 -10
- package/dist/cli-delegate/{chunk-T7L2QK3K.js → chunk-LQOCRFWT.js} +1 -1
- package/dist/cli-delegate/{chunk-S4YWNRKO.js → chunk-MDPPKQCQ.js} +34 -1
- package/dist/cli-delegate/chunk-OAFSJBHH.js +272 -0
- package/dist/cli-delegate/{chunk-GKE7ZVFA.js → chunk-PWX7CKMD.js} +1 -1
- package/dist/cli-delegate/{chunk-NDJLNC6Q.js → chunk-T6BP34WI.js} +1 -0
- package/dist/cli-delegate/{chunk-RQNNW7IF.js → chunk-T7XSJSUQ.js} +433 -632
- package/dist/cli-delegate/{chunk-NQ7WT43P.js → chunk-TMWYUOK4.js} +75 -12
- package/dist/cli-delegate/{chunk-EEHDXSYD.js → chunk-WQWXGIWB.js} +12 -273
- package/dist/cli-delegate/{chunk-QLDHIPWR.js → chunk-XQTPXKFA.js} +1 -1
- package/dist/cli-delegate/cmd_analyze_session.js +223 -43
- package/dist/cli-delegate/cmd_brand_status.js +3 -2
- package/dist/cli-delegate/cmd_conformance.js +489 -238
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +15 -12
- package/dist/cli-delegate/cmd_doctor_neighbours.js +11 -4
- package/dist/cli-delegate/cmd_explain.js +43 -19
- package/dist/cli-delegate/cmd_export.js +10 -10
- package/dist/cli-delegate/cmd_git_convention.js +1851 -0
- package/dist/cli-delegate/cmd_handoff.js +40 -21
- package/dist/cli-delegate/cmd_mcp_available.js +3 -2
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +4 -3
- package/dist/cli-delegate/cmd_prune.js +10 -10
- package/dist/cli-delegate/cmd_route_audit.js +7 -7
- package/dist/cli-delegate/cmd_route_explain.js +5 -5
- package/dist/cli-delegate/cmd_session_recycle.js +9 -7
- package/dist/cli-delegate/cmd_settings_check.js +54 -7
- package/dist/cli-delegate/cmd_settings_get.js +18 -8
- package/dist/cli-delegate/cmd_settings_migrate.js +10 -10
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +10 -10
- package/dist/cli-delegate/cmd_uninstall.js +10 -10
- package/dist/cli-delegate/cmd_update.js +7 -7
- package/dist/cli-delegate/cmd_upgrade.js +7 -7
- package/dist/cli-delegate/cmd_validate.js +10 -10
- package/dist/cli-delegate/cmd_versions.js +5 -5
- package/dist/cli-delegate/gate_budget.js +1 -1
- package/dist/cli-delegate/gate_execute.js +2 -2
- package/dist/cli-delegate/roadmap_gates.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +171 -107
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +7 -7
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +41 -22
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +16 -11
- package/dist/hooks/dispatch.js +2520 -1924
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/svg_rules.chunk.cjs +1626 -0
- package/dist/hooks/svg_rules.chunk.cjs.sha256 +1 -0
- package/dist/install/install.mjs +462 -431
- package/dist/install/installThinLayer.js +40 -12
- package/dist/install/installedRuleLinks.js +105 -19
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +5 -5
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3063 -2160
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +15 -120
- package/dist/scripts/_lib/compile_time_toggles.js +12 -21
- package/dist/scripts/_lib/git_convention.js +232 -0
- package/dist/scripts/_lib/global_deploy_inventory.js +730 -0
- package/dist/scripts/_lib/installed_layer.js +344 -18
- package/dist/scripts/_lib/package_root.js +68 -0
- package/dist/scripts/_lib/rule_law_section.js +1 -1
- package/dist/scripts/_lib/scan_scope.js +105 -0
- package/dist/scripts/_lib/settings_removed_keys.js +168 -0
- package/dist/scripts/_lib/svg_dom.js +284 -0
- package/dist/scripts/_lib/svg_geometry.js +477 -0
- package/dist/scripts/_lib/svg_motion.js +179 -0
- package/dist/scripts/_lib/svg_rules.js +1337 -0
- package/dist/scripts/_lib/svg_urls.js +67 -0
- package/dist/scripts/_lib/thin_rules.js +80 -3
- package/dist/scripts/lint_svg_asset.js +324 -0
- package/dist/server/app.js +2 -1
- package/dist/server/gitKeysGate.js +105 -0
- package/dist/server/routes/settings.js +67 -8
- package/dist/server/routes/wizard.js +4 -0
- package/dist/server/schemas/settings.js +23 -81
- package/dist/shared/skillRanking.js +30 -4
- package/dist/ui/assets/{index-Cwq4d0hX.js → index-C3djEJah.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +72 -1
- package/docs/contracts/settings-classes.md +35 -65
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +39 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/cross-source-consistency-mechanics.md +8 -10
- package/docs/guidelines/agent-infra/frontier-reasoning-operating-profile.md +3 -2
- package/docs/guidelines/agent-infra/layered-settings.md +13 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +10 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +36 -0
- package/docs/guidelines/design-antipatterns.md +1 -1
- package/docs/guidelines/php/git.md +15 -2
- package/package.json +4 -3
- package/src/agent-src/scripts/gate_budget.ts +47 -23
- package/src/agent-src/scripts/gate_execute.ts +12 -4
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -6
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +11 -7
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +3 -2
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +2 -1
- package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +0 -6
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +9 -12
- package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +2 -1
- package/src/config/agent-settings.template.yml +79 -312
- package/src/config/agents-paths.yml +16 -0
- package/src/config/assurance-capability-registry.json +7 -3
- package/src/config/ci-local-parity.yml +3 -0
- package/src/config/continuity-surface.json +3 -3
- package/src/config/discovery/packs.yml +17 -1
- package/src/config/evaluator-budgets.json +19 -6
- package/src/config/gate-coverage.yml +65 -23
- package/src/config/gate-violation-baselines.json +56 -16
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/hook-bundle-budget.json +1 -1
- package/src/config/release-gate-locality.yml +2 -1
- package/src/config/routing-coverage-seed.json +55 -6
- package/src/config/rule-law-ceilings.json +12 -6
- package/src/config/rule-obligations.json +833 -0
- package/src/install/installThinLayer.ts +76 -10
- package/src/install/installedRuleLinks.ts +105 -17
- package/src/scripts/_cli/cmd_analyze_session.ts +9 -15
- package/src/scripts/_cli/cmd_conformance.ts +13 -2
- package/src/scripts/_cli/cmd_doctor.ts +10 -10
- package/src/scripts/_cli/cmd_doctor_neighbours.ts +133 -0
- package/src/scripts/_cli/cmd_explain.ts +72 -14
- package/src/scripts/_cli/cmd_git_convention.ts +637 -0
- package/src/scripts/_cli/cmd_settings_check.ts +66 -8
- package/src/scripts/_cli/cmd_settings_get.ts +18 -3
- package/src/scripts/_cli/doctor_execution.ts +72 -1
- package/src/scripts/_dispatch.bash +37 -2
- package/src/scripts/_lib/agent_settings.ts +15 -124
- package/src/scripts/_lib/blocked_by_marker.ts +1 -1
- package/src/scripts/_lib/compile_time_toggles.ts +12 -21
- package/src/scripts/_lib/dispatch_import_closure.ts +163 -0
- package/src/scripts/_lib/git_base_ref.ts +92 -0
- package/src/scripts/_lib/git_convention.ts +291 -0
- package/src/scripts/_lib/git_convention_carrier.ts +389 -0
- package/src/scripts/_lib/git_convention_grammar.ts +246 -0
- package/src/scripts/_lib/git_convention_measure.ts +344 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +74 -0
- package/src/scripts/_lib/graph_feeder_record.ts +236 -13
- package/src/scripts/_lib/installed_layer.ts +448 -23
- package/src/scripts/_lib/knowledge_global.ts +14 -1
- package/src/scripts/_lib/module_reach.ts +456 -0
- package/src/scripts/_lib/neighbour_contradictions.ts +236 -0
- package/src/scripts/_lib/neighbour_tool_use.ts +8 -10
- package/src/scripts/_lib/ratification_artifact.ts +220 -1
- package/src/scripts/_lib/ratification_subject.ts +92 -0
- package/src/scripts/_lib/release_findings_ingest.ts +15 -7
- package/src/scripts/_lib/release_highlights.ts +5 -0
- package/src/scripts/_lib/rule_law_section.ts +1 -1
- package/src/scripts/_lib/rule_obligations.ts +205 -0
- package/src/scripts/_lib/self_repair.ts +1 -1
- package/src/scripts/_lib/settings_removed_keys.ts +173 -0
- package/src/scripts/_lib/svg_dom.ts +318 -0
- package/src/scripts/_lib/svg_geometry.ts +480 -0
- package/src/scripts/_lib/svg_motion.ts +208 -0
- package/src/scripts/_lib/svg_rules.ts +1453 -0
- package/src/scripts/_lib/svg_urls.ts +72 -0
- package/src/scripts/_lib/test_red_state.ts +5 -0
- package/src/scripts/_lib/thin_rules.ts +83 -3
- package/src/scripts/_lib/touched_file_quality.ts +142 -6
- package/src/scripts/_lib/verification_command.ts +59 -0
- package/src/scripts/_lib/verify_clause.ts +27 -17
- package/src/scripts/_lib/worker_budget.ts +1 -1
- package/src/scripts/adr/regenerate_index.ts +16 -1
- package/src/scripts/ai_council/config.ts +7 -7
- package/src/scripts/ai_council/modification_review.ts +100 -0
- package/src/scripts/ai_council/modification_review_questions.ts +90 -0
- package/src/scripts/ai_council/orchestrator.ts +5 -5
- package/src/scripts/ai_council/spend_gate.ts +18 -7
- package/src/scripts/ai_team/config.ts +11 -16
- package/src/scripts/audit_user_type_axis.ts +20 -8
- package/src/scripts/bench_ab_clone.ts +1 -2
- package/src/scripts/bench_concern_spawn_path.ts +129 -0
- package/src/scripts/bench_graph_feeder_latency.ts +584 -0
- package/src/scripts/check_agent_artifact_location.ts +36 -0
- package/src/scripts/check_branch_freshness.ts +147 -24
- package/src/scripts/check_enforcement_coverage.ts +130 -0
- package/src/scripts/check_evaluator_budgets.ts +4 -0
- package/src/scripts/check_finding_dispositions.ts +88 -25
- package/src/scripts/check_gate_coverage.ts +4 -3
- package/src/scripts/check_gate_reachability.ts +48 -1
- package/src/scripts/check_generator_sync.ts +48 -0
- package/src/scripts/check_hook_bundle_composition.ts +107 -0
- package/src/scripts/check_kernel_edit_ratified.ts +86 -6
- package/src/scripts/check_pack_size.ts +1 -1
- package/src/scripts/check_release_manifest_content.ts +189 -0
- package/src/scripts/check_standing_rule_delivery.ts +96 -0
- package/src/scripts/check_suppression_hygiene.ts +2 -5
- package/src/scripts/ci_settle.ts +42 -5
- package/src/scripts/classify_merge_risk.ts +320 -0
- package/src/scripts/closure_scan.ts +91 -1
- package/src/scripts/command_suggester/settings.ts +8 -108
- package/src/scripts/command_suggester/types.ts +4 -5
- package/src/scripts/consumer_matrix.ts +102 -0
- package/src/scripts/context_hygiene_hook.ts +13 -23
- package/src/scripts/cost/preflight.mjs +12 -1
- package/src/scripts/council_cli.ts +7 -7
- package/src/scripts/dispatch_r2_reviewer.ts +24 -11
- package/src/scripts/explain_run.ts +8 -13
- package/src/scripts/generate_git_convention_grammar.ts +127 -0
- package/src/scripts/generate_host_cost_table.ts +20 -3
- package/src/scripts/handoff_context_hook.ts +86 -37
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +25 -6
- package/src/scripts/hooks/block_config_weakening.ts +48 -5
- package/src/scripts/hooks/block_plumbing_writes.ts +8 -4
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +16 -1
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +2 -3
- package/src/scripts/hooks/design_pass_hook.ts +157 -3
- package/src/scripts/hooks/evidence_independence.ts +12 -4
- package/src/scripts/hooks/mcp_usage_observation_hook.ts +121 -0
- package/src/scripts/hooks/push_settle_hook.ts +4 -3
- package/src/scripts/hooks/rule_inject_hook.ts +44 -3
- package/src/scripts/hooks/state_io.ts +8 -4
- package/src/scripts/hooks/svg_obligation.ts +497 -0
- package/src/scripts/hooks/svg_rules_loader.ts +133 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +7 -46
- package/src/scripts/hooks/turn_end_gate_hook.ts +6 -3
- package/src/scripts/install-hooks.sh +42 -14
- package/src/scripts/install.ts +7 -31
- package/src/scripts/lint_blocker_citations.ts +198 -0
- package/src/scripts/lint_consolidation_lineage.ts +6 -3
- package/src/scripts/lint_design_quality.ts +60 -12
- package/src/scripts/lint_evidence_artifacts.ts +9 -0
- package/src/scripts/lint_hook_concern_budget.ts +10 -21
- package/src/scripts/lint_motion_authority_drift.ts +27 -0
- package/src/scripts/lint_pack_boundaries.ts +406 -28
- package/src/scripts/lint_roadmap_blockers.ts +282 -29
- package/src/scripts/lint_roadmap_complexity.ts +4 -2
- package/src/scripts/lint_rule_law_section.ts +151 -21
- package/src/scripts/lint_rule_skill_pack_reach.ts +17 -0
- package/src/scripts/lint_settings_classes.ts +1 -1
- package/src/scripts/lint_svg_asset.ts +365 -0
- package/src/scripts/lint_ticked_unmet_criteria.ts +165 -0
- package/src/scripts/lint_workflow_security.ts +12 -1
- package/src/scripts/measure_skill_ranker_baseline.ts +5 -0
- package/src/scripts/model_rule_injection.ts +3 -3
- package/src/scripts/onboarding_gate_hook.ts +7 -6
- package/src/scripts/print_required_checks.ts +15 -3
- package/src/scripts/probe_host_compliance.ts +33 -5
- package/src/scripts/probe_turnaround.ts +183 -3
- package/src/scripts/ratification_header.ts +77 -0
- package/src/scripts/refusal_envelope.ts +138 -0
- package/src/scripts/release_publication.ts +29 -3
- package/src/scripts/report_dispatch_import_closure.ts +76 -0
- package/src/scripts/report_installed_rule_links.ts +118 -1
- package/src/scripts/report_module_reach.ts +212 -0
- package/src/scripts/report_obligation_mechanism.ts +16 -18
- package/src/scripts/report_parked_blockers.ts +222 -0
- package/src/scripts/report_required_checks_drift.ts +196 -0
- package/src/scripts/report_skill_ranker_confusion.ts +103 -2
- package/src/scripts/report_standing_payload_by_host.ts +1 -1
- package/src/scripts/routing_doctor.ts +2 -2
- package/src/scripts/schemas/agent-settings.schema.json +17 -31
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/skill.schema.json +1 -1
- package/src/scripts/self_review_gate.ts +39 -2
- package/src/scripts/skill_tools/score_skill_relevance.ts +28 -19
- package/src/scripts/sync_pr_branch.ts +403 -132
- package/src/scripts/trigger_eval_grandfather.json +0 -16
- package/src/server/schemas/settings.ts +33 -177
- package/src/shared/skillRanking.ts +53 -4
- package/dist/cli-delegate/chunk-NYX3L2PZ.js +0 -99
- package/src/scripts/_lib/config_chain.ts +0 -477
- package/src/scripts/_lib/conformance_report.ts +0 -403
- package/src/scripts/_lib/eval_discrimination.ts +0 -95
- package/src/scripts/_lib/evaluator_promotion.ts +0 -225
- package/src/scripts/_lib/file_slicer.ts +0 -117
- package/src/scripts/_lib/legacy_boundary_map.ts +0 -228
- package/src/scripts/_lib/minimality_tiebreak.ts +0 -168
- package/src/scripts/_lib/overbuild_lens_contract.ts +0 -196
- package/src/scripts/_lib/role_split.ts +0 -338
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The committed git convention: `.git-convention.yml` at the repository root
|
|
3
|
+
* (ADR-283), layered over the developer settings files.
|
|
4
|
+
*
|
|
5
|
+
* The developer files are gitignored, so a convention declared only there
|
|
6
|
+
* reaches the checkout it was written in and no worktree, fresh clone or CI
|
|
7
|
+
* run. The carrier is tracked, and where it is read from is the authority:
|
|
8
|
+
*
|
|
9
|
+
* - `update_strategy` at the resolved TARGET commit — the explicit `--base`
|
|
10
|
+
* when given, else the default branch — as the server reports it. The
|
|
11
|
+
* strategy that judges a pull request must not come from that pull request,
|
|
12
|
+
* the same ruling `branch_convergence.ts` records; a value on the branch is a
|
|
13
|
+
* candidate and is never adopted.
|
|
14
|
+
* - `commit_format` and `branch_pattern` at `HEAD`, at the repository root,
|
|
15
|
+
* because they shape the branch's own commits and names.
|
|
16
|
+
*
|
|
17
|
+
* In both cases the carrier is the highest-precedence layer, over every
|
|
18
|
+
* developer layer including the gitignored local one (the owner's D8), and a
|
|
19
|
+
* carrier that does not set a key leaves it to the developer layers read at
|
|
20
|
+
* the repository root, never per subdirectory. A target commit that cannot be
|
|
21
|
+
* resolved is `unresolvable`, never the default.
|
|
22
|
+
*
|
|
23
|
+
* The target is never inferred from a forge. A caller acting on a pull request
|
|
24
|
+
* already knows its base and passes it as `--base`; the default branch is the
|
|
25
|
+
* answer only for an operation that is not about a pull request. Inferring the
|
|
26
|
+
* base downstream took seven review rounds without converging, and a wrong
|
|
27
|
+
* inference reads a valid carrier from the wrong commit and exits 0.
|
|
28
|
+
*/
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { find_project_root } from './agent_settings.js';
|
|
34
|
+
import { parseBaseRef, splitResolvedRef } from './git_base_ref.js';
|
|
35
|
+
import {
|
|
36
|
+
GIT_CONVENTION_KEYS,
|
|
37
|
+
checkoutSource,
|
|
38
|
+
conventionReading,
|
|
39
|
+
parseLayerText,
|
|
40
|
+
readGitConventionKey,
|
|
41
|
+
type GitConventionKey,
|
|
42
|
+
type GitConventionLayer,
|
|
43
|
+
type GitConventionReading,
|
|
44
|
+
} from './git_convention.js';
|
|
45
|
+
|
|
46
|
+
/** The tracked repository-root file a team declares its git convention in. */
|
|
47
|
+
export const CARRIER_PATH = '.git-convention.yml';
|
|
48
|
+
|
|
49
|
+
/** Sized for a ref lookup (`ls-remote`), which transfers no objects. */
|
|
50
|
+
export const NETWORK_TIMEOUT_MS = 8_000;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The target-commit fetch transfers objects, and the target is routinely a
|
|
54
|
+
* commit the server has advanced past the local fetch, so a large repository
|
|
55
|
+
* needs longer than a ref lookup does. Running out of time here makes the
|
|
56
|
+
* reading `unresolvable` and stops a sync, so the bound is generous.
|
|
57
|
+
*/
|
|
58
|
+
export const CARRIER_FETCH_TIMEOUT_MS = 60_000;
|
|
59
|
+
|
|
60
|
+
export interface GitRunResult {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
out: string;
|
|
63
|
+
err: string;
|
|
64
|
+
timedOut: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type GitRunner = (cmd: string, args: readonly string[], cwd: string, timeoutMs: number) => GitRunResult;
|
|
68
|
+
|
|
69
|
+
export const runGit: GitRunner = (cmd, args, cwd, timeoutMs) => {
|
|
70
|
+
const r = spawnSync(cmd, [...args], { cwd, encoding: 'utf-8', timeout: timeoutMs, maxBuffer: 32 * 1024 * 1024 });
|
|
71
|
+
const code = (r.error as NodeJS.ErrnoException | undefined)?.code;
|
|
72
|
+
return { ok: r.status === 0, out: r.stdout ?? '', err: (r.stderr ?? '').trim(), timedOut: code === 'ETIMEDOUT' };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function sh(cmd: string, args: readonly string[], cwd: string): { ok: boolean; out: string; err: string } {
|
|
76
|
+
return runGit(cmd, args, cwd, NETWORK_TIMEOUT_MS);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The git questions target resolution asks, injected so no network is needed in tests. */
|
|
80
|
+
export interface TargetDeps {
|
|
81
|
+
/** The default branch as a remote-tracking ref (`origin/main`), or null. */
|
|
82
|
+
readonly defaultBranch: () => string | null;
|
|
83
|
+
/** The SHA the server reports for a ref right now, or null. */
|
|
84
|
+
readonly remoteSha: (ref: string) => string | null;
|
|
85
|
+
/** The configured remote names; without it only `origin/` marks a remote. */
|
|
86
|
+
readonly remotes?: () => readonly string[];
|
|
87
|
+
/** The default branch another remote's server names (`upstream/main`), or null. */
|
|
88
|
+
readonly defaultBranchOf?: (remote: string) => string | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type TargetReason = 'explicit-base-override' | 'repository-default-branch';
|
|
92
|
+
|
|
93
|
+
export interface TargetRef {
|
|
94
|
+
readonly ref: string;
|
|
95
|
+
readonly reason: TargetReason;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The ref a branch is judged against: `--base`, else the default branch.
|
|
100
|
+
* `defaultRef` is passed by a caller that already asked for it.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveTarget(deps: TargetDeps, override: string | null, defaultRef?: string | null): TargetRef | null {
|
|
103
|
+
// A bare name is the branch on origin: the local branch of that name is a
|
|
104
|
+
// clone-time copy nothing keeps current.
|
|
105
|
+
const given = override === null ? null : parseBaseRef(override, deps.remotes?.() ?? []);
|
|
106
|
+
if (given !== null) return { ref: given.ref, reason: 'explicit-base-override' };
|
|
107
|
+
const def = defaultRef === undefined ? deps.defaultBranch() : defaultRef;
|
|
108
|
+
return def === null ? null : { ref: def, reason: 'repository-default-branch' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The default branch the SERVER reports, from `git ls-remote --symref origin HEAD`.
|
|
113
|
+
*
|
|
114
|
+
* Pure and exported so the parse is testable without a network round trip, and
|
|
115
|
+
* because it closes a measured defect rather than a hypothetical one. Until
|
|
116
|
+
* 2026-09-03 the default branch was read from `refs/remotes/origin/HEAD` alone —
|
|
117
|
+
* a clone-time ref that is simply absent in some checkouts. Measured in a
|
|
118
|
+
* worktree of this repository the same day: the local form returned null, so
|
|
119
|
+
* `sync_pr_branch` refused with "no open PR and no origin/HEAD" while
|
|
120
|
+
* `check_branch_freshness`, three lines later in the same documented sequence,
|
|
121
|
+
* resolved `origin/main` from the server symref and passed. Mirrors
|
|
122
|
+
* `check_branch_freshness.ts:223`.
|
|
123
|
+
*/
|
|
124
|
+
export function parseSymrefDefault(lsRemoteOut: string): string | null {
|
|
125
|
+
for (const line of lsRemoteOut.split('\n')) {
|
|
126
|
+
const t = line.trim();
|
|
127
|
+
if (!t.startsWith('ref:')) continue;
|
|
128
|
+
const name = t.slice('ref:'.length).trim().split(/\s+/)[0] ?? '';
|
|
129
|
+
if (!name.startsWith('refs/heads/')) continue;
|
|
130
|
+
const short = name.slice('refs/heads/'.length).trim();
|
|
131
|
+
if (short !== '') return `origin/${short}`;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The SHA of exactly `refs/heads/<branch>` in `git ls-remote` output.
|
|
138
|
+
*
|
|
139
|
+
* A ls-remote pattern matches every ref whose trailing path components equal
|
|
140
|
+
* it and the output is sorted by refname, so `backport/main` is listed before
|
|
141
|
+
* `main`: the first line is not the branch that was asked for. A SHA-256
|
|
142
|
+
* repository reports 64 hex digits, a SHA-1 one 40.
|
|
143
|
+
*/
|
|
144
|
+
export function parseExactHeadSha(lsRemoteOut: string, branch: string): string | null {
|
|
145
|
+
const want = `refs/heads/${branch}`;
|
|
146
|
+
for (const line of lsRemoteOut.split('\n')) {
|
|
147
|
+
const [sha, name] = line.trim().split(/\s+/);
|
|
148
|
+
if (name === want && sha !== undefined && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(sha)) return sha;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function makeTargetDeps(repo: string, run: GitRunner = runGit): TargetDeps {
|
|
154
|
+
const ask = (cmd: string, args: readonly string[]): GitRunResult => run(cmd, args, repo, NETWORK_TIMEOUT_MS);
|
|
155
|
+
return {
|
|
156
|
+
// The server first, the local ref second: `refs/remotes/origin/HEAD` is
|
|
157
|
+
// not set in every checkout (see `parseSymrefDefault`).
|
|
158
|
+
defaultBranch: (): string | null => {
|
|
159
|
+
const remote = ask('git', ['ls-remote', '--symref', '--', 'origin', 'HEAD']);
|
|
160
|
+
const fromServer = remote.ok ? parseSymrefDefault(remote.out) : null;
|
|
161
|
+
if (fromServer !== null) return fromServer;
|
|
162
|
+
const head = ask('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
|
163
|
+
return head.ok && head.out.trim() !== '' ? head.out.trim() : null;
|
|
164
|
+
},
|
|
165
|
+
remoteSha: (ref: string): string | null => {
|
|
166
|
+
const { remote, branch } = splitResolvedRef(ref);
|
|
167
|
+
const out = ask('git', ['ls-remote', '--', remote, `refs/heads/${branch}`]);
|
|
168
|
+
return out.ok ? parseExactHeadSha(out.out, branch) : null;
|
|
169
|
+
},
|
|
170
|
+
remotes: (): readonly string[] => {
|
|
171
|
+
const out = ask('git', ['remote']);
|
|
172
|
+
return out.ok ? out.out.split('\n').map((r) => r.trim()).filter((r) => r !== '') : [];
|
|
173
|
+
},
|
|
174
|
+
defaultBranchOf: (remote: string): string | null => {
|
|
175
|
+
const out = ask('git', ['ls-remote', '--symref', '--', remote, 'HEAD']);
|
|
176
|
+
const named = out.ok ? parseSymrefDefault(out.out) : null;
|
|
177
|
+
return named === null ? null : `${remote}/${named.slice('origin/'.length)}`;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The same questions, each asked of git at most once. One run reads the
|
|
184
|
+
* strategy and then syncs; without this each step asked again, paying the
|
|
185
|
+
* network twice and able to judge the strategy against one commit and sync
|
|
186
|
+
* against another when the target moves in between.
|
|
187
|
+
*/
|
|
188
|
+
export function memoTargetDeps<T extends TargetDeps>(deps: T): T {
|
|
189
|
+
const cache = new Map<string, unknown>();
|
|
190
|
+
const once = <V>(key: string, ask: () => V): V => {
|
|
191
|
+
if (!cache.has(key)) cache.set(key, ask());
|
|
192
|
+
return cache.get(key) as V;
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
...deps,
|
|
196
|
+
defaultBranch: (): string | null => once('default', () => deps.defaultBranch()),
|
|
197
|
+
remoteSha: (ref: string): string | null => once(`sha ${ref}`, () => deps.remoteSha(ref)),
|
|
198
|
+
...(deps.remotes === undefined ? {} : { remotes: (): readonly string[] => once('remotes', () => (deps.remotes as () => readonly string[])()) }),
|
|
199
|
+
...(deps.defaultBranchOf === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: { defaultBranchOf: (remote: string): string | null => once(`default ${remote}`, () => (deps.defaultBranchOf as (r: string) => string | null)(remote)) }),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* `absent` (the commit has no carrier) and `no-commit` (the commit itself is
|
|
207
|
+
* unknown) are different facts; `timedOut` marks a commit the fetch ran out of
|
|
208
|
+
* time on, which says nothing about whether it exists.
|
|
209
|
+
*/
|
|
210
|
+
export type CommitBlob = { kind: 'absent' } | { kind: 'present'; text: string } | { kind: 'no-commit'; timedOut?: boolean };
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The carrier as one commit holds it. A commit the server reported but this
|
|
214
|
+
* clone has not fetched yet is fetched by its ref once; an answer of "the file
|
|
215
|
+
* does not exist" is only ever given for a commit that is present.
|
|
216
|
+
*/
|
|
217
|
+
export function carrierBlobAt(repo: string, sha: string, fetchRef: string | null, run: GitRunner = runGit): CommitBlob {
|
|
218
|
+
const local = (args: readonly string[]): GitRunResult => run('git', args, repo, NETWORK_TIMEOUT_MS);
|
|
219
|
+
const has = (): boolean => local(['cat-file', '-e', `${sha}^{commit}`]).ok;
|
|
220
|
+
if (!has()) {
|
|
221
|
+
const from = fetchRef === null ? null : splitResolvedRef(fetchRef);
|
|
222
|
+
const fetched = from === null ? null : run('git', ['fetch', '-q', '--', from.remote, from.branch], repo, CARRIER_FETCH_TIMEOUT_MS);
|
|
223
|
+
if (fetched?.timedOut === true) return { kind: 'no-commit', timedOut: true };
|
|
224
|
+
if (!has()) return { kind: 'no-commit' };
|
|
225
|
+
}
|
|
226
|
+
const listed = local(['ls-tree', '--name-only', sha, '--', CARRIER_PATH]);
|
|
227
|
+
if (!listed.ok) return { kind: 'no-commit' };
|
|
228
|
+
if (listed.out.trim() === '') return { kind: 'absent' };
|
|
229
|
+
const blob = local(['cat-file', '-p', `${sha}:${CARRIER_PATH}`]);
|
|
230
|
+
return blob.ok ? { kind: 'present', text: blob.out } : { kind: 'no-commit' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** A filename a carrier is sometimes given that nothing reads. */
|
|
234
|
+
export const IGNORED_CARRIER_PATH = '.git-convention.yaml';
|
|
235
|
+
|
|
236
|
+
/** The warning every reader prints when `root` holds a carrier under the name nothing reads, or null. */
|
|
237
|
+
export function ignoredCarrierWarning(root: string): string | null {
|
|
238
|
+
return fs.existsSync(path.join(root, IGNORED_CARRIER_PATH))
|
|
239
|
+
? `${IGNORED_CARRIER_PATH} is ignored — the team declaration is read only from ${CARRIER_PATH}; rename it to ${CARRIER_PATH}`
|
|
240
|
+
: null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The carrier holds its keys under `git:`, as the settings files do. Keys set at
|
|
245
|
+
* the top level are read by nothing, and reading them as absent would apply the
|
|
246
|
+
* default the file was written to replace.
|
|
247
|
+
*/
|
|
248
|
+
function _parseCarrier(text: string): Pick<GitConventionLayer, 'parsed' | 'data' | 'why' | 'shapeError'> {
|
|
249
|
+
const parsed = parseLayerText(text);
|
|
250
|
+
if (parsed.parsed !== 'valid') return parsed;
|
|
251
|
+
const top = Object.keys(parsed.data as Record<string, unknown>).filter((k) => (GIT_CONVENTION_KEYS as readonly string[]).includes(k));
|
|
252
|
+
if (top.length === 0) return parsed;
|
|
253
|
+
const listed = top.map((k) => `\`${k}\``).join(', ');
|
|
254
|
+
return { ...parsed, shapeError: `${listed} set at the top level; the carrier reads its keys under \`git:\` (\`git:\` on one line, then \` ${top[0]}: <value>\` indented)` };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function _carrierLayer(label: string, blob: Exclude<CommitBlob, { kind: 'no-commit' }>): GitConventionLayer {
|
|
258
|
+
if (blob.kind === 'absent') return { path: label, carries: true, parsed: 'absent', data: null };
|
|
259
|
+
return { path: label, carries: true, ..._parseCarrier(blob.text) };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Read failures map as `git_convention.ts` maps them for the developer files. */
|
|
263
|
+
function _workingTreeLayer(root: string): GitConventionLayer {
|
|
264
|
+
const p = path.join(root, CARRIER_PATH);
|
|
265
|
+
let text: string;
|
|
266
|
+
try {
|
|
267
|
+
if (!fs.statSync(p).isFile()) return { path: p, carries: true, parsed: 'absent', data: null };
|
|
268
|
+
text = fs.readFileSync(p, 'utf-8');
|
|
269
|
+
} catch (err) {
|
|
270
|
+
const parsed = (err as NodeJS.ErrnoException).code === 'ENOENT' ? 'absent' : 'malformed';
|
|
271
|
+
return { path: p, carries: true, parsed, data: null };
|
|
272
|
+
}
|
|
273
|
+
return { path: p, carries: true, ..._parseCarrier(text) };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The repository root, and whether it is a git repository at all. */
|
|
277
|
+
export function conventionRoot(cwd: string): { root: string; git: boolean } {
|
|
278
|
+
const top = sh('git', ['rev-parse', '--show-toplevel'], cwd);
|
|
279
|
+
if (top.ok && top.out.trim() !== '') return { root: top.out.trim(), git: true };
|
|
280
|
+
return { root: find_project_root(cwd) ?? cwd, git: false };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface ConventionTarget extends TargetRef {
|
|
284
|
+
readonly sha: string | null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface CommittedConvention {
|
|
288
|
+
readonly root: string;
|
|
289
|
+
/** The value in force per key. */
|
|
290
|
+
readonly readings: Partial<Record<GitConventionKey, GitConventionReading>>;
|
|
291
|
+
/** The commit the carrier was read at per key; null outside a git repository. */
|
|
292
|
+
readonly readAt: Partial<Record<GitConventionKey, string | null>>;
|
|
293
|
+
/** What `update_strategy` was judged against; null when no target was asked for. */
|
|
294
|
+
readonly target: ConventionTarget | null;
|
|
295
|
+
/** The checkout's own value per key, only where it differs from the value in force. */
|
|
296
|
+
readonly candidates: Partial<Record<GitConventionKey, GitConventionReading>>;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface CommittedOptions {
|
|
300
|
+
readonly override?: string | null;
|
|
301
|
+
readonly deps?: TargetDeps;
|
|
302
|
+
readonly keys?: readonly GitConventionKey[];
|
|
303
|
+
/** Runs the git calls that read the carrier at a commit. */
|
|
304
|
+
readonly run?: GitRunner;
|
|
305
|
+
/** `false` reads only a commit already in this checkout; a missing one is `unresolvable`, never fetched. */
|
|
306
|
+
readonly fetch?: boolean;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function _short(sha: string): string {
|
|
310
|
+
return sha.slice(0, 12);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The git convention in force for the repository that contains `cwd`. */
|
|
314
|
+
export function readCommittedConvention(cwd: string, options: CommittedOptions = {}): CommittedConvention {
|
|
315
|
+
const keys = options.keys ?? GIT_CONVENTION_KEYS;
|
|
316
|
+
const { root, git } = conventionRoot(cwd);
|
|
317
|
+
const developer = checkoutSource(root).layers();
|
|
318
|
+
const readings: Partial<Record<GitConventionKey, GitConventionReading>> = {};
|
|
319
|
+
const readAt: Partial<Record<GitConventionKey, string | null>> = {};
|
|
320
|
+
const candidates: Partial<Record<GitConventionKey, GitConventionReading>> = {};
|
|
321
|
+
let target: ConventionTarget | null = null;
|
|
322
|
+
|
|
323
|
+
// Outside a git repository there is no committed layer to read.
|
|
324
|
+
if (!git) {
|
|
325
|
+
for (const key of keys) {
|
|
326
|
+
readings[key] = readGitConventionKey(key, { layers: () => developer });
|
|
327
|
+
readAt[key] = null;
|
|
328
|
+
}
|
|
329
|
+
return { root, readings, readAt, target, candidates };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const headSha = sh('git', ['rev-parse', '--verify', '-q', 'HEAD'], root).out.trim();
|
|
333
|
+
const headLabel = `${CARRIER_PATH} at ${headSha === '' ? 'HEAD' : _short(headSha)}`;
|
|
334
|
+
let headBlob: CommitBlob | null = null;
|
|
335
|
+
const atHead = (): CommitBlob => {
|
|
336
|
+
if (headBlob === null) headBlob = headSha === '' ? { kind: 'absent' } : carrierBlobAt(root, headSha, null, options.run);
|
|
337
|
+
return headBlob;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
for (const key of keys) {
|
|
341
|
+
if (key !== 'update_strategy') {
|
|
342
|
+
const blob = atHead();
|
|
343
|
+
readings[key] = blob.kind === 'no-commit'
|
|
344
|
+
? conventionReading(key, 'unresolvable', null, headLabel, `the carrier at HEAD (${_short(headSha)}) could not be read`)
|
|
345
|
+
: readGitConventionKey(key, { layers: () => [...developer, _carrierLayer(headLabel, blob)] });
|
|
346
|
+
readAt[key] = headSha === '' ? null : headSha;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const deps = options.deps ?? makeTargetDeps(root);
|
|
350
|
+
const ref = resolveTarget(deps, options.override ?? null);
|
|
351
|
+
const sha = ref === null ? null : deps.remoteSha(ref.ref);
|
|
352
|
+
target = ref === null ? null : { ...ref, sha };
|
|
353
|
+
readAt[key] = sha;
|
|
354
|
+
if (ref === null || sha === null) {
|
|
355
|
+
readings[key] = conventionReading(
|
|
356
|
+
key,
|
|
357
|
+
'unresolvable',
|
|
358
|
+
null,
|
|
359
|
+
CARRIER_PATH,
|
|
360
|
+
ref === null
|
|
361
|
+
? 'no --base was given and origin names no default branch, so the target commit is unknown'
|
|
362
|
+
: `the server reports no commit for ${ref.ref} (no such branch on ${splitResolvedRef(ref.ref).remote}, no such remote, or it is unreachable)`,
|
|
363
|
+
);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const blob = carrierBlobAt(root, sha, options.fetch === false ? null : ref.ref, options.run);
|
|
367
|
+
if (blob.kind === 'no-commit') {
|
|
368
|
+
readings[key] = conventionReading(
|
|
369
|
+
key,
|
|
370
|
+
'unresolvable',
|
|
371
|
+
null,
|
|
372
|
+
`${CARRIER_PATH} at ${_short(sha)}`,
|
|
373
|
+
blob.timedOut === true
|
|
374
|
+
? `fetching commit ${_short(sha)} of ${ref.ref} timed out after ${CARRIER_FETCH_TIMEOUT_MS / 1000} s`
|
|
375
|
+
: `commit ${_short(sha)} of ${ref.ref} could not be fetched`,
|
|
376
|
+
);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
readings[key] = readGitConventionKey(key, { layers: () => [...developer, _carrierLayer(`${CARRIER_PATH} at ${_short(sha)}`, blob)] });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const tree = _workingTreeLayer(root);
|
|
383
|
+
for (const key of keys) {
|
|
384
|
+
const inForce = readings[key] as GitConventionReading;
|
|
385
|
+
const local = readGitConventionKey(key, { layers: () => [...developer, tree] });
|
|
386
|
+
if (local.value !== inForce.value || local.state !== inForce.state) candidates[key] = local;
|
|
387
|
+
}
|
|
388
|
+
return { root, readings, readAt, target, candidates };
|
|
389
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ticket grammar, the commit-subject grammars and the branch renderer.
|
|
3
|
+
*
|
|
4
|
+
* These were restated as regexes across several commands and checked by a model
|
|
5
|
+
* reading them, which let a ticket inside a compound scope pass and left an
|
|
6
|
+
* approved measured family with no validator at all. Pure: no I/O, so the CLI,
|
|
7
|
+
* the reference generator and the tests read the same values.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const TICKET_GRAMMAR = '[A-Z][A-Z0-9]+-[0-9]+';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The grammar as a whole token: no letter or digit right before the key and no
|
|
14
|
+
* digit right after the number, so `fooDEV-12` holds no ticket. A version-like
|
|
15
|
+
* key (`HTTP2-1`, `V2-3`) is still ticket-shaped under the grammar; a card's
|
|
16
|
+
* `ticket_keys` is what tells it apart.
|
|
17
|
+
*/
|
|
18
|
+
export const TICKET_TOKEN = `(?<![A-Za-z0-9])${TICKET_GRAMMAR}(?![0-9])`;
|
|
19
|
+
|
|
20
|
+
/** Prefixes of standard names that share the ticket shape (`UTF-8`, `CVE-2026-1234`). */
|
|
21
|
+
export const TICKET_DENYLIST = ['UTF', 'ISO', 'SHA', 'RFC', 'CVE', 'CWE', 'GHSA'] as const;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Protocol and language names that branch names carry with a major version
|
|
25
|
+
* (`HTTP-2`, `PHP-8`). Without a card such a key with a one-digit number is
|
|
26
|
+
* `version-like`, not a ticket; a card that lists the key keeps it a ticket.
|
|
27
|
+
* Two or more digits read as a ticket number, which is how the two are told apart.
|
|
28
|
+
*/
|
|
29
|
+
export const VERSION_LIKE_KEYS = ['HTTP', 'HTTPS', 'TLS', 'SSL', 'OAUTH', 'PHP', 'PYTHON', 'JAVA', 'JDK', 'NODE', 'ES', 'HTML', 'CSS'] as const;
|
|
30
|
+
|
|
31
|
+
export type TicketStatus = 'ticket' | 'standard-name' | 'version-like' | 'unknown-key';
|
|
32
|
+
|
|
33
|
+
export interface TicketCandidate {
|
|
34
|
+
token: string;
|
|
35
|
+
key: string;
|
|
36
|
+
status: TicketStatus;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The ticket token in either case, scanned only when a card names the keys. */
|
|
40
|
+
const ANY_CASE_TOKEN = `(?<![A-Za-z0-9])[A-Za-z][A-Za-z0-9]+-[0-9]+(?![0-9])`;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every ticket-shaped token in `text`, in order. With `keys`, a candidate whose
|
|
44
|
+
* key is not listed is `unknown-key`, and a key written in another case is
|
|
45
|
+
* matched against the list and returned upper-cased; a lower-case token whose
|
|
46
|
+
* key is not listed is not a candidate. Without `keys` only upper-case tokens
|
|
47
|
+
* count, and the grammar, the denylist and the version-like names decide.
|
|
48
|
+
*/
|
|
49
|
+
export function ticketCandidates(text: string, keys?: readonly string[] | null): TicketCandidate[] {
|
|
50
|
+
const card = keys !== undefined && keys !== null && keys.length > 0 ? keys : null;
|
|
51
|
+
const out: TicketCandidate[] = [];
|
|
52
|
+
for (const m of text.matchAll(new RegExp(card === null ? TICKET_TOKEN : ANY_CASE_TOKEN, 'g'))) {
|
|
53
|
+
const dash = m[0].indexOf('-');
|
|
54
|
+
const written = m[0].slice(0, dash);
|
|
55
|
+
const key = written.toUpperCase();
|
|
56
|
+
const number = m[0].slice(dash + 1);
|
|
57
|
+
if (written !== key && !card?.includes(key)) continue;
|
|
58
|
+
let status: TicketStatus = 'ticket';
|
|
59
|
+
if ((TICKET_DENYLIST as readonly string[]).includes(key)) status = 'standard-name';
|
|
60
|
+
else if (card !== null) status = card.includes(key) ? 'ticket' : 'unknown-key';
|
|
61
|
+
else if (number.length === 1 && (VERSION_LIKE_KEYS as readonly string[]).includes(key)) status = 'version-like';
|
|
62
|
+
out.push({ token: `${key}-${number}`, key, status });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function firstTicket(text: string, keys?: readonly string[] | null): string | null {
|
|
68
|
+
return ticketCandidates(text, keys).find((c) => c.status === 'ticket')?.token ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const KEY = /^[A-Z][A-Z0-9]+$/;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The approved project keys from a convention card's `ticket_keys` line, or
|
|
75
|
+
* from that line's value alone. A card without the line yields none.
|
|
76
|
+
*/
|
|
77
|
+
export function parseTicketKeys(text: string): string[] {
|
|
78
|
+
const line = /^\s*ticket_keys\s*:(.*)$/m.exec(text)?.[1];
|
|
79
|
+
const value = line ?? (text.includes(':') ? '' : text);
|
|
80
|
+
return value
|
|
81
|
+
.replace(/[[\]"']/g, ' ')
|
|
82
|
+
.split(/[\s,]+/)
|
|
83
|
+
.filter((k) => KEY.test(k));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The keys a commitlint config's `issuePrefixes` names: offered for the card, never written by code. */
|
|
87
|
+
export function commitlintIssuePrefixes(configText: string): string[] {
|
|
88
|
+
const list = /["']?issuePrefixes["']?\s*:\s*\[([^\]]*)\]/.exec(configText)?.[1] ?? '';
|
|
89
|
+
return [...list.matchAll(/["']([^"']*)["']/g)].map((m) => (m[1] as string).replace(/-$/, '')).filter((k) => KEY.test(k));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const COMMIT_TYPES = ['feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'perf', 'style', 'build', 'ci', 'revert'] as const;
|
|
93
|
+
|
|
94
|
+
export type CommitFormat = 'ticket-scope' | 'ticket-conventional';
|
|
95
|
+
export type SubjectFamily = 'conventional' | 'ticket-conventional' | 'ticket-prefix' | 'gitmoji' | 'imperative-plain';
|
|
96
|
+
|
|
97
|
+
const TYPES = COMMIT_TYPES.join('|');
|
|
98
|
+
|
|
99
|
+
/** The `git.commit_format` grammars, in the syntax a JavaScript or PCRE engine reads. */
|
|
100
|
+
export const FORMAT_GRAMMAR: Readonly<Record<CommitFormat, string>> = {
|
|
101
|
+
'ticket-scope': `^(${TYPES})(\\([^)]+\\))?!?: .+`,
|
|
102
|
+
'ticket-conventional': `^(${TICKET_GRAMMAR} )?(${TYPES})(\\([^)]+\\))?!?: .+`,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The measured families as POSIX extended regular expressions, matched in
|
|
107
|
+
* order, first hit wins; anything else is `other`.
|
|
108
|
+
*/
|
|
109
|
+
export const FAMILY_ERE: ReadonlyArray<readonly [SubjectFamily, string]> = [
|
|
110
|
+
['conventional', '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([^)]+\\))?!?: '],
|
|
111
|
+
['ticket-conventional', `^${TICKET_GRAMMAR} (build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([^)]+\\))?!?: `],
|
|
112
|
+
['ticket-prefix', `^\\[${TICKET_GRAMMAR}\\][: ]|^${TICKET_GRAMMAR}[: ]`],
|
|
113
|
+
// `[:ascii:]` is not a POSIX class (GNU and BSD grep reject it); a leading
|
|
114
|
+
// character outside printable ASCII, the controls and the spaces is the same set.
|
|
115
|
+
['gitmoji', '^:[a-z0-9_+-]+:[[:space:]]|^[^ -~[:cntrl:][:space:]]'],
|
|
116
|
+
['imperative-plain', '^[A-Z][a-z]+[[:space:]].*[^.]$'],
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
/** The two POSIX bracket expressions the families use, and nothing else, translated for a JavaScript engine. */
|
|
120
|
+
function _js(ere: string): RegExp {
|
|
121
|
+
return new RegExp(ere.replaceAll('[^ -~[:cntrl:][:space:]]', '[^\\x00-\\x7F\\s]').replaceAll('[[:space:]]', '\\s'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const FAMILY_JS: ReadonlyArray<readonly [SubjectFamily, RegExp]> = FAMILY_ERE.map(([f, ere]) => [f, _js(ere)] as const);
|
|
125
|
+
|
|
126
|
+
export function classifySubject(subject: string): SubjectFamily | 'other' {
|
|
127
|
+
return FAMILY_JS.find(([, re]) => re.test(subject))?.[0] ?? 'other';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type SubjectRule = { format: CommitFormat } | { family: SubjectFamily };
|
|
131
|
+
export type SubjectVerdict = { ok: true } | { ok: false; rule: string };
|
|
132
|
+
|
|
133
|
+
const SCOPE = /^[^(:]*\(([^)]*)\)/;
|
|
134
|
+
|
|
135
|
+
/** Grammars where the ticket leads the subject, so a ticket in the scope is a second, misplaced copy. */
|
|
136
|
+
function _ticketLeads(rule: SubjectRule): boolean {
|
|
137
|
+
return 'format' in rule ? rule.format === 'ticket-conventional' : rule.family === 'ticket-conventional';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function ruleName(rule: SubjectRule): string {
|
|
141
|
+
return 'format' in rule ? `git.commit_format: ${rule.format}` : `approved family ${rule.family}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Subjects git writes itself (`commit --fixup`, `--squash`, `--fixup=amend:`, `revert`), valid under every format and every approved family. */
|
|
145
|
+
export const GIT_OWN_SUBJECT = '^((fixup|squash|amend)! .+|Revert ".+")$';
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A family's form without its ticket, for the families whose ticket is a
|
|
149
|
+
* leading part (owner decision 2026-10-07): the ticket is optional, as under
|
|
150
|
+
* the `ticket-conventional` setting value, and when present it leads.
|
|
151
|
+
*/
|
|
152
|
+
export const TICKETLESS_FORM: Readonly<Partial<Record<SubjectFamily, string>>> = {
|
|
153
|
+
'ticket-prefix': '^\\S.*',
|
|
154
|
+
'ticket-conventional': FAMILY_ERE[0]?.[1] ?? '',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export function checkSubject(subject: string, rule: SubjectRule): SubjectVerdict {
|
|
158
|
+
if (new RegExp(GIT_OWN_SUBJECT).test(subject)) return { ok: true };
|
|
159
|
+
const grammar = 'format' in rule ? new RegExp(FORMAT_GRAMMAR[rule.format]) : (FAMILY_JS.find(([f]) => f === rule.family)?.[1] as RegExp);
|
|
160
|
+
const ticketless = 'family' in rule ? TICKETLESS_FORM[rule.family] : undefined;
|
|
161
|
+
if (!grammar.test(subject) && ticketless !== undefined) {
|
|
162
|
+
const family = (rule as { family: SubjectFamily }).family;
|
|
163
|
+
if (!_js(ticketless).test(subject)) {
|
|
164
|
+
return { ok: false, rule: `${ruleName(rule)} — does not match ${FAMILY_ERE.find(([f]) => f === family)?.[1] ?? ''}, nor its form without a ticket ${ticketless}` };
|
|
165
|
+
}
|
|
166
|
+
const stray = ticketCandidates(subject).find((c) => c.status === 'ticket');
|
|
167
|
+
if (stray !== undefined) {
|
|
168
|
+
return { ok: false, rule: `${ruleName(rule)} — the ticket \`${stray.token}\` stands outside the leading position; it leads the subject or is left out` };
|
|
169
|
+
}
|
|
170
|
+
return { ok: true };
|
|
171
|
+
}
|
|
172
|
+
if (!grammar.test(subject)) {
|
|
173
|
+
return { ok: false, rule: `${ruleName(rule)} — does not match ${'format' in rule ? FORMAT_GRAMMAR[rule.format] : (FAMILY_ERE.find(([f]) => f === rule.family)?.[1] ?? '')}` };
|
|
174
|
+
}
|
|
175
|
+
if (!_ticketLeads(rule)) return { ok: true };
|
|
176
|
+
const lead = /^(\S+) /.exec(subject)?.[1] ?? '';
|
|
177
|
+
const leading = ticketCandidates(lead).find((c) => c.token === lead);
|
|
178
|
+
if (leading !== undefined && leading.status === 'standard-name') {
|
|
179
|
+
return { ok: false, rule: `${ruleName(rule)} — \`${lead}\` is a standard name, not a ticket` };
|
|
180
|
+
}
|
|
181
|
+
const scope = SCOPE.exec(lead === '' || leading === undefined ? subject : subject.slice(lead.length + 1))?.[1] ?? '';
|
|
182
|
+
const inScope = ticketCandidates(scope).find((c) => c.status === 'ticket');
|
|
183
|
+
if (inScope !== undefined) {
|
|
184
|
+
return { ok: false, rule: `${ruleName(rule)} — the ticket \`${inScope.token}\` stands inside the scope; it leads the subject, never the scope` };
|
|
185
|
+
}
|
|
186
|
+
return { ok: true };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface BranchInput {
|
|
190
|
+
type?: string | null;
|
|
191
|
+
ticket?: string | null;
|
|
192
|
+
slug: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type BranchRender = { ok: true; name: string } | { ok: false; reason: string };
|
|
196
|
+
|
|
197
|
+
const VALUE = /^[A-Za-z0-9._-]+$/;
|
|
198
|
+
const SEPARATORS = '-/_.';
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* `pattern` with its placeholders filled. An empty placeholder drops together
|
|
202
|
+
* with the separator after it, or the one before it when it is last; a ticket
|
|
203
|
+
* the pattern has no slot for prefixes the slug. Literal characters of the
|
|
204
|
+
* pattern are never rewritten, and a value outside `[A-Za-z0-9._-]` is refused
|
|
205
|
+
* rather than repaired, because the name reaches shell commands.
|
|
206
|
+
*/
|
|
207
|
+
export function renderBranch(pattern: string, input: BranchInput): BranchRender {
|
|
208
|
+
const type = input.type ?? '';
|
|
209
|
+
const ticket = input.ticket ?? '';
|
|
210
|
+
let slug = input.slug;
|
|
211
|
+
if (slug === '' || !VALUE.test(slug)) return { ok: false, reason: `slug ${JSON.stringify(slug)} is empty or outside [A-Za-z0-9._-]` };
|
|
212
|
+
if (type !== '' && !VALUE.test(type)) return { ok: false, reason: `type ${JSON.stringify(type)} is outside [A-Za-z0-9._-]` };
|
|
213
|
+
if (ticket !== '' && !new RegExp(`^${TICKET_GRAMMAR}$`).test(ticket)) {
|
|
214
|
+
return { ok: false, reason: `ticket ${JSON.stringify(ticket)} does not match ${TICKET_GRAMMAR}` };
|
|
215
|
+
}
|
|
216
|
+
if (ticket !== '' && !pattern.includes('{ticket}') && slug !== ticket && !slug.startsWith(`${ticket}-`)) slug = `${ticket}-${slug}`;
|
|
217
|
+
|
|
218
|
+
const values: Record<string, string> = { type, ticket, slug };
|
|
219
|
+
const parts = pattern.split(/(\{(?:type|ticket|slug)\})/).filter((p) => p !== '');
|
|
220
|
+
const out: string[] = [];
|
|
221
|
+
for (let i = 0; i < parts.length; i++) {
|
|
222
|
+
const part = parts[i] as string;
|
|
223
|
+
const ph = /^\{(type|ticket|slug)\}$/.exec(part);
|
|
224
|
+
if (ph === null) {
|
|
225
|
+
out.push(part);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const value = values[ph[1] as string] as string;
|
|
229
|
+
if (value !== '') {
|
|
230
|
+
out.push(value);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const next = parts[i + 1];
|
|
234
|
+
if (next !== undefined && !next.startsWith('{') && SEPARATORS.includes(next[0] as string)) {
|
|
235
|
+
parts[i + 1] = next.slice(1);
|
|
236
|
+
} else if (next === undefined && out.length > 0) {
|
|
237
|
+
const prev = out[out.length - 1] as string;
|
|
238
|
+
if (SEPARATORS.includes(prev.slice(-1))) out[out.length - 1] = prev.slice(0, -1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const name = out.join('');
|
|
242
|
+
if (name === '' || name.startsWith('/') || name.endsWith('/') || name.includes('//') || name.includes('..')) {
|
|
243
|
+
return { ok: false, reason: `the rendered name ${JSON.stringify(name)} is not a branch name` };
|
|
244
|
+
}
|
|
245
|
+
return { ok: true, name };
|
|
246
|
+
}
|