@joshuafolkken/kit 1.1455.0 → 1.1514.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/.claude/settings.json +6 -6
- package/.claude/skills/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +83 -156
- package/.claude/skills/epic-commands/epic-bundle.md +51 -79
- package/.claude/skills/epic-commands/execution-waves.md +43 -81
- package/.claude/skills/verify-ui/SKILL.md +24 -50
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +31 -46
- package/.claude/skills/workflow-commands/backlogrun-child.md +65 -85
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +86 -125
- package/.claude/skills/workflow-commands/backlogrun-park.md +68 -118
- package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -28
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +17 -26
- package/.claude/skills/workflow-commands/backlogrun-steps.md +35 -52
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +31 -40
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +55 -77
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +59 -118
- package/.claude/skills/workflow-commands/observation-ledger.md +52 -112
- package/.claude/skills/workflow-commands/pre-gate-cut.md +88 -142
- package/.claude/skills/workflow-commands/prerequisite.md +11 -17
- package/.claude/skills/workflow-commands/progress-watcher.md +23 -33
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +35 -54
- package/.claude/skills/workflow-commands/split-assessment.md +32 -58
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -73
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +7 -6
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +7 -6
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-6TS2JD6D.js} +14 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-5H4SSDSD.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-APUO6U4J.js} +102 -19
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/chunk-JP45SOCJ.js +56 -0
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-TBJFTSUY.js → chunk-O6GDRNJB.js} +9 -7
- package/dist/hooks/{chunk-HFGWYGUV.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-WNLVGT7E.js → chunk-PTICX4OL.js} +4 -5
- package/dist/hooks/{chunk-HHI6FO3R.js → chunk-RT27QRJW.js} +763 -1515
- package/dist/hooks/{chunk-7WQBPLUN.js → chunk-SYOTDYCI.js} +1149 -1095
- package/dist/hooks/{chunk-LIAFCXL7.js → chunk-TNUMY6NL.js} +1519 -1332
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-WK6SMBIC.js} +1932 -784
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +302 -247
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +372 -26
- package/dist/hooks/stop-guard.js +2593 -189
- package/dist/josh.js +2128 -282
- package/dist/managed-marker/index.js +1 -2
- package/dist/version/index.js +15 -12
- package/eslint/base.js +2 -1
- package/eslint/rules/unicorn.js +16 -0
- package/lefthook/base.yml +5 -3
- package/package.json +14 -12
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +25 -45
- package/prompts/collaboration-workflow/gh-rest.md +19 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +6 -14
- package/prompts/collaboration-workflow/issue-template.md +40 -32
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +33 -46
- package/prompts/collaboration-workflow/output-bounds.md +15 -15
- package/prompts/collaboration-workflow/overview.md +14 -19
- package/prompts/collaboration-workflow/plan-comment.md +13 -13
- package/prompts/collaboration-workflow/principles.md +57 -54
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +41 -49
- package/prompts/collaboration-workflow/residency.md +15 -11
- package/prompts/collaboration-workflow/rule-delivery.md +7 -15
- package/prompts/collaboration-workflow/shell-body.md +10 -20
- package/prompts/collaboration-workflow/turn-batching.md +8 -14
- package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
- package/prompts/collaboration-workflow/wip-cap.md +26 -32
- package/prompts/collaboration-workflow.md +25 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +110 -141
- package/prompts/sonar-hotspot-handling.md +22 -50
- package/prompts/testing-guide.md +48 -99
- package/scripts/adopt/adopt-toolkits.ts +2 -4
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +9 -2
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-carried.ts +26 -0
- package/scripts/backlog/backlog-drive-cli.ts +10 -5
- package/scripts/backlog/backlog-drive-named.ts +3 -5
- package/scripts/backlog/backlog-drive-restore.ts +28 -2
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +59 -15
- package/scripts/behavior/behavior-rules.ts +3 -2
- package/scripts/cost-runtime/codex-usage.ts +4 -2
- package/scripts/cost-runtime/cost-cli.ts +1 -2
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +1 -2
- package/scripts/discovery/repo-discovery.ts +4 -2
- package/scripts/discovery/repo-origin.ts +2 -4
- package/scripts/doctor/doctor-logic.ts +6 -3
- package/scripts/document/document-byte-budget.ts +28 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +3 -3
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +65 -127
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +19 -3
- package/scripts/git/git-common-directory.ts +3 -3
- package/scripts/git/git-diff-reads.ts +16 -9
- package/scripts/git/git-execa-error.ts +1 -2
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +48 -79
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +22 -16
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +49 -5
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +5 -5
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +52 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +4 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +17 -10
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +68 -125
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/overrides/lockfile-overrides.ts +1 -3
- package/scripts/propagate/propagate.ts +1 -2
- package/scripts/release/release-progress.ts +3 -2
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/report/report-lint.ts +7 -4
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/review/review-record-cli.ts +6 -2
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/destructive-command.ts +6 -9
- package/scripts/rules/direct-filing.ts +3 -7
- package/scripts/rules/early-heartbeat.ts +7 -18
- package/scripts/rules/file-body.ts +6 -13
- package/scripts/rules/filing-cap.ts +4 -6
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +67 -16
- package/scripts/rules/josh-git-bare.ts +3 -6
- package/scripts/rules/lane-background.ts +10 -21
- package/scripts/rules/lane-carry-conflict.ts +7 -17
- package/scripts/rules/lane-interactive-ask.ts +6 -11
- package/scripts/rules/lane-park.ts +5 -13
- package/scripts/rules/lane-split-park.ts +2 -7
- package/scripts/rules/lane-switch-main.ts +9 -12
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +84 -32
- package/scripts/rules/poll-loop.ts +7 -17
- package/scripts/rules/pre-gate-cut.ts +7 -21
- package/scripts/rules/prior-comment-read.ts +7 -5
- package/scripts/rules/protected-files.ts +5 -9
- package/scripts/rules/raw-field-body.ts +3 -7
- package/scripts/rules/rule-body-guard.ts +4 -11
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +4 -6
- package/scripts/rules/shell-segments.ts +22 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +164 -77
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/rules/test-declared-commit.ts +4 -10
- package/scripts/rules/third-party-write.ts +5 -10
- package/scripts/rules/worktree-guard.ts +5 -12
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +19 -3
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +25 -12
- package/scripts/run/board/run-board-reason.ts +17 -4
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +9 -0
- package/scripts/run/board/run-board-status.ts +89 -23
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +37 -8
- package/scripts/run/carry/run-carry-args.ts +1 -3
- package/scripts/run/carry/run-carry-conversation.ts +7 -2
- package/scripts/run/cut/run-cut-cli.ts +18 -22
- package/scripts/run/cut/run-cut-decline.ts +72 -0
- package/scripts/run/cut/run-cut-ready.ts +74 -0
- package/scripts/run/cut/run-cut-report.ts +29 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +13 -4
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -42
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +22 -9
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/run-stop-notify.ts +1 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +25 -14
- package/scripts/run/ship/run-ship-review.ts +15 -2
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/run/tidy/run-tidy-cli.ts +3 -3
- package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
- package/scripts/safe-chain/project-config.ts +7 -2
- package/scripts/sync/sync-hook-safety.ts +8 -2
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -3
- package/scripts/time-runtime/time-batch-guard.ts +3 -3
- package/scripts/time-runtime/time-hook-transcript.ts +3 -2
- package/scripts/time-runtime/time-shell.ts +39 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/scripts/version/effective-upstream.ts +3 -4
- package/scripts/version/kit-version-config.ts +8 -2
- package/scripts/version/latest-corepack.ts +1 -2
- package/scripts/version/release-hold.ts +11 -4
- package/scripts/version/version-commands.ts +12 -4
- package/templates/gitignore +6 -2
- package/dist/hooks/chunk-D5QBTOCZ.js +0 -68
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -180,6 +180,55 @@ function acceptance_section(body: string | undefined): string {
|
|
|
180
180
|
return markdown_section.section_lines_matching(body, is_acceptance_heading).join('\n')
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Where a body reference that counts as a relation was written. Only these two places state what the
|
|
184
|
+
// issue delivers or where it belongs; a number in the background, symptom or origin section is the
|
|
185
|
+
// example that prompted the filing, and reading it as a relation bundled issues whose work never
|
|
186
|
+
// overlaps.
|
|
187
|
+
type SignalSection = 'acceptance criteria' | 'parent line'
|
|
188
|
+
|
|
189
|
+
interface SignalReference {
|
|
190
|
+
number: number
|
|
191
|
+
section: SignalSection
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// What marks a line as naming the issue's parent, in both spellings for the same reason as
|
|
195
|
+
// `ACCEPTANCE_HEADINGS`. `epic:audit`'s orphan search matches it anywhere in the line, then asks
|
|
196
|
+
// whether the same line names the epic.
|
|
197
|
+
const PARENT_MARKERS: ReadonlyArray<string> = ['親:', '親:', 'Parent:', 'parent:']
|
|
198
|
+
|
|
199
|
+
function has_parent_marker(line: string): boolean {
|
|
200
|
+
return PARENT_MARKERS.some((marker) => line.includes(marker))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The reference written directly after a parent marker, bold markup allowed (`- **Parent:** #N`).
|
|
204
|
+
// Only that reference is the parent: a background sentence that mentions `parent:` and names an
|
|
205
|
+
// issue elsewhere on the line is not a parent line.
|
|
206
|
+
const PARENT_REFERENCE = /(?:親|Parent|parent)\**[::]\**\s*(?:[\w.-]+\/[\w.-]+)?#\d+/gu
|
|
207
|
+
|
|
208
|
+
function parent_references(body: string): string {
|
|
209
|
+
return markdown_section
|
|
210
|
+
.unfenced_lines(body)
|
|
211
|
+
.flatMap((line) => [...line.matchAll(PARENT_REFERENCE)].map((match) => match[0]))
|
|
212
|
+
.join('\n')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function references_in(text: string, repo: string, section: SignalSection): Array<SignalReference> {
|
|
216
|
+
return parse_references(text, repo).map((number) => ({ number, section }))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// The same-repository references a body makes in the places that count as a relation, each with the
|
|
220
|
+
// section it was read from. A number both places name is reported once, at the first.
|
|
221
|
+
function signal_references(body: string, repo = ''): Array<SignalReference> {
|
|
222
|
+
const read = [
|
|
223
|
+
...references_in(acceptance_section(body), repo, 'acceptance criteria'),
|
|
224
|
+
...references_in(parent_references(body), repo, 'parent line'),
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
return read.filter((reference, index) => {
|
|
228
|
+
return read.findIndex((other) => other.number === reference.number) === index
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
183
232
|
// Everything `node` depends on, directly or through a chain. Walked iteratively so a graph that
|
|
184
233
|
// still contains a cycle terminates — `epic:next` rejects one first, but an auditor must not hang on
|
|
185
234
|
// the input it exists to examine.
|
|
@@ -223,8 +272,10 @@ const epic_audit_logic = {
|
|
|
223
272
|
parse_issue_references,
|
|
224
273
|
parse_references,
|
|
225
274
|
acceptance_section,
|
|
275
|
+
signal_references,
|
|
276
|
+
has_parent_marker,
|
|
226
277
|
depends_on,
|
|
227
278
|
}
|
|
228
279
|
|
|
229
|
-
export type { AuditFinding, FindingLevel, ReferenceState }
|
|
280
|
+
export type { AuditFinding, FindingLevel, ReferenceState, SignalReference, SignalSection }
|
|
230
281
|
export { epic_audit_logic }
|
|
@@ -323,12 +323,11 @@ async function widen_with_referenced(
|
|
|
323
323
|
backlog: FetchedBacklog,
|
|
324
324
|
): Promise<FetchedBacklog> {
|
|
325
325
|
const { context } = backlog
|
|
326
|
-
if (context === undefined) return backlog
|
|
327
326
|
// An issue an epic already tracks has nothing to bundle — `decide_bundle` answers from that alone
|
|
328
327
|
// and never looks at the candidates. Widening first spends a request per reference on a verdict
|
|
329
328
|
// that ignores them, and prints "⚠ Could not read #891." above "Already in an epic", which reads
|
|
330
329
|
// as a gap in an answer the read was never part of.
|
|
331
|
-
if (subject.epic !== undefined) return backlog
|
|
330
|
+
if (context === undefined || subject.epic !== undefined) return backlog
|
|
332
331
|
const known = new Set(backlog.issues.map((issue) => issue.number))
|
|
333
332
|
const found = await epic_bundle_referenced.referenced_candidates(subject, known, context)
|
|
334
333
|
if (found === undefined) return backlog
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
import type { SignalSection } from './epic-audit'
|
|
2
3
|
import { epic_bundle, type BacklogIssue } from './epic-bundle'
|
|
3
4
|
|
|
4
5
|
// The facts the bundle decision was already made from, printed beside the order it prints.
|
|
@@ -20,10 +21,26 @@ import { epic_bundle, type BacklogIssue } from './epic-bundle'
|
|
|
20
21
|
type EvidenceKind = 'dependency' | 'reference'
|
|
21
22
|
|
|
22
23
|
// One fact, and the order it points to. `first` is the issue the fact puts ahead of `second`.
|
|
24
|
+
// `section` is where a reference was read, so the reader sees it was the acceptance criteria or the
|
|
25
|
+
// parent line that counted, not a mention elsewhere in the body.
|
|
23
26
|
interface OrderingEvidence {
|
|
24
27
|
kind: EvidenceKind
|
|
25
28
|
first: number
|
|
26
29
|
second: number
|
|
30
|
+
section?: SignalSection
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// What one directed test found: nothing, or the fact with whatever detail it carries.
|
|
34
|
+
type EvidenceFound = Pick<OrderingEvidence, 'section'> | undefined
|
|
35
|
+
|
|
36
|
+
function blocker_found(from: BacklogIssue, to: BacklogIssue): EvidenceFound {
|
|
37
|
+
return epic_bundle.names_as_blocker(from, to) ? {} : undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function reference_found(from: BacklogIssue, to: BacklogIssue): EvidenceFound {
|
|
41
|
+
const section = epic_bundle.reference_section(from, to)
|
|
42
|
+
|
|
43
|
+
return section === undefined ? undefined : { section }
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
// The two readings, each written as a directed test of one issue against another. Both are
|
|
@@ -34,14 +51,28 @@ interface OrderingEvidence {
|
|
|
34
51
|
// `blocked-by` says so outright: the blocker has to land before the issue that declares it. A
|
|
35
52
|
// reference says it by having been written — the issue that names another was drafted with that
|
|
36
53
|
// other one already in existence, so the named one is the earlier of the two.
|
|
37
|
-
|
|
54
|
+
interface EvidenceTest {
|
|
38
55
|
kind: EvidenceKind
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
find: (from: BacklogIssue, to: BacklogIssue) => EvidenceFound
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const EVIDENCE_TESTS: ReadonlyArray<EvidenceTest> = [
|
|
60
|
+
{ kind: 'dependency', find: blocker_found },
|
|
61
|
+
{ kind: 'reference', find: reference_found },
|
|
43
62
|
]
|
|
44
63
|
|
|
64
|
+
// One test in one direction: the fact it found, or none.
|
|
65
|
+
function directed_evidence(
|
|
66
|
+
test: EvidenceTest,
|
|
67
|
+
from: BacklogIssue,
|
|
68
|
+
to: BacklogIssue,
|
|
69
|
+
): Array<OrderingEvidence> {
|
|
70
|
+
const found = test.find(from, to)
|
|
71
|
+
if (found === undefined) return []
|
|
72
|
+
|
|
73
|
+
return [{ kind: test.kind, first: to.number, second: from.number, ...found }]
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
// Both directions of one pair, because both are facts. Two issues that name each other produce two
|
|
46
77
|
// lines pointing opposite ways, which is the honest report: the reader is the one deciding.
|
|
47
78
|
function pair_evidence(subject: BacklogIssue, other: BacklogIssue): Array<OrderingEvidence> {
|
|
@@ -50,10 +81,8 @@ function pair_evidence(subject: BacklogIssue, other: BacklogIssue): Array<Orderi
|
|
|
50
81
|
{ from: other, to: subject },
|
|
51
82
|
]
|
|
52
83
|
|
|
53
|
-
return EVIDENCE_TESTS.flatMap((
|
|
54
|
-
directions
|
|
55
|
-
.filter((pair) => holds(pair.from, pair.to))
|
|
56
|
-
.map((pair) => ({ kind, first: pair.to.number, second: pair.from.number })),
|
|
84
|
+
return EVIDENCE_TESTS.flatMap((test) =>
|
|
85
|
+
directions.flatMap((pair) => directed_evidence(test, pair.from, pair.to)),
|
|
57
86
|
)
|
|
58
87
|
}
|
|
59
88
|
|
|
@@ -85,8 +114,8 @@ function ordering_evidence(
|
|
|
85
114
|
return member_pairs(members).flatMap((pair) => pair_evidence(pair.left, pair.right))
|
|
86
115
|
}
|
|
87
116
|
|
|
88
|
-
// How each kind reads. `
|
|
89
|
-
//
|
|
117
|
+
// How each kind reads. The two `order_*` halves say how strongly the fact settles the order; where a
|
|
118
|
+
// reference was read comes from the fact itself (`section_phrase`).
|
|
90
119
|
//
|
|
91
120
|
// **A recorded dependency states the order; a reference only points at it.** A `blocked-by` says
|
|
92
121
|
// outright that the blocker lands first. A reference says it by having been written — which holds
|
|
@@ -95,20 +124,14 @@ function ordering_evidence(
|
|
|
95
124
|
// the reference line reports the direction it points to and asserts nothing further; deciding is
|
|
96
125
|
// still the reader's.
|
|
97
126
|
const EVIDENCE_WORDING: Readonly<
|
|
98
|
-
Record<EvidenceKind, { verb: string;
|
|
127
|
+
Record<EvidenceKind, { verb: string; order_prefix: string; order_suffix: string }>
|
|
99
128
|
> = {
|
|
100
|
-
dependency: {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
reference: {
|
|
107
|
-
verb: 'names',
|
|
108
|
-
where: ' in its body',
|
|
109
|
-
order_prefix: 'that points to ',
|
|
110
|
-
order_suffix: ' first',
|
|
111
|
-
},
|
|
129
|
+
dependency: { verb: 'is recorded as blocked by', order_prefix: '', order_suffix: ' comes first' },
|
|
130
|
+
reference: { verb: 'names', order_prefix: 'that points to ', order_suffix: ' first' },
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function section_phrase(item: OrderingEvidence): string {
|
|
134
|
+
return item.section === undefined ? '' : ` in its ${item.section}`
|
|
112
135
|
}
|
|
113
136
|
|
|
114
137
|
const EVIDENCE_HEADING = ' Evidence:'
|
|
@@ -117,12 +140,12 @@ const EVIDENCE_INDENT_WIDTH = 4
|
|
|
117
140
|
const EVIDENCE_INDENT = ' '.repeat(EVIDENCE_INDENT_WIDTH)
|
|
118
141
|
|
|
119
142
|
function format_one(item: OrderingEvidence): string {
|
|
120
|
-
const { verb,
|
|
143
|
+
const { verb, order_prefix, order_suffix } = EVIDENCE_WORDING[item.kind]
|
|
121
144
|
const first = issue_cite.plain(item.first)
|
|
122
145
|
const second = issue_cite.plain(item.second)
|
|
123
146
|
const order = `${order_prefix}${first}${order_suffix}`
|
|
124
147
|
|
|
125
|
-
return `${EVIDENCE_INDENT}${second} ${verb} ${first}${
|
|
148
|
+
return `${EVIDENCE_INDENT}${second} ${verb} ${first}${section_phrase(item)} — ${order}`
|
|
126
149
|
}
|
|
127
150
|
|
|
128
151
|
// The block that goes under `Order:`. Empty when neither reading found anything, which leaves the
|
|
@@ -44,10 +44,21 @@ interface ReferencedLookups {
|
|
|
44
44
|
dropped: Array<number>
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Every number the body names, the ones a bundle signal counts first: the cap below must not leave a
|
|
48
|
+
// signal unread because twenty background mentions came ahead of it in the body.
|
|
49
|
+
function signal_first(subject: BacklogIssue): Array<number> {
|
|
50
|
+
const signals = epic_audit_logic
|
|
51
|
+
.signal_references(subject.body, subject.repo)
|
|
52
|
+
.map((reference) => reference.number)
|
|
53
|
+
const all = epic_audit_logic.parse_references(subject.body, subject.repo)
|
|
54
|
+
|
|
55
|
+
return [...new Set([...signals, ...all])]
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
function referenced_lookups(subject: BacklogIssue, known: ReadonlySet<number>): ReferencedLookups {
|
|
48
|
-
const candidates =
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
const candidates = signal_first(subject).filter(
|
|
60
|
+
(number) => number !== subject.number && !known.has(number),
|
|
61
|
+
)
|
|
51
62
|
|
|
52
63
|
return {
|
|
53
64
|
numbers: candidates.slice(0, REFERENCED_LOOKUP_LIMIT),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
2
|
import { has_label_name, RELEASE_LABEL } from '#scripts/issue/issue-labels'
|
|
3
|
-
import { epic_audit_logic } from './epic-audit'
|
|
3
|
+
import { epic_audit_logic, type SignalSection } from './epic-audit'
|
|
4
4
|
import { epic_graph } from './epic-graph'
|
|
5
5
|
import type { DependencyLink } from './epic-parse'
|
|
6
6
|
import type { IssueReference } from './epic-reference'
|
|
@@ -11,9 +11,9 @@ import type { IssueReference } from './epic-reference'
|
|
|
11
11
|
// not reach the other way in: two issues filed days apart that turn out to be the front and back of
|
|
12
12
|
// one job are executed separately, in whatever order, with the reasoning recorded nowhere.
|
|
13
13
|
//
|
|
14
|
-
// The candidate search reads issue references out of
|
|
15
|
-
// inside one epic. That analysis is imported rather than
|
|
16
|
-
// difference is what it is pointed at.
|
|
14
|
+
// The candidate search reads issue references out of the acceptance criteria and the `parent: #N`
|
|
15
|
+
// line — the same reading `epic:audit` does inside one epic. That analysis is imported rather than
|
|
16
|
+
// written a second time; the only difference is what it is pointed at.
|
|
17
17
|
|
|
18
18
|
// An open issue as the search sees it.
|
|
19
19
|
interface BacklogIssue {
|
|
@@ -59,11 +59,19 @@ interface BundleDecision {
|
|
|
59
59
|
reason: string
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
62
|
+
// Where `from`'s body names `to` in a place that counts as a relation — its acceptance criteria or
|
|
63
|
+
// its `parent: #N` line — or nothing. One direction of the citation test, because which direction
|
|
64
|
+
// and which section it was is what `epic-bundle-evidence.ts` reports — and the read is single-sourced
|
|
65
|
+
// here rather than written a second time there, so the candidate search and the evidence beside it
|
|
66
|
+
// cannot drift apart. A number written only as the background of a filing is not a relation.
|
|
67
|
+
function reference_section(from: BacklogIssue, to: BacklogIssue): SignalSection | undefined {
|
|
68
|
+
return epic_audit_logic
|
|
69
|
+
.signal_references(from.body, from.repo)
|
|
70
|
+
.find((reference) => reference.number === to.number)?.section
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
function names_in_body(from: BacklogIssue, to: BacklogIssue): boolean {
|
|
66
|
-
return
|
|
74
|
+
return reference_section(from, to) !== undefined
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
// Whether two issues cite each other, in either direction. A one-way citation is enough: the point
|
|
@@ -324,7 +332,7 @@ function bundle_children(
|
|
|
324
332
|
|
|
325
333
|
const epic_bundle = {
|
|
326
334
|
names_as_blocker,
|
|
327
|
-
|
|
335
|
+
reference_section,
|
|
328
336
|
NO_SIGNAL_REASON,
|
|
329
337
|
already_tracked_reason,
|
|
330
338
|
has_recorded_dependency,
|
|
@@ -190,8 +190,12 @@ function find_position_ambiguity(
|
|
|
190
190
|
chains: Chains,
|
|
191
191
|
position: InsertPosition | undefined,
|
|
192
192
|
): string | undefined {
|
|
193
|
-
if (
|
|
194
|
-
|
|
193
|
+
if (
|
|
194
|
+
position?.kind !== 'before' ||
|
|
195
|
+
chains_containing(chains, position.target).length < AMBIGUOUS_MATCH_COUNT
|
|
196
|
+
) {
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
195
199
|
|
|
196
200
|
return to_ambiguous_position_error(position)
|
|
197
201
|
}
|
|
@@ -66,8 +66,7 @@ function marker_children(line: string): Array<string> | undefined {
|
|
|
66
66
|
if (indent_of(line) >= INDENTED_CODE_SPACES) return undefined
|
|
67
67
|
|
|
68
68
|
const trimmed = line.trim()
|
|
69
|
-
if (!trimmed.startsWith(MARKER_OPEN)) return undefined
|
|
70
|
-
if (!trimmed.endsWith(MARKER_CLOSE)) return undefined
|
|
69
|
+
if (!trimmed.startsWith(MARKER_OPEN) || !trimmed.endsWith(MARKER_CLOSE)) return undefined
|
|
71
70
|
|
|
72
71
|
const listed = trimmed.slice(MARKER_OPEN.length, -MARKER_CLOSE.length)
|
|
73
72
|
|
|
@@ -7,6 +7,7 @@ import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
|
|
|
7
7
|
import { cli_body } from '#scripts/josh/cli-body'
|
|
8
8
|
import { josh_environment_file } from '#scripts/josh/josh-environment-file'
|
|
9
9
|
import { git_notify, type GitNotifyConfig } from '#scripts/notify/git-notify'
|
|
10
|
+
import { acceptance_map_check } from '#scripts/review/acceptance-map-check'
|
|
10
11
|
import { live_evidence } from '#scripts/review/live-evidence'
|
|
11
12
|
import { review_attest } from '#scripts/review/review-attest'
|
|
12
13
|
import { review_record } from '#scripts/review/review-record'
|
|
@@ -143,7 +144,7 @@ function is_merge_permitted(status: string): boolean {
|
|
|
143
144
|
return status === 'ok' || status === 'not-required'
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
function is_valid_issue(issue_number: string | undefined):
|
|
147
|
+
function is_valid_issue(issue_number: string | undefined): issue_number is string {
|
|
147
148
|
return issue_number !== undefined && issue_number_shape.ISSUE_NUMBER_PATTERN.test(issue_number)
|
|
148
149
|
}
|
|
149
150
|
|
|
@@ -194,6 +195,21 @@ async function assert_live_evidence(should_merge: boolean, branch_name: string):
|
|
|
194
195
|
throw new Error(live_evidence.refusal_message())
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
// Every acceptance criterion the Issue carries at the merge maps to a row in the pull request body —
|
|
199
|
+
// `acceptance_map` carries the why. Re-read here rather than at the commit, so a criterion added
|
|
200
|
+
// during the review is refused too. A run whose issue it cannot identify has no criteria to read.
|
|
201
|
+
async function assert_acceptance_mapped(
|
|
202
|
+
should_merge: boolean,
|
|
203
|
+
issue_number: string | undefined,
|
|
204
|
+
branch_name: string,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
if (!should_merge || !is_valid_issue(issue_number)) return
|
|
207
|
+
|
|
208
|
+
const refusal = await acceptance_map_check.refusal(issue_number, branch_name)
|
|
209
|
+
|
|
210
|
+
if (refusal !== undefined) throw new Error(refusal)
|
|
211
|
+
}
|
|
212
|
+
|
|
197
213
|
// Every pre-merge refusal, in order — each one throws, so nothing merges past the first that fails.
|
|
198
214
|
async function assert_merge_gates(
|
|
199
215
|
should_merge: boolean,
|
|
@@ -203,6 +219,7 @@ async function assert_merge_gates(
|
|
|
203
219
|
await assert_review_attested(should_merge)
|
|
204
220
|
await assert_review_recorded(should_merge, issue_number)
|
|
205
221
|
await assert_live_evidence(should_merge, branch_name)
|
|
222
|
+
await assert_acceptance_mapped(should_merge, issue_number, branch_name)
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
// After the gates, before the CI wait: a line appended since the run's commit rides the pull request
|
|
@@ -261,6 +278,7 @@ try {
|
|
|
261
278
|
}
|
|
262
279
|
|
|
263
280
|
const git_followup_workflow = {
|
|
281
|
+
assert_acceptance_mapped,
|
|
264
282
|
assert_live_evidence,
|
|
265
283
|
assert_review_attested,
|
|
266
284
|
assert_review_recorded,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { lint_related_scope } from '#scripts/lint/lint-related-scope'
|
|
3
|
+
import { gate_plan } from './gate-plan'
|
|
4
|
+
|
|
5
|
+
// Which paths each gate check reads, so a gate re-run reuses a check none of whose inputs moved
|
|
6
|
+
// (`check-reuse.ts`). **A check with no scope here always runs**: `cspell` reads every
|
|
7
|
+
// text file, the unit suite reads documents and fixtures as well as code, `behavior` reads a transcript
|
|
8
|
+
// outside the tree and `metrics` reads whatever it measures. Naming a scope for any of them would be a
|
|
9
|
+
// guess, and a wrong guess is a check reported green without having run.
|
|
10
|
+
//
|
|
11
|
+
// **The scopes over-approximate, never under**: a path in doubt is an input. `check` and `exports`
|
|
12
|
+
// read source modules and the JSON they import. `lint` is prettier plus eslint, so its inputs are those
|
|
13
|
+
// sources plus every file prettier formats — the extensions `lint:related` already selects, less what
|
|
14
|
+
// prettier's own ignore files leave out (`.claude/**/*.md`). Prettier answers that itself through
|
|
15
|
+
// `getFileInfo`, so the two ignore files stay the single source.
|
|
16
|
+
//
|
|
17
|
+
// **A configuration file is an input of every scoped check**: a manifest or lock file moves the
|
|
18
|
+
// installed toolchain, and an ignore or editor file moves what prettier reads.
|
|
19
|
+
|
|
20
|
+
const SOURCE_EXTENSIONS: ReadonlySet<string> = new Set([
|
|
21
|
+
'.ts',
|
|
22
|
+
'.tsx',
|
|
23
|
+
'.mts',
|
|
24
|
+
'.cts',
|
|
25
|
+
'.js',
|
|
26
|
+
'.jsx',
|
|
27
|
+
'.mjs',
|
|
28
|
+
'.cjs',
|
|
29
|
+
'.svelte',
|
|
30
|
+
'.vue',
|
|
31
|
+
'.json',
|
|
32
|
+
'.jsonc',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
const GIT_IGNORE = '.gitignore'
|
|
36
|
+
const PRETTIER_IGNORE = '.prettierignore'
|
|
37
|
+
const PRETTIER_IGNORE_FILES = [GIT_IGNORE, PRETTIER_IGNORE] as const
|
|
38
|
+
|
|
39
|
+
const CONFIG_FILES: ReadonlySet<string> = new Set([
|
|
40
|
+
'package.json',
|
|
41
|
+
'pnpm-lock.yaml',
|
|
42
|
+
'pnpm-workspace.yaml',
|
|
43
|
+
'package-lock.json',
|
|
44
|
+
'yarn.lock',
|
|
45
|
+
'bun.lock',
|
|
46
|
+
'bun.lockb',
|
|
47
|
+
'.npmrc',
|
|
48
|
+
'.editorconfig',
|
|
49
|
+
...PRETTIER_IGNORE_FILES,
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
const PRETTIER_CONFIG_PREFIX = '.prettierrc'
|
|
53
|
+
|
|
54
|
+
// Whether prettier would leave `relative` out; `false` when prettier cannot be asked, which keeps the
|
|
55
|
+
// path an input.
|
|
56
|
+
type PrettierIgnored = (relative: string) => Promise<boolean>
|
|
57
|
+
|
|
58
|
+
type InputTest = (relative: string, is_ignored: PrettierIgnored) => Promise<boolean>
|
|
59
|
+
|
|
60
|
+
function is_config(relative: string): boolean {
|
|
61
|
+
const name = path.basename(relative)
|
|
62
|
+
|
|
63
|
+
return CONFIG_FILES.has(name) || name.startsWith(PRETTIER_CONFIG_PREFIX)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function is_source(relative: string): boolean {
|
|
67
|
+
return SOURCE_EXTENSIONS.has(path.extname(relative))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function is_lint_input(relative: string, is_ignored: PrettierIgnored): Promise<boolean> {
|
|
71
|
+
if (is_source(relative)) return true
|
|
72
|
+
if (!lint_related_scope.is_lintable(relative)) return false
|
|
73
|
+
|
|
74
|
+
return !(await is_ignored(relative))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function is_source_input(relative: string): Promise<boolean> {
|
|
78
|
+
return is_source(relative)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function prettier_ignored(root: string): PrettierIgnored {
|
|
82
|
+
const ignore_path = PRETTIER_IGNORE_FILES.map((name) => path.join(root, name))
|
|
83
|
+
|
|
84
|
+
return async function is_ignored(relative: string): Promise<boolean> {
|
|
85
|
+
try {
|
|
86
|
+
const prettier = await import('prettier')
|
|
87
|
+
const info = await prettier.getFileInfo(path.join(root, relative), {
|
|
88
|
+
ignorePath: ignore_path,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return info.ignored
|
|
92
|
+
} catch {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const SCOPES: ReadonlyMap<string, InputTest> = new Map([
|
|
99
|
+
[gate_plan.LINT_LABEL, is_lint_input],
|
|
100
|
+
[gate_plan.TYPE_CHECK_LABEL, is_source_input],
|
|
101
|
+
[gate_plan.EXPORTS_LABEL, is_source_input],
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
function is_scoped(label: string): boolean {
|
|
105
|
+
return SCOPES.has(label)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Whether any of `paths` (repository-root relative) is an input of the check `label`. An unscoped
|
|
109
|
+
// check answers `true` for every set of paths, so the only way to reuse it is never.
|
|
110
|
+
async function touches_inputs(
|
|
111
|
+
label: string,
|
|
112
|
+
paths: ReadonlyArray<string>,
|
|
113
|
+
is_ignored: PrettierIgnored,
|
|
114
|
+
): Promise<boolean> {
|
|
115
|
+
const scope = SCOPES.get(label)
|
|
116
|
+
|
|
117
|
+
if (scope === undefined || paths.some((relative) => is_config(relative))) return true
|
|
118
|
+
|
|
119
|
+
const answers = await Promise.all(
|
|
120
|
+
paths.map(async (relative) => await scope(relative, is_ignored)),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return answers.includes(true)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const check_inputs = { is_scoped, prettier_ignored, touches_inputs }
|
|
127
|
+
|
|
128
|
+
export type { PrettierIgnored }
|
|
129
|
+
export { check_inputs }
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { git_command } from '#scripts/git/git-command'
|
|
2
|
+
import { git_spawn } from '#scripts/git/git-spawn'
|
|
3
|
+
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
4
|
+
import { file_map_stamp, type FileMapStamp } from '#scripts/josh/file-map-stamp'
|
|
5
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
6
|
+
import { review_tree } from '#scripts/review/review-tree'
|
|
7
|
+
import { check_inputs, type PrettierIgnored } from './check-inputs'
|
|
8
|
+
import { gate_report, type GateStepResult } from './gate-report'
|
|
9
|
+
import type { GateTree } from './gate-tree'
|
|
10
|
+
|
|
11
|
+
// One green record per scoped check, so a gate re-run starts only the checks whose inputs moved
|
|
12
|
+
// (`check-inputs.ts` decides what each reads). The whole-gate record (`gate-skip.ts`) is refused the
|
|
13
|
+
// moment the merge base moves; a main sync that brought in only `.claude/**/*.md` then re-ran lint and
|
|
14
|
+
// the type check over sources neither had a reason to read again.
|
|
15
|
+
//
|
|
16
|
+
// **What moved is the union of two diffs**, and together they are every path whose content can differ
|
|
17
|
+
// from the tree the record was taken on. The changed-file map is a diff against the merge base, so a
|
|
18
|
+
// path absent from both maps holds the merge base's content — and the base's own diff
|
|
19
|
+
// (`git diff <recorded> <current>`) names every path that content moved for. A path in either map whose
|
|
20
|
+
// digest differs is the other half (`file_map_stamp.changed_since`).
|
|
21
|
+
//
|
|
22
|
+
// **A record is reuse of a result, not a check dropped**: no base, an empty changed-file map, an
|
|
23
|
+
// unreadable base diff or no root answers "not reusable", and a check with no scope is never reusable. The records are written and read
|
|
24
|
+
// only where the CLI entry resolved their destination (`CheckRecords`), so a suite driving the gate
|
|
25
|
+
// never plants a green record over the live tree.
|
|
26
|
+
|
|
27
|
+
const CHECK_PREFIX = 'josh-gate-check-'
|
|
28
|
+
const NAME_SEPARATOR = '\0'
|
|
29
|
+
const BASE_DIFF = ['diff', '--name-only', '--no-renames', '-z'] as const
|
|
30
|
+
|
|
31
|
+
const check_stamp = file_map_stamp.create(CHECK_PREFIX, PROJECT_ROOT)
|
|
32
|
+
|
|
33
|
+
// Where each check's record lives — a parameter so a test plants records in a directory of its own.
|
|
34
|
+
type CheckRecords = (label: string) => string
|
|
35
|
+
|
|
36
|
+
function default_records(label: string): string {
|
|
37
|
+
return stamp_file.stamp_path(`${CHECK_PREFIX}${label}-`, PROJECT_ROOT)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function base_diff(from: string, to: string): Promise<ReadonlyArray<string> | undefined> {
|
|
41
|
+
if (from === to) return []
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const names = await git_spawn.read([...BASE_DIFF, from, to])
|
|
45
|
+
|
|
46
|
+
return names.split(NAME_SEPARATOR).filter((name) => name !== '')
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function moved_since(
|
|
53
|
+
stamp: FileMapStamp,
|
|
54
|
+
tree: GateTree,
|
|
55
|
+
): Promise<ReadonlyArray<string> | undefined> {
|
|
56
|
+
// An empty map is also what `gate-tree.ts` answers when git could not be read, so it describes no
|
|
57
|
+
// tree to diff against — the reading `gate_skip.gate_reuse` refuses too.
|
|
58
|
+
const is_unread = Object.keys(tree.files).length === 0
|
|
59
|
+
|
|
60
|
+
if (is_unread || stamp.base === undefined || tree.base === undefined) return undefined
|
|
61
|
+
|
|
62
|
+
const moved_base = await base_diff(stamp.base, tree.base)
|
|
63
|
+
|
|
64
|
+
if (moved_base === undefined) return undefined
|
|
65
|
+
|
|
66
|
+
return [...file_map_stamp.changed_since(stamp, tree.files), ...moved_base]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ReuseContext {
|
|
70
|
+
tree: GateTree
|
|
71
|
+
records: CheckRecords
|
|
72
|
+
is_ignored: PrettierIgnored
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function is_stamp_reusable(
|
|
76
|
+
label: string,
|
|
77
|
+
stamp: FileMapStamp,
|
|
78
|
+
context: ReuseContext,
|
|
79
|
+
): Promise<boolean> {
|
|
80
|
+
const moved = await moved_since(stamp, context.tree)
|
|
81
|
+
|
|
82
|
+
if (moved === undefined) return false
|
|
83
|
+
|
|
84
|
+
return !(await check_inputs.touches_inputs(label, moved, context.is_ignored))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The time the check's record was taken, or `undefined` when the check has to run.
|
|
88
|
+
async function reused_at(label: string, context: ReuseContext): Promise<string | undefined> {
|
|
89
|
+
const stamp = check_inputs.is_scoped(label) ? check_stamp.read(context.records(label)) : undefined
|
|
90
|
+
|
|
91
|
+
if (stamp === undefined) return undefined
|
|
92
|
+
|
|
93
|
+
return (await is_stamp_reusable(label, stamp, context)) ? stamp.taken_at : undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function read_root(): Promise<string | undefined> {
|
|
97
|
+
try {
|
|
98
|
+
return await git_command.repository_root()
|
|
99
|
+
} catch {
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Each reusable check's label, mapped to when it was last green.
|
|
105
|
+
async function reusable_checks(
|
|
106
|
+
labels: ReadonlyArray<string>,
|
|
107
|
+
tree: GateTree,
|
|
108
|
+
records: CheckRecords = default_records,
|
|
109
|
+
): Promise<ReadonlyMap<string, string>> {
|
|
110
|
+
const root = await read_root()
|
|
111
|
+
|
|
112
|
+
if (root === undefined) return new Map()
|
|
113
|
+
|
|
114
|
+
const context = { tree, records, is_ignored: check_inputs.prettier_ignored(root) }
|
|
115
|
+
const answers = await Promise.all(
|
|
116
|
+
labels.map(async (label) => [label, await reused_at(label, context)] as const),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return new Map(answers.flatMap(([label, at]) => (at === undefined ? [] : [[label, at] as const])))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function is_recordable(result: GateStepResult): boolean {
|
|
123
|
+
return (
|
|
124
|
+
check_inputs.is_scoped(result.label) &&
|
|
125
|
+
!gate_report.is_gate_step_failed(result) &&
|
|
126
|
+
gate_report.has_nothing_to_say(result)
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function write_records(
|
|
131
|
+
results: ReadonlyArray<GateStepResult>,
|
|
132
|
+
tree: GateTree,
|
|
133
|
+
records: CheckRecords,
|
|
134
|
+
): void {
|
|
135
|
+
const recordable = results.filter((result) => is_recordable(result))
|
|
136
|
+
|
|
137
|
+
for (const result of recordable) check_stamp.write(tree.files, records(result.label), tree.base)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Every scoped check that passed silently, recorded against the tree it read — a red gate's green
|
|
141
|
+
// checks included, since each record answers for its own check alone. Nothing is written when the tree
|
|
142
|
+
// moved while the checks ran, and a failed write leaves no record, the safe direction either way.
|
|
143
|
+
async function record_green_checks(
|
|
144
|
+
results: ReadonlyArray<GateStepResult>,
|
|
145
|
+
tree: GateTree,
|
|
146
|
+
records: CheckRecords = default_records,
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
try {
|
|
149
|
+
const after = await review_tree.read_changed_tree()
|
|
150
|
+
|
|
151
|
+
if (JSON.stringify(after) !== JSON.stringify(tree.files)) return
|
|
152
|
+
|
|
153
|
+
write_records(results, { files: after, base: tree.base }, records)
|
|
154
|
+
} catch {
|
|
155
|
+
/* no record is the safe answer */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const check_reuse = { DEFAULT_RECORDS: default_records, record_green_checks, reusable_checks }
|
|
160
|
+
|
|
161
|
+
export type { CheckRecords }
|
|
162
|
+
export { check_reuse }
|