@joshuafolkken/kit 1.1387.0 → 1.1453.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/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
- package/.claude/skills/workflow-commands/backlogrun.md +3 -2
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/kickoff.md +12 -13
- package/.claude/skills/workflow-commands/observation-filing.md +1 -1
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/.claude/skills/workflow-commands/prerequisite.md +6 -0
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/README.md +27 -4
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +261 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +95 -19
- package/package.json +4 -3
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/review.md +30 -4
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
- package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
- package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
- package/scripts/auto-ok/auto-ok-judge.ts +67 -0
- package/scripts/backlog/backlog-drive-cli.ts +14 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +38 -11
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +18 -4
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +13 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +7 -4
- package/scripts/init/init-ai-copy.ts +34 -11
- package/scripts/init/init-basic.ts +9 -4
- package/scripts/init/init-bootstrap.ts +6 -2
- package/scripts/init/init-dry-run-report.ts +150 -0
- package/scripts/init/init-dry-run.ts +189 -0
- package/scripts/init/init-install.ts +8 -1
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +62 -42
- package/scripts/init/kit-setup-state.ts +11 -17
- package/scripts/init/own-formatter.ts +21 -0
- package/scripts/init/project-profile.ts +60 -12
- package/scripts/init/start-dry-run.ts +70 -0
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +23 -3
- package/scripts/init/start-setup-pr.ts +13 -24
- package/scripts/init/start-steps.ts +62 -1
- package/scripts/init/start.ts +38 -17
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/init/working-tree.ts +116 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-auto-ok.ts +68 -99
- package/scripts/issue/issue-file-cli.ts +17 -17
- package/scripts/issue/issue-file.ts +1 -6
- package/scripts/issue/issue-labels.ts +46 -10
- package/scripts/issue/issue-release-cli.ts +69 -23
- package/scripts/issue/issue-release.ts +3 -1
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-ai.ts +11 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +10 -3
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +26 -2
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/josh/process-identity.ts +32 -6
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-broker-server.ts +69 -0
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/lane-session-broker.ts +363 -0
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/observations/observation-ledger-migrate.ts +3 -15
- package/scripts/release/release-issue-close.ts +133 -0
- package/scripts/release/release-publish.ts +12 -1
- package/scripts/repo/repository-labels.ts +11 -1
- package/scripts/review/comment-ranges.ts +199 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +24 -7
- package/scripts/review/review-code-tree.ts +119 -0
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/review/review-round2-cli.ts +16 -5
- package/scripts/review/review-round2.ts +30 -10
- package/scripts/review/review-stamps.ts +22 -1
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/lane/openai-review-broker.ts +0 -262
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
// The text of a source file with its comments taken out, so two versions that differ only in comments
|
|
4
|
+
// read as the same text.
|
|
5
|
+
//
|
|
6
|
+
// **Every range comes from the TypeScript parser, never from a pattern.** A `//` inside a regular
|
|
7
|
+
// expression or a template literal is code, and a hand-written scanner that took it for a comment
|
|
8
|
+
// would strip a real edit and call the fix comment-only — the one wrong answer that buys a `skip`.
|
|
9
|
+
// Driven by the parse tree, a range is only ever read from the trivia before a token.
|
|
10
|
+
//
|
|
11
|
+
// **What it cannot read, it does not strip**: a file that does not parse answers `undefined`, and a
|
|
12
|
+
// comment the walk does not reach stays in the text. Both resolve to "the code changed", which costs a
|
|
13
|
+
// round rather than skipping one.
|
|
14
|
+
|
|
15
|
+
type TypeScriptApi = typeof ts
|
|
16
|
+
type Range = readonly [number, number]
|
|
17
|
+
|
|
18
|
+
// A comment's own opening — the shebang is trivia to the parser too, but it decides how the file is
|
|
19
|
+
// executed, so it stays.
|
|
20
|
+
const COMMENT_OPENINGS: ReadonlyArray<string> = ['//', '/*']
|
|
21
|
+
// A private-use character, which no source file this reads holds.
|
|
22
|
+
const MARK = ''
|
|
23
|
+
const LINE_BREAK = '\n'
|
|
24
|
+
const SEPARATOR = ' '
|
|
25
|
+
// Script blocks are parsed; style blocks are left whole, so `<!--` in a stylesheet is never read as
|
|
26
|
+
// a markup comment.
|
|
27
|
+
const RAW_BLOCK = /<(?<tag>script|style)\b[^>]*>(?<body>[\s\S]*?)<\/\k<tag>>/gu
|
|
28
|
+
const SCRIPT_TAG = 'script'
|
|
29
|
+
const MARKUP_COMMENT = /<!--[\s\S]*?-->/gu
|
|
30
|
+
const VIRTUAL_FILE_NAME = 'fix-delta.ts'
|
|
31
|
+
// A comment a tool reads is code: a fix that silences lint, the type check or coverage, or changes
|
|
32
|
+
// what a bundler or the type checker reads, does it by adding one — the edit only the review sees.
|
|
33
|
+
// JSDoc type tags count too, since a checked `.js` file takes its types from them — any tag carrying a
|
|
34
|
+
// `{…}` type expression, plus the type tags that carry none. Matching too much only costs a round.
|
|
35
|
+
const DIRECTIVE_MARKERS: ReadonlyArray<string> = [
|
|
36
|
+
'///',
|
|
37
|
+
'@ts-',
|
|
38
|
+
'eslint',
|
|
39
|
+
'jshint',
|
|
40
|
+
'global ',
|
|
41
|
+
'globals ',
|
|
42
|
+
'prettier-ignore',
|
|
43
|
+
'svelte-ignore',
|
|
44
|
+
'cspell',
|
|
45
|
+
'v8 ignore',
|
|
46
|
+
'c8 ignore',
|
|
47
|
+
'istanbul ignore',
|
|
48
|
+
'__PURE__',
|
|
49
|
+
'__NO_SIDE_EFFECTS__',
|
|
50
|
+
'@vite-ignore',
|
|
51
|
+
'webpack',
|
|
52
|
+
'@jsx',
|
|
53
|
+
'@vitest-environment',
|
|
54
|
+
]
|
|
55
|
+
const JSDOC_TYPE_TAG =
|
|
56
|
+
/@(?:type|typedef|param|returns?|template|satisfies|import|callback|overload|enum|this)\b/u
|
|
57
|
+
const JSDOC_TYPE_EXPRESSION = /@\w+\s*\{/u
|
|
58
|
+
|
|
59
|
+
function is_directive(text: string, range: Range): boolean {
|
|
60
|
+
const comment = text.slice(range[0], range[1])
|
|
61
|
+
|
|
62
|
+
if (JSDOC_TYPE_TAG.test(comment) || JSDOC_TYPE_EXPRESSION.test(comment)) return true
|
|
63
|
+
|
|
64
|
+
return DIRECTIVE_MARKERS.some((marker) => comment.includes(marker))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function is_comment(text: string, range: Range): boolean {
|
|
68
|
+
return COMMENT_OPENINGS.some((opening) => text.startsWith(opening, range[0]))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function trivia_at(api: TypeScriptApi, text: string, position: number): Array<Range> {
|
|
72
|
+
const found = [
|
|
73
|
+
...(api.getLeadingCommentRanges(text, position) ?? []),
|
|
74
|
+
...(api.getTrailingCommentRanges(text, position) ?? []),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
return found.map((range): Range => [range.pos, range.end])
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// **JSDoc nodes are not descended into**: their children sit inside the comment, and a `//` there
|
|
81
|
+
// would read as a line comment running past the `*/` into the code that follows it.
|
|
82
|
+
function collect(
|
|
83
|
+
api: TypeScriptApi,
|
|
84
|
+
node: ts.Node,
|
|
85
|
+
source: ts.SourceFile,
|
|
86
|
+
into: Array<Range>,
|
|
87
|
+
): void {
|
|
88
|
+
if (api.isJSDoc(node)) return
|
|
89
|
+
|
|
90
|
+
into.push(...trivia_at(api, source.text, node.pos))
|
|
91
|
+
|
|
92
|
+
for (const child of node.getChildren(source)) collect(api, child, source, into)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parsed as TypeScript whatever the extension, because a `.js` file is valid TypeScript syntax and JSX
|
|
96
|
+
// is not — JSX text holding `//` is exactly what a trivia read would mistake for a comment, and here
|
|
97
|
+
// it is a parse error instead.
|
|
98
|
+
function script_ranges(api: TypeScriptApi, text: string): Array<Range> | undefined {
|
|
99
|
+
const source = api.createSourceFile(VIRTUAL_FILE_NAME, text, api.ScriptTarget.Latest, false)
|
|
100
|
+
const diagnostics = (source as ts.SourceFile & { parseDiagnostics?: ReadonlyArray<unknown> })
|
|
101
|
+
.parseDiagnostics
|
|
102
|
+
|
|
103
|
+
if (diagnostics === undefined || diagnostics.length > 0) return undefined
|
|
104
|
+
const ranges: Array<Range> = []
|
|
105
|
+
|
|
106
|
+
collect(api, source, source, ranges)
|
|
107
|
+
|
|
108
|
+
return ranges.filter((range) => is_comment(text, range))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function shifted(ranges: ReadonlyArray<Range>, offset: number): Array<Range> {
|
|
112
|
+
return ranges.map((range): Range => [range[0] + offset, range[1] + offset])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function is_inside(blocks: ReadonlyArray<Range>, index: number): boolean {
|
|
116
|
+
return blocks.some((block) => index >= block[0] && index < block[1])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function markup_ranges(text: string, blocks: ReadonlyArray<Range>): Array<Range> {
|
|
120
|
+
return [...text.matchAll(MARKUP_COMMENT)]
|
|
121
|
+
.filter((match) => !is_inside(blocks, match.index))
|
|
122
|
+
.map((match): Range => [match.index, match.index + match[0].length])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function block_ranges(api: TypeScriptApi, match: RegExpExecArray): Array<Range> | undefined {
|
|
126
|
+
if (match.groups?.['tag'] !== SCRIPT_TAG) return []
|
|
127
|
+
const body = match.groups['body'] ?? ''
|
|
128
|
+
const ranges = script_ranges(api, body)
|
|
129
|
+
|
|
130
|
+
return ranges && shifted(ranges, match.index + match[0].indexOf(body))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function svelte_ranges(api: TypeScriptApi, text: string): Array<Range> | undefined {
|
|
134
|
+
const matches = [...text.matchAll(RAW_BLOCK)]
|
|
135
|
+
const scripts = matches.map((match) => block_ranges(api, match))
|
|
136
|
+
|
|
137
|
+
if (scripts.includes(undefined)) return undefined
|
|
138
|
+
const blocks = matches.map((match): Range => [match.index, match.index + match[0].length])
|
|
139
|
+
|
|
140
|
+
return [...scripts.flatMap((ranges) => ranges ?? []), ...markup_ranges(text, blocks)]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A comment spanning lines is a line terminator to the program — `return` followed by one returns
|
|
144
|
+
// nothing — so its break is kept, and a one-line comment in its place cannot strip to the same text.
|
|
145
|
+
function mark_for(text: string, start: number, end: number): string {
|
|
146
|
+
return text.slice(start, end).includes(LINE_BREAK) ? `${MARK}${LINE_BREAK}${MARK}` : MARK
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function marked(text: string, ranges: ReadonlyArray<Range>): string {
|
|
150
|
+
const ordered = ranges.toSorted((left, right) => left[0] - right[0])
|
|
151
|
+
let result = ''
|
|
152
|
+
let cursor = 0
|
|
153
|
+
|
|
154
|
+
for (const [start, end] of ordered) {
|
|
155
|
+
if (start < cursor) continue
|
|
156
|
+
result += `${text.slice(cursor, start)}${mark_for(text, start, end)}`
|
|
157
|
+
cursor = end
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return `${result}${text.slice(cursor)}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A comment is whitespace to the program, so it becomes one space, and a line it alone occupied goes
|
|
164
|
+
// away. Indentation and lines without a comment are kept byte for byte — a blank line in a template
|
|
165
|
+
// literal is data.
|
|
166
|
+
function normalized_line(line: string): Array<string> {
|
|
167
|
+
if (!line.includes(MARK)) return [line]
|
|
168
|
+
const indentation = line.slice(0, line.length - line.trimStart().length)
|
|
169
|
+
const words = line
|
|
170
|
+
.split(MARK)
|
|
171
|
+
.map((part) => part.trim())
|
|
172
|
+
.filter((part) => part !== '')
|
|
173
|
+
|
|
174
|
+
return words.length === 0 ? [] : [`${indentation}${words.join(SEPARATOR)}`]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function without(text: string, ranges: ReadonlyArray<Range>): string {
|
|
178
|
+
return marked(text, ranges)
|
|
179
|
+
.split(LINE_BREAK)
|
|
180
|
+
.flatMap((line) => normalized_line(line))
|
|
181
|
+
.join(LINE_BREAK)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function strip(api: TypeScriptApi, text: string, is_svelte: boolean): string | undefined {
|
|
185
|
+
const ranges = is_svelte ? svelte_ranges(api, text) : script_ranges(api, text)
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
ranges &&
|
|
189
|
+
without(
|
|
190
|
+
text,
|
|
191
|
+
ranges.filter((range) => !is_directive(text, range)),
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const comment_ranges = { strip }
|
|
197
|
+
|
|
198
|
+
export { comment_ranges }
|
|
199
|
+
export type { TypeScriptApi }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
|
|
3
4
|
import { review_attest, type AttestVerdict } from './review-attest'
|
|
4
5
|
import { review_checkout } from './review-checkout'
|
|
5
6
|
|
|
@@ -51,6 +52,22 @@ async function run_attest(nonce: string): Promise<number> {
|
|
|
51
52
|
return FAILURE_EXIT_CODE
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
// An attested review still describes the issue as it read it: a person's update it has not taken in,
|
|
56
|
+
// or one taken in after the brief, makes its verdict stale (`issue-watch-gate.ts`).
|
|
57
|
+
async function report_issue_watch(verdict: AttestVerdict): Promise<number> {
|
|
58
|
+
const refusal = await issue_watch_gate.review_refusal_here()
|
|
59
|
+
|
|
60
|
+
if (refusal !== undefined) {
|
|
61
|
+
console.error(refusal)
|
|
62
|
+
|
|
63
|
+
return FAILURE_EXIT_CODE
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.info(attested_line(verdict))
|
|
67
|
+
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
// **`check_here`, not `check`** — the pointer is keyed on the repository root, and `check`'s default
|
|
55
72
|
// is `PROJECT_ROOT`, which is `process.cwd()`. Run from a subdirectory, or from the session checkout
|
|
56
73
|
// rather than the lane's, the two hash different keys: no pointer is found, the command prints "no
|
|
@@ -65,15 +82,13 @@ async function run_check(): Promise<number> {
|
|
|
65
82
|
return 0
|
|
66
83
|
}
|
|
67
84
|
|
|
68
|
-
if (verdict.status
|
|
69
|
-
console.
|
|
85
|
+
if (verdict.status !== 'ok') {
|
|
86
|
+
console.error(review_attest.refusal_message(verdict))
|
|
70
87
|
|
|
71
|
-
return
|
|
88
|
+
return FAILURE_EXIT_CODE
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return FAILURE_EXIT_CODE
|
|
91
|
+
return await report_issue_watch(verdict)
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
// Anything unrecognized is a usage error rather than a default. A misspelled flag that fell through
|
|
@@ -12,11 +12,12 @@ import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
|
|
|
12
12
|
import { find_package_directory } from '#scripts/josh/josh-logic'
|
|
13
13
|
import { path_decision } from '#scripts/josh/path-decision'
|
|
14
14
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
15
|
-
import {
|
|
15
|
+
import { lane_session_broker } from '#scripts/lane/lane-session-broker'
|
|
16
16
|
import { run_ship_review } from '#scripts/run/ship/run-ship-review'
|
|
17
17
|
import { review_attest } from './review-attest'
|
|
18
18
|
import { review_brief } from './review-brief'
|
|
19
19
|
import { review_checkout, type ReviewCheckout } from './review-checkout'
|
|
20
|
+
import { review_code_tree } from './review-code-tree'
|
|
20
21
|
import type { DiffParts } from './review-diff-parts'
|
|
21
22
|
import { review_diff_write } from './review-diff-write'
|
|
22
23
|
import { review_level, type ReviewLevel } from './review-level'
|
|
@@ -112,13 +113,15 @@ function kept_note(taken_at: string): string {
|
|
|
112
113
|
// `change_base`, and `change_base` is recomputed on every invocation — so without the commit it was
|
|
113
114
|
// measured against, round 2 cannot tell a fix from a base that moved under it, and reports the
|
|
114
115
|
// difference between two incomparable sets as round 1's fixes.
|
|
116
|
+
// **Answers whether it wrote**, because the comment-free code record is taken only at that moment: a
|
|
117
|
+
// code record retaken over a kept snapshot would describe the fixed tree, the exact retake above.
|
|
115
118
|
function record_round_one(
|
|
116
119
|
round: number,
|
|
117
120
|
tree: Record<string, string>,
|
|
118
121
|
base: string | undefined,
|
|
119
122
|
target?: string,
|
|
120
|
-
):
|
|
121
|
-
if (round !== FIRST_ROUND) return
|
|
123
|
+
): boolean {
|
|
124
|
+
if (round !== FIRST_ROUND) return false
|
|
122
125
|
|
|
123
126
|
try {
|
|
124
127
|
const recorded = review_stamps.round_one_stamp.read(target)
|
|
@@ -126,12 +129,15 @@ function record_round_one(
|
|
|
126
129
|
if (recorded !== undefined) {
|
|
127
130
|
console.error(kept_note(recorded.taken_at))
|
|
128
131
|
|
|
129
|
-
return
|
|
132
|
+
return false
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
review_stamps.round_one_stamp.write(tree, target, base)
|
|
136
|
+
|
|
137
|
+
return true
|
|
133
138
|
} catch {
|
|
134
139
|
/* no record widens the next round rather than narrowing it */
|
|
140
|
+
return false
|
|
135
141
|
}
|
|
136
142
|
}
|
|
137
143
|
|
|
@@ -150,7 +156,7 @@ function record_round_one(
|
|
|
150
156
|
// was written by the brief the pointer names: one left by an earlier brief is not the tree the
|
|
151
157
|
// finished review read, so the resume reads the whole change instead.
|
|
152
158
|
function attested_tree(checkout: ReviewCheckout): FileMapStamp | undefined {
|
|
153
|
-
const { findings } =
|
|
159
|
+
const { findings } = lane_session_broker.review_paths(PROJECT_ROOT)
|
|
154
160
|
const briefed = review_stamps.briefed_stamp.read()
|
|
155
161
|
|
|
156
162
|
if (briefed === undefined || !review_attest.is_briefed_since(briefed.taken_at, checkout.root)) {
|
|
@@ -308,6 +314,17 @@ async function read_change(): Promise<ChangeReading> {
|
|
|
308
314
|
return { base, commit, paths, tree }
|
|
309
315
|
}
|
|
310
316
|
|
|
317
|
+
async function record_review(
|
|
318
|
+
round: number,
|
|
319
|
+
tree: Record<string, string>,
|
|
320
|
+
commit: string | undefined,
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
const is_snapshot_written = record_round_one(round, tree, commit)
|
|
323
|
+
|
|
324
|
+
if (is_snapshot_written) await review_code_tree.record(Object.keys(tree), commit)
|
|
325
|
+
record_briefed(round, tree, commit)
|
|
326
|
+
}
|
|
327
|
+
|
|
311
328
|
async function run_review(round: number, reading: ChangeReading): Promise<number> {
|
|
312
329
|
const { base, commit, paths, tree } = reading
|
|
313
330
|
const refusal = scoped_green.refusal_for(tree, commit)
|
|
@@ -318,8 +335,7 @@ async function run_review(round: number, reading: ChangeReading): Promise<number
|
|
|
318
335
|
if (resolved.kind === 'rejected') return report_error(resolved.note)
|
|
319
336
|
agent_role_profile.warn_of_default()
|
|
320
337
|
console.info(await compose_brief({ round, paths, tree, base, profile: resolved.profile }))
|
|
321
|
-
|
|
322
|
-
record_briefed(round, tree, commit)
|
|
338
|
+
await record_review(round, tree, commit)
|
|
323
339
|
|
|
324
340
|
return 0
|
|
325
341
|
}
|
|
@@ -343,6 +359,7 @@ const review_brief_cli = {
|
|
|
343
359
|
format_reason,
|
|
344
360
|
KEPT_NOTE_PREFIX,
|
|
345
361
|
parse_round,
|
|
362
|
+
record_review,
|
|
346
363
|
record_round_one,
|
|
347
364
|
report_error,
|
|
348
365
|
run,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { git_command } from '#scripts/git/git-command'
|
|
5
|
+
import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
|
|
6
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
7
|
+
import { comment_ranges, type TypeScriptApi } from './comment-ranges'
|
|
8
|
+
import { review_stamps } from './review-stamps'
|
|
9
|
+
|
|
10
|
+
// The second map round 1 records beside `review-tree.ts`'s: for each source file, the digest of its
|
|
11
|
+
// text with the comments taken out (joshuafolkken/kit#3826).
|
|
12
|
+
//
|
|
13
|
+
// **Why a second record rather than a second reading of the first.** The round-1 snapshot holds
|
|
14
|
+
// digests, not text, so after the fixes nothing can say what a changed `.ts` file used to contain. A
|
|
15
|
+
// comment-free digest taken at the same moment is what lets round 2 tell "only the comments changed"
|
|
16
|
+
// from "the code changed" — equal digests mean equal code.
|
|
17
|
+
//
|
|
18
|
+
// **`.claude/` is outside it**: the decision on joshuafolkken/kit#3787 left `.claude/**` reviewed in
|
|
19
|
+
// full, comments included.
|
|
20
|
+
//
|
|
21
|
+
// **TypeScript is resolved from the project, and its absence records nothing.** It is an optional peer
|
|
22
|
+
// of this package, and a missing map is a comment-only arm that never fires — a round run, never one
|
|
23
|
+
// skipped.
|
|
24
|
+
|
|
25
|
+
const SCRIPT_EXTENSIONS: ReadonlyArray<string> = ['.ts', '.js']
|
|
26
|
+
const SVELTE_EXTENSION = '.svelte'
|
|
27
|
+
const EXCLUDED_PREFIX = '.claude/'
|
|
28
|
+
const TYPESCRIPT_MODULE = 'typescript'
|
|
29
|
+
const PACKAGE_JSON = 'package.json'
|
|
30
|
+
|
|
31
|
+
function is_svelte(relative: string): boolean {
|
|
32
|
+
return relative.endsWith(SVELTE_EXTENSION)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function is_source(relative: string): boolean {
|
|
36
|
+
if (relative.startsWith(EXCLUDED_PREFIX)) return false
|
|
37
|
+
|
|
38
|
+
return is_svelte(relative) || SCRIPT_EXTENSIONS.some((extension) => relative.endsWith(extension))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function load_typescript(root: string): TypeScriptApi | undefined {
|
|
42
|
+
try {
|
|
43
|
+
return createRequire(path.join(root, PACKAGE_JSON))(TYPESCRIPT_MODULE) as TypeScriptApi
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function code_digest(api: TypeScriptApi, root: string, relative: string): string | undefined {
|
|
50
|
+
try {
|
|
51
|
+
const code = comment_ranges.strip(
|
|
52
|
+
api,
|
|
53
|
+
readFileSync(path.join(root, relative), 'utf8'),
|
|
54
|
+
is_svelte(relative),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return code === undefined ? undefined : stamp_file.digest(code)
|
|
58
|
+
} catch {
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A path that is deleted, does not parse, or is not a source file has no entry — and a path with no
|
|
64
|
+
// entry is never comment-only.
|
|
65
|
+
function tree_of(
|
|
66
|
+
root: string,
|
|
67
|
+
paths: ReadonlyArray<string>,
|
|
68
|
+
api: TypeScriptApi | undefined = load_typescript(root),
|
|
69
|
+
): Record<string, string> {
|
|
70
|
+
if (api === undefined) return {}
|
|
71
|
+
const entries = paths
|
|
72
|
+
.filter((relative) => is_source(relative))
|
|
73
|
+
.flatMap((relative) => {
|
|
74
|
+
const digest = code_digest(api, root, relative)
|
|
75
|
+
|
|
76
|
+
return digest === undefined ? [] : [[relative, digest] as const]
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return Object.fromEntries(entries)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function read(paths: ReadonlyArray<string>, root?: string): Promise<Record<string, string>> {
|
|
83
|
+
return tree_of(root ?? (await git_command.repository_root()), paths)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Written only in the same brief that wrote the round-1 snapshot, so the two describe one moment. A
|
|
87
|
+
// failure removes any earlier record rather than leaving one from another moment beside the new
|
|
88
|
+
// snapshot.
|
|
89
|
+
async function record(
|
|
90
|
+
paths: ReadonlyArray<string>,
|
|
91
|
+
base: string | undefined,
|
|
92
|
+
target?: string,
|
|
93
|
+
root?: string,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
try {
|
|
96
|
+
review_stamps.round_one_code_stamp.write(await read(paths, root), target, base)
|
|
97
|
+
} catch {
|
|
98
|
+
review_stamps.clear_round_one_code(target)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The paths whose code round 1's fixes left as it was — whose difference, if any, is in comments.
|
|
103
|
+
// Nothing qualifies against a record taken on another base, for the reason the round-1 snapshot is
|
|
104
|
+
// refused on one.
|
|
105
|
+
function comment_only(
|
|
106
|
+
snapshot: FileMapStamp | undefined,
|
|
107
|
+
tree: Record<string, string>,
|
|
108
|
+
base: string | undefined,
|
|
109
|
+
): ReadonlyArray<string> {
|
|
110
|
+
if (snapshot === undefined || base === undefined || snapshot.base !== base) return []
|
|
111
|
+
|
|
112
|
+
return Object.keys(snapshot.files).filter(
|
|
113
|
+
(relative) => snapshot.files[relative] === tree[relative],
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const review_code_tree = { comment_only, read, record, tree_of }
|
|
118
|
+
|
|
119
|
+
export { review_code_tree }
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { gh_author_association } from '#scripts/gh/gh-author-association'
|
|
3
|
+
import { git_gh_exec } from '#scripts/gh/git-gh-exec'
|
|
4
|
+
import { git_gh_issue_read } from '#scripts/gh/git-gh-issue-read'
|
|
5
|
+
import { parse_json, parse_json_array_safe } from '#scripts/git/parse-json-array'
|
|
6
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
7
|
+
import { observation_ledger_home } from '#scripts/observations/observation-ledger-home'
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { review_finding_ledger } from './review-finding-ledger'
|
|
10
|
+
|
|
11
|
+
// A review round recorded after its pull request opened (`josh review:record --comment`), and how its
|
|
12
|
+
// lines reach the ledger's readers (joshuafolkken/kit#3811). The round is posted on the issue because
|
|
13
|
+
// a ledger line written then would be pushed onto the open pull request and restart its CI — and so
|
|
14
|
+
// nothing that reads the ledger (`review:findings`, the retrospective) ever saw it.
|
|
15
|
+
//
|
|
16
|
+
// **The next run that records a round carries it.** Once an issue is closed no round is added to it,
|
|
17
|
+
// so `review:record` copies each closed issue's comment rounds not yet carried into
|
|
18
|
+
// `<issue>-comment-rounds.md` in the ledger, and the lines ride that run's own commit: the ledger stays
|
|
19
|
+
// the one place a reader looks, and no push restarts any CI. **The file is the carried mark** — an
|
|
20
|
+
// issue whose file exists is never read again, and two lanes carrying one issue write the same file,
|
|
21
|
+
// so their pull requests merge to the same lines.
|
|
22
|
+
|
|
23
|
+
const COMMENT_HEADING_MARK = '### '
|
|
24
|
+
const COMMENT_TITLE = 'Review round recorded after the pull request opened'
|
|
25
|
+
const COMMENT_HEADING = `${COMMENT_HEADING_MARK}${COMMENT_TITLE}`
|
|
26
|
+
const COMMENT_NOTE =
|
|
27
|
+
'Kept here rather than in the observation ledger: a line appended after the commit would be pushed onto the open pull request and restart its CI.'
|
|
28
|
+
const CODE_FENCE = '```'
|
|
29
|
+
const CARRIED_SUFFIX = '-comment-rounds'
|
|
30
|
+
// One page of the newest closed issues: an issue leaves the search's interest once carried, so the
|
|
31
|
+
// ones still to carry are the recently closed.
|
|
32
|
+
const PER_PAGE = '100'
|
|
33
|
+
const UNNAMED_REPOSITORY = 'Could not name this repository from `git remote`.'
|
|
34
|
+
const UNREAD_SEARCH = 'Could not read the search for closed issues with a comment round.'
|
|
35
|
+
const UNREAD_COMMENTS = 'Could not read the comments of'
|
|
36
|
+
|
|
37
|
+
const search_item_schema = z.object({ number: z.number() })
|
|
38
|
+
const search_schema = z.object({ items: z.array(search_item_schema) })
|
|
39
|
+
const comment_schema = z.object({
|
|
40
|
+
body: z.string().nullish(),
|
|
41
|
+
author_association: z.string().nullish(),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
type IssueComment = z.infer<typeof comment_schema>
|
|
45
|
+
|
|
46
|
+
function body_of(lines: ReadonlyArray<string>): string {
|
|
47
|
+
return [COMMENT_HEADING, '', COMMENT_NOTE, '', CODE_FENCE, ...lines, CODE_FENCE].join('\n')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The ledger lines of every comment round among one issue's comments, oldest first; any other
|
|
51
|
+
// comment, one that only quotes the heading among them, carries none — nor does one whose author
|
|
52
|
+
// cannot write to the repository: anyone can comment on a public issue, and the carried lines ride
|
|
53
|
+
// the run's commit unreviewed.
|
|
54
|
+
function lines_of(comments: ReadonlyArray<IssueComment>): Array<string> {
|
|
55
|
+
return comments
|
|
56
|
+
.filter((comment) => gh_author_association.is_trusted(comment.author_association))
|
|
57
|
+
.map((comment) => comment.body ?? '')
|
|
58
|
+
.filter((body) => body.startsWith(COMMENT_HEADING))
|
|
59
|
+
.flatMap((body) => body.split('\n'))
|
|
60
|
+
.filter((line) => review_finding_ledger.is_finding_line(line))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function search_path(repo: string): string {
|
|
64
|
+
const query = `repo:${repo} is:issue is:closed in:comments "${COMMENT_TITLE}"`
|
|
65
|
+
const parameters = new URLSearchParams({ q: query, sort: 'updated', per_page: PER_PAGE })
|
|
66
|
+
|
|
67
|
+
return `search/issues?${parameters.toString()}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The closed issues whose comments name a comment round; a repository that cannot be named or a
|
|
71
|
+
// search that cannot be read throws, so the run says so rather than carrying nothing in silence.
|
|
72
|
+
async function closed_issues(): Promise<ReadonlyArray<number>> {
|
|
73
|
+
const repo = session_cite.session_repo()
|
|
74
|
+
|
|
75
|
+
if (repo === undefined) throw new Error(UNNAMED_REPOSITORY)
|
|
76
|
+
|
|
77
|
+
const raw = await git_gh_exec.exec_gh_api({ path: search_path(repo) })
|
|
78
|
+
const page = parse_json.parse_json_object_or_undefined(raw, search_schema)
|
|
79
|
+
|
|
80
|
+
if (page === undefined) throw new Error(UNREAD_SEARCH)
|
|
81
|
+
|
|
82
|
+
return page.items.map((item) => item.number)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function carried_path(issue: number, root: string): string {
|
|
86
|
+
return observation_ledger_home.file_path(`${String(issue)}${CARRIED_SUFFIX}`, root)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// An issue whose comments could not be read writes no file, so the next run reads it again, and
|
|
90
|
+
// throws so this one says so.
|
|
91
|
+
async function carry_issue(issue: number, root: string): Promise<number> {
|
|
92
|
+
const raw = await git_gh_issue_read.issue_list_comments(String(issue))
|
|
93
|
+
|
|
94
|
+
if (raw === undefined) throw new Error(`${UNREAD_COMMENTS} ${session_cite.issue(issue)}`)
|
|
95
|
+
|
|
96
|
+
const lines = lines_of(parse_json_array_safe(raw, comment_schema))
|
|
97
|
+
|
|
98
|
+
await observation_ledger_home.append(carried_path(issue, root), lines)
|
|
99
|
+
|
|
100
|
+
return lines.length
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Copies every closed issue's comment rounds not yet in the ledger, and answers how many lines.
|
|
104
|
+
async function carry(root: string): Promise<number> {
|
|
105
|
+
const issues = await closed_issues()
|
|
106
|
+
const pending = issues.filter((issue) => !existsSync(carried_path(issue, root)))
|
|
107
|
+
const counts = await Promise.all(pending.map(async (issue) => await carry_issue(issue, root)))
|
|
108
|
+
|
|
109
|
+
return counts.reduce((total, count) => total + count, 0)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const review_comment_round = { body_of, carried_path, carry, lines_of }
|
|
113
|
+
|
|
114
|
+
export { review_comment_round }
|
|
@@ -4,6 +4,7 @@ import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
|
|
|
4
4
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
5
5
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
6
6
|
import { observation_ledger_home } from '#scripts/observations/observation-ledger-home'
|
|
7
|
+
import { review_comment_round } from './review-comment-round'
|
|
7
8
|
import { review_finding_ledger, type Finding } from './review-finding-ledger'
|
|
8
9
|
import { review_record, type RecordVerdict } from './review-record'
|
|
9
10
|
|
|
@@ -16,7 +17,8 @@ import { review_record, type RecordVerdict } from './review-record'
|
|
|
16
17
|
// **`--comment` records a round that ran after the pull request opened**: the same lines go to the
|
|
17
18
|
// issue as a comment and nothing is appended. A line written to the tree then has no commit of the
|
|
18
19
|
// run's left to ride, so `pnpm josh followup` would push it onto the open pull request and its CI
|
|
19
|
-
// would start over.
|
|
20
|
+
// would start over. The next round recorded to the ledger carries it there once the issue is closed
|
|
21
|
+
// (`review-comment-round.ts`).
|
|
20
22
|
//
|
|
21
23
|
// `josh review:record --check --issue <N>` is the read half `pnpm josh followup` runs before it
|
|
22
24
|
// merges: `ok` / `not-required` exit zero, `missing` exits non-zero so the
|
|
@@ -31,10 +33,10 @@ const CHECK_USAGE = 'Usage: josh review:record --check --issue <N>'
|
|
|
31
33
|
const RECORDED_LINE = 'Recorded: the review round for this issue is in the ledger.'
|
|
32
34
|
const NOT_REQUIRED_LINE =
|
|
33
35
|
'No review-finding ledger is kept in this checkout, so there is nothing to check.'
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const CARRIED_LINE =
|
|
37
|
+
'review-finding line(s) recorded on closed issues after their pull requests opened into the ledger.'
|
|
38
|
+
const CARRY_FAILED_LINE =
|
|
39
|
+
'Could not carry the closed issues’ comment rounds into the ledger; the next round retries:'
|
|
38
40
|
const OPTIONS = {
|
|
39
41
|
issue: { type: 'string' },
|
|
40
42
|
check: { type: 'boolean' },
|
|
@@ -161,15 +163,14 @@ async function run_check(issue: string | undefined, root: string): Promise<numbe
|
|
|
161
163
|
return FAILURE_EXIT_CODE
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
function comment_body(lines: ReadonlyArray<string>): string {
|
|
165
|
-
return [COMMENT_HEADING, '', COMMENT_NOTE, '', CODE_FENCE, ...lines, CODE_FENCE].join('\n')
|
|
166
|
-
}
|
|
167
|
-
|
|
168
166
|
// **A comment that could not be posted fails the record**: it is the round's only copy, so a
|
|
169
167
|
// swallowed refusal would read as a recorded round that exists nowhere.
|
|
170
168
|
async function post_comment(issue: number, lines: ReadonlyArray<string>): Promise<number> {
|
|
171
169
|
try {
|
|
172
|
-
const url = await git_gh_issue_write.issue_comment(
|
|
170
|
+
const url = await git_gh_issue_write.issue_comment(
|
|
171
|
+
String(issue),
|
|
172
|
+
review_comment_round.body_of(lines),
|
|
173
|
+
)
|
|
173
174
|
|
|
174
175
|
console.info(`Recorded ${String(lines.length)} review-finding line(s) in ${url.trim()}.`)
|
|
175
176
|
|
|
@@ -181,8 +182,35 @@ async function post_comment(issue: number, lines: ReadonlyArray<string>): Promis
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
// The closed issues' comment rounds ride this round's commit (`review-comment-round.ts`). **A carry
|
|
186
|
+
// that fails never fails the record**: this round is recorded, and the comment rounds wait for the
|
|
187
|
+
// next run's.
|
|
188
|
+
async function carry_comment_rounds(root: string): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
const count = await review_comment_round.carry(root)
|
|
191
|
+
|
|
192
|
+
if (count > 0) console.info(`Carried ${String(count)} ${CARRIED_LINE}`)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.warn(`${CARRY_FAILED_LINE} ${error instanceof Error ? error.message : String(error)}`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
184
198
|
// The round lands in its own issue's file, so two lanes recording at once
|
|
185
199
|
// write two files, and their pull requests never conflict on the ledger.
|
|
200
|
+
async function append_round(
|
|
201
|
+
issue: number,
|
|
202
|
+
lines: ReadonlyArray<string>,
|
|
203
|
+
root: string,
|
|
204
|
+
): Promise<number> {
|
|
205
|
+
const ledger_path = observation_ledger_home.issue_path(issue, root)
|
|
206
|
+
|
|
207
|
+
await observation_ledger_home.append(ledger_path, lines)
|
|
208
|
+
console.info(confirmation(lines.length, ledger_path))
|
|
209
|
+
await carry_comment_rounds(root)
|
|
210
|
+
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
|
|
186
214
|
async function run_record(parsed: Parsed, now: Date, root: string): Promise<number> {
|
|
187
215
|
const request = to_request(parsed)
|
|
188
216
|
|
|
@@ -196,12 +224,7 @@ async function run_record(parsed: Parsed, now: Date, root: string): Promise<numb
|
|
|
196
224
|
|
|
197
225
|
if (parsed.comment) return await post_comment(request.issue, lines)
|
|
198
226
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
await observation_ledger_home.append(ledger_path, lines)
|
|
202
|
-
console.info(confirmation(lines.length, ledger_path))
|
|
203
|
-
|
|
204
|
-
return 0
|
|
227
|
+
return await append_round(request.issue, lines, root)
|
|
205
228
|
}
|
|
206
229
|
|
|
207
230
|
// **The default is the work tree the command runs in, a lane's inside a lane**.
|
|
@@ -232,7 +255,6 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
|
232
255
|
const review_record_cli = {
|
|
233
256
|
accepted_values,
|
|
234
257
|
build_lines,
|
|
235
|
-
comment_body,
|
|
236
258
|
parse_finding,
|
|
237
259
|
run,
|
|
238
260
|
usage_text,
|