@erclx/canon 5.77.0 → 5.79.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/CHANGELOG.md +43 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/app-drive/REQUIREMENT.md +12 -0
- package/claude/skills/app-drive/SKILL.md +44 -8
- package/claude/skills/app-drive/references/config.md +18 -8
- package/claude/skills/app-drive/references/drive-record.md +4 -0
- package/claude/skills/auto-ship/REQUIREMENT.md +1 -1
- package/claude/skills/auto-ship/SKILL.md +5 -4
- package/claude/skills/auto-ship/references/draft-mark.md +33 -15
- package/claude/skills/auto-ship/references/failure-recovery.md +4 -2
- package/claude/skills/auto-ship/references/ui-checklist.md +9 -1
- package/claude/skills/code-craft/REQUIREMENT.md +1 -1
- package/claude/skills/code-craft/SKILL.md +7 -0
- package/claude/skills/code-craft/references/adopted.md +8 -0
- package/claude/skills/git-followup/SKILL.md +1 -0
- package/claude/skills/git-pr/REQUIREMENT.md +2 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/final-command.md +34 -2
- package/claude/skills/git-pr/references/pr.md +1 -1
- package/claude/skills/git-ship/REQUIREMENT.md +2 -2
- package/claude/skills/git-ship/SKILL.md +16 -11
- package/claude/skills/git-ship/references/ci-watch.md +28 -4
- package/claude/skills/markdown-craft/references/markdown.md +4 -4
- package/claude/skills/plan-feature/references/plan.md +2 -0
- package/claude/skills/pr-evidence/SKILL.md +6 -1
- package/claude/skills/pr-evidence/references/evidence.md +1 -0
- package/claude/skills/pr-evidence/references/reel.md +9 -1
- package/claude/skills/review-address/SKILL.md +2 -1
- package/claude/skills/review-craft/references/ui.md +13 -1
- package/claude/skills/review-pr/references/body-variants.md +2 -0
- package/claude/skills/review-pr/references/close-out.md +2 -0
- package/claude/skills/review-pr/references/key-changes.md +2 -1
- package/claude/skills/review-pr/references/post.md +1 -0
- package/claude/skills/review-ui/REQUIREMENT.md +1 -0
- package/claude/skills/review-ui/SKILL.md +2 -2
- package/claude/skills/review-ui/references/driving.md +32 -4
- package/claude/skills/review-ui/references/post.md +6 -2
- package/claude/skills/role-orchestrator/REQUIREMENT.md +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-hand-launch.md +2 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-limit-stops.md +47 -0
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +24 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +3 -2
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +35 -4
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +3 -1
- package/claude/skills/role-worker/SKILL.md +2 -1
- package/claude/skills/task-board/SKILL.md +1 -0
- package/claude/skills/task-board/references/tasks.md +1 -0
- package/claude/skills/test-craft/SKILL.md +1 -0
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
- package/claude/skills/ui-checklist/SKILL.md +2 -1
- package/claude/skills/ui-craft/references/capture.md +16 -0
- package/claude/skills/ui-film/REQUIREMENT.md +2 -2
- package/claude/skills/ui-film/SKILL.md +6 -1
- package/claude/skills/ui-film/references/compose-craft.md +68 -11
- package/claude/skills/ui-film/references/config.md +2 -2
- package/claude/skills/ui-film/references/handoff.md +1 -1
- package/claude/skills/ui-film/references/harness.md +71 -0
- package/claude/skills/ui-film/references/take-craft.md +42 -17
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/operating-model.md +6 -5
- package/docs/workflow/parallel-features.md +2 -1
- package/docs/workflow/visual-design-workflow.md +2 -1
- package/governance/ledger/skills/code-craft/SKILL.md +9 -0
- package/governance/ledger/skills/test-craft/SKILL.md +1 -0
- package/governance/ledger/skills/ui-craft/capture.md +18 -5
- package/package.json +4 -3
- package/src/commands/pr.ts +255 -46
- package/src/commands/sessions.ts +88 -6
- package/src/commands/take.ts +12 -1
- package/src/commands/tasks.ts +41 -2
- package/src/design/components.ts +5 -107
- package/src/design/favicon.ts +21 -17
- package/src/design/render.ts +9 -27
- package/src/design/template.ts +42 -0
- package/src/design/templates/favicon.svg +17 -0
- package/src/design/templates/hand-drawn-figure.css +54 -0
- package/src/design/templates/preview.html +27 -0
- package/src/design/templates/scrollbar.css +29 -0
- package/src/design/templates/status.css +24 -0
- package/src/gate/embedded.ts +400 -0
- package/src/gate/stages.ts +8 -0
- package/src/git/files.ts +5 -2
- package/src/indexes/frontmatter.ts +27 -6
- package/src/indexes/render.ts +21 -3
- package/src/markdown/colons.ts +14 -4
- package/src/markdown/heading-case.ts +9 -2
- package/src/pr/bijection.ts +8 -0
- package/src/pr/evidence.ts +81 -13
- package/src/pr/frames.ts +1 -6
- package/src/pr/health.ts +63 -15
- package/src/pr/lift.ts +77 -9
- package/src/pr/links.ts +160 -0
- package/src/pr/local.ts +17 -4
- package/src/pr/paths.ts +20 -2
- package/src/pr/rest.ts +2 -0
- package/src/pr/review-scope.ts +28 -0
- package/src/pr/review-state.ts +5 -5
- package/src/pr/waiver.ts +45 -0
- package/src/sessions/stop.ts +145 -0
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/take/camera.ts +337 -208
- package/src/take/encode.ts +16 -42
- package/src/take/film.ts +7 -1
- package/src/take/frame-clock.ts +46 -0
- package/src/take/log.ts +43 -2
- package/src/take/perform.ts +85 -13
- package/src/take/plan.ts +26 -5
- package/src/take/reel.ts +30 -5
- package/src/take/screencast.ts +223 -13
- package/src/take/stepper.ts +24 -4
- package/src/tasks/holders.ts +198 -0
- package/src/tasks/validate.ts +148 -0
- package/src/commands/fixtures/gh.sh +0 -64
- package/src/take/fixtures/busy.html +0 -107
- package/src/take/fixtures/fade.html +0 -49
- package/src/take/fixtures/fetch.html +0 -38
- package/src/take/fixtures/input.html +0 -138
- package/src/take/zoom/NOTICE.md +0 -45
- package/src/take/zoom/math-utils.ts +0 -69
- package/src/take/zoom/region-utils.ts +0 -55
- package/src/take/zoom/spring.ts +0 -133
- /package/src/sessions/{transfer/transcript.ts → transcript.ts} +0 -0
package/src/commands/pr.ts
CHANGED
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
isEvidencePath,
|
|
44
44
|
PNG_HEADER_BYTES,
|
|
45
45
|
readChecklistBoxes,
|
|
46
|
+
countDrivable,
|
|
47
|
+
type ChecklistBox,
|
|
46
48
|
readOwed,
|
|
47
49
|
readPngWidth,
|
|
48
50
|
readUncaptured,
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
settleChecklist,
|
|
52
54
|
} from '@/pr/evidence'
|
|
53
55
|
import { isWithinTolerance } from '@/pr/pixels'
|
|
56
|
+
import { REEL_MAX_BYTES } from '@/take/reel'
|
|
54
57
|
import {
|
|
55
58
|
dropFramesWithLegacy,
|
|
56
59
|
FRAMES_BRANCH,
|
|
@@ -64,7 +67,6 @@ import {
|
|
|
64
67
|
LEGACY_FRAMES_BRANCH,
|
|
65
68
|
pruneFramesWithLegacy,
|
|
66
69
|
pushFrame,
|
|
67
|
-
REEL_MAX_BYTES,
|
|
68
70
|
readFrame,
|
|
69
71
|
readReel,
|
|
70
72
|
isDesignReference,
|
|
@@ -83,6 +85,7 @@ import {
|
|
|
83
85
|
type PullState,
|
|
84
86
|
readBaseStanding,
|
|
85
87
|
readChangedPaths,
|
|
88
|
+
readsDrivableBox,
|
|
86
89
|
} from '@/pr/lift'
|
|
87
90
|
import { close, type CloseApi, type CloseRefusal } from '@/pr/close'
|
|
88
91
|
import {
|
|
@@ -110,6 +113,7 @@ import {
|
|
|
110
113
|
type LocalRefusal,
|
|
111
114
|
type LocalRunner,
|
|
112
115
|
} from '@/pr/local'
|
|
116
|
+
import { linkKeyChanges } from '@/pr/links'
|
|
113
117
|
import { KEY_CHANGES } from '@/pr/paths'
|
|
114
118
|
import {
|
|
115
119
|
findDeployWorkflow,
|
|
@@ -158,6 +162,7 @@ const GH_TIMEOUT_MS = 30_000
|
|
|
158
162
|
const UNNAMED_PRINT_LIMIT = 10
|
|
159
163
|
|
|
160
164
|
interface KeyChangesOptions {
|
|
165
|
+
readonly link?: boolean
|
|
161
166
|
readonly body?: string
|
|
162
167
|
readonly base?: string
|
|
163
168
|
readonly root?: string
|
|
@@ -171,7 +176,8 @@ interface ReadOptions {
|
|
|
171
176
|
|
|
172
177
|
interface EvidenceOptions extends ReadOptions {
|
|
173
178
|
readonly preview?: string
|
|
174
|
-
|
|
179
|
+
/** A path to fold in, or false from `--no-checklist` to drop the carried one. */
|
|
180
|
+
readonly checklist?: string | false
|
|
175
181
|
readonly local?: string
|
|
176
182
|
readonly reel?: string
|
|
177
183
|
readonly localSkip?: string
|
|
@@ -255,7 +261,7 @@ const TICK_REFUSALS: Record<TickReason | 'gh-failed', string> = {
|
|
|
255
261
|
'no-box':
|
|
256
262
|
'A named box is past the end of the checklist. Number boxes off `canon pr evidence --json`.',
|
|
257
263
|
'taste-box':
|
|
258
|
-
'A named box
|
|
264
|
+
'A named box carries (taste) among its trailing markers, which no driver passes. Nothing was written.',
|
|
259
265
|
'gh-failed':
|
|
260
266
|
'gh could not read or edit the marked comment on this pull request.',
|
|
261
267
|
}
|
|
@@ -275,7 +281,7 @@ const LOCAL_REFUSALS: Record<
|
|
|
275
281
|
'Nothing listening inside this worktree served an HTML page. Start the dev server here and re-run to get a link.',
|
|
276
282
|
unhealthy:
|
|
277
283
|
'A page answered inside this worktree, but a health path it declares failed. Start what the page reaches and re-run to get a link.',
|
|
278
|
-
'invalid-config': `${LOCAL_CONFIG_REL} cannot be read, so no health path was checked and no link is given. Fix or delete it and re-run.`,
|
|
284
|
+
'invalid-config': `${LOCAL_CONFIG_REL} cannot be read, so no health path was checked and no link is given. Fix or delete it and re-run. health takes paths starting with /, and page-only takes folders relative to the repository.`,
|
|
279
285
|
'no-listener-reader':
|
|
280
286
|
'This machine offers neither lsof nor /proc, so no listening socket could be read. Nothing is posted.',
|
|
281
287
|
'gh-failed':
|
|
@@ -438,7 +444,7 @@ type EvidenceRefusal =
|
|
|
438
444
|
|
|
439
445
|
const EVIDENCE_REFUSALS: Record<EvidenceRefusal, string> = {
|
|
440
446
|
'check-writes':
|
|
441
|
-
'--check is a read and renders no body, so it takes none of --preview, --local, --checklist, --reel, --local-skip, or --reel-skip. Run the check and the render as two calls.',
|
|
447
|
+
'--check is a read and renders no body, so it takes none of --preview, --local, --checklist, --no-checklist, --reel, --local-skip, or --reel-skip. Run the check and the render as two calls.',
|
|
442
448
|
'gh-failed':
|
|
443
449
|
'gh could not answer for this repository. Name the pull request number.',
|
|
444
450
|
'unreadable-checklist':
|
|
@@ -499,13 +505,17 @@ export function register(program: Command): void {
|
|
|
499
505
|
.helpOption('-h, --help', 'Show this help message')
|
|
500
506
|
.option(
|
|
501
507
|
'--body <path>',
|
|
502
|
-
'Read the body from a file rather than the API, ignoring any number',
|
|
508
|
+
'Read the body from a file rather than the API, ignoring any number except the one --link needs',
|
|
503
509
|
)
|
|
504
510
|
.option(
|
|
505
511
|
'--base <ref>',
|
|
506
512
|
'Far side of the range when --body supplies the body',
|
|
507
513
|
)
|
|
508
514
|
.option('--root <path>', 'Repository to read, defaulting to the cwd')
|
|
515
|
+
.option(
|
|
516
|
+
'--link',
|
|
517
|
+
`Print the body with each ## ${KEY_CHANGES} path linked to its diff, needing a number`,
|
|
518
|
+
)
|
|
509
519
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
510
520
|
.addHelpText(
|
|
511
521
|
'after',
|
|
@@ -534,6 +544,13 @@ export function register(program: Command): void {
|
|
|
534
544
|
'points the change at a locative the path does not name. Read the bullet on',
|
|
535
545
|
"the record's preview before filing an unmet path as a stale claim.",
|
|
536
546
|
'',
|
|
547
|
+
'--link prints the body and nothing else, with every path the section',
|
|
548
|
+
'names that the diff changes written as a link to its row in the Files',
|
|
549
|
+
'tab. The anchor is `diff-` and the SHA-256 of the path. Pass the pull',
|
|
550
|
+
'request number, since the link needs it. It also rewrites the anchor',
|
|
551
|
+
'of a link opening another file, and a plain run reports that link as',
|
|
552
|
+
"mislinked when its label is a changed path and its anchor hashes another's.",
|
|
553
|
+
'',
|
|
537
554
|
'Refusals, read off `reason`:',
|
|
538
555
|
' no-section the body carries no `## Key changes` heading, so it',
|
|
539
556
|
' claims nothing',
|
|
@@ -551,14 +568,18 @@ export function register(program: Command): void {
|
|
|
551
568
|
' gh-missing `gh` is not on the path, so no pull request could be',
|
|
552
569
|
' resolved',
|
|
553
570
|
' gh-failed `gh` could not answer for this repository or branch',
|
|
571
|
+
' no-number --link was given with no pull request number',
|
|
572
|
+
' no-repo-url --link could not read the repository address',
|
|
573
|
+
' link-json --link was given with --json, though it prints the body',
|
|
554
574
|
'',
|
|
555
575
|
'Exit codes:',
|
|
556
|
-
' 0 every claimed path is in the diff',
|
|
576
|
+
' 0 every claimed path is in the diff, and every link opens its own file',
|
|
557
577
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
558
|
-
' 2
|
|
578
|
+
' 2 a claimed path is absent from the diff, or a link opens another file',
|
|
559
579
|
'',
|
|
560
580
|
'Examples:',
|
|
561
581
|
' canon pr key-changes',
|
|
582
|
+
' canon pr key-changes 1265 --link --body .canon/tmp/body.md',
|
|
562
583
|
' canon pr key-changes 1265 --json',
|
|
563
584
|
' canon pr key-changes --body .canon/tmp/body.md --base origin/main',
|
|
564
585
|
'',
|
|
@@ -709,9 +730,11 @@ export function register(program: Command): void {
|
|
|
709
730
|
'',
|
|
710
731
|
'`ui` also carries the newest `## UI review` pass (`heading`, `state`,',
|
|
711
732
|
'`commit`, `source`), `isBound`, which is whether the evidence comment',
|
|
712
|
-
'carries a checklist
|
|
713
|
-
'
|
|
714
|
-
'
|
|
733
|
+
'carries a checklist holding a box a driver can run or the newest UI',
|
|
734
|
+
'pass is open, so a checklist of taste boxes alone binds nothing until',
|
|
735
|
+
'a pass opens, and `carriedFrom` when the lift carried a close',
|
|
736
|
+
'forward, or `drivenPaths` and the painted subset `paintedPaths` when',
|
|
737
|
+
'it refused to. An unbound family reads covered.',
|
|
715
738
|
'',
|
|
716
739
|
'Both families post as pull request reviews, so this reads',
|
|
717
740
|
'`pulls/<n>/reviews` alone, as the lift does. Issue comments are not',
|
|
@@ -772,6 +795,10 @@ export function register(program: Command): void {
|
|
|
772
795
|
'--checklist <path>',
|
|
773
796
|
'Close the body with this visual checklist file, from canon:ui-checklist',
|
|
774
797
|
)
|
|
798
|
+
.option(
|
|
799
|
+
'--no-checklist',
|
|
800
|
+
'Drop the checklist the marked comment carries, rendering the body without it',
|
|
801
|
+
)
|
|
775
802
|
.option(
|
|
776
803
|
'--local <url>',
|
|
777
804
|
"Add this worktree's running server to the address line, from canon pr local",
|
|
@@ -809,8 +836,9 @@ export function register(program: Command): void {
|
|
|
809
836
|
'Read `reason` on the JSON record before posting anything:',
|
|
810
837
|
' ok a body was rendered, with `commentId` set when a marked',
|
|
811
838
|
' comment already exists and should be edited in place',
|
|
812
|
-
' no-evidence nothing in the diff carries an evidence/ segment
|
|
813
|
-
' is
|
|
839
|
+
' no-evidence nothing in the diff carries an evidence/ segment and no',
|
|
840
|
+
' painted path is uncaptured, which is an ordinary,',
|
|
841
|
+
' silent no-op rather than a refusal',
|
|
814
842
|
' would-empty the render holds no cases and the marked comment holds',
|
|
815
843
|
' some, so no body is printed and a person edits the comment',
|
|
816
844
|
'',
|
|
@@ -853,6 +881,12 @@ export function register(program: Command): void {
|
|
|
853
881
|
'branch with a checklist and no evidence image renders a marked body',
|
|
854
882
|
'holding the checklist, so a later call finds and edits that comment.',
|
|
855
883
|
'',
|
|
884
|
+
'--no-checklist drops the checklist the marked comment carries, so the',
|
|
885
|
+
'body renders without it and the record reports no checklist fields.',
|
|
886
|
+
'A marked comment holding a checklist still renders as ok rather than',
|
|
887
|
+
'no-evidence, so the comment is edited rather than left showing it.',
|
|
888
|
+
'Of --checklist and --no-checklist, the last one passed wins.',
|
|
889
|
+
'',
|
|
856
890
|
'--local adds a **Local preview:** segment after any hosted one on the',
|
|
857
891
|
'address line, or carries the line alone, and is carried forward the',
|
|
858
892
|
'same way. It does not by itself turn no-evidence into ok, so a',
|
|
@@ -891,9 +925,13 @@ export function register(program: Command): void {
|
|
|
891
925
|
' checklist the checklist between its delimiters, ticks included',
|
|
892
926
|
' boxes each checklist box with its number, tick, stamp, and taste',
|
|
893
927
|
' mark, the numbering `canon pr tick` takes',
|
|
928
|
+
' drivable how many boxes a driver can run, every box but one whose',
|
|
929
|
+
' trailing markers hold (taste), in any order. Zero binds no',
|
|
930
|
+
' UI review, so `canon pr lift` and `review-state` leave the',
|
|
931
|
+
' UI family unbound',
|
|
894
932
|
'These come from the comment already posted, never from the flags this',
|
|
895
933
|
'call passed. A checklist the caller posted raw after a refused render',
|
|
896
|
-
'carries no marker, so the record reports none of
|
|
934
|
+
'carries no marker, so the record reports none of these for it. An',
|
|
897
935
|
'unread thread refuses as gh-failed on both reasons rather than',
|
|
898
936
|
'reporting the fields absent.',
|
|
899
937
|
'',
|
|
@@ -907,6 +945,14 @@ export function register(program: Command): void {
|
|
|
907
945
|
'unchecked rather than clean. It never adds to `owed`, since no step of',
|
|
908
946
|
'the ship chain can take a capture.',
|
|
909
947
|
'',
|
|
948
|
+
'A rendered body carries the list as an **Uncaptured:** line under the',
|
|
949
|
+
'addresses, each path in a code span. A non-empty list opens a comment',
|
|
950
|
+
'the way a checklist does, so a head whose only news is an uncaptured',
|
|
951
|
+
'path reports ok rather than no-evidence. The line is computed on every',
|
|
952
|
+
'render and never carried, so it drops once a capture empties the list',
|
|
953
|
+
'or a reel filmed at the head renders beside it. `canon pr lift`',
|
|
954
|
+
'refuses `painted-uncaptured` on the same list.',
|
|
955
|
+
'',
|
|
910
956
|
'--check reads the same changed set and thread, renders no body, and',
|
|
911
957
|
'reports what the ship chain still owes the pull request:',
|
|
912
958
|
' settled nothing is owed',
|
|
@@ -917,8 +963,8 @@ export function register(program: Command): void {
|
|
|
917
963
|
'request changed, and the marked comment carries no **Preview:**',
|
|
918
964
|
'address. A failed deploy reads the same as a skipped one.',
|
|
919
965
|
'The record carries `owed` and the same marked fields. --check refuses',
|
|
920
|
-
'with --preview, --local, --checklist, --
|
|
921
|
-
'--reel-skip as check-writes.',
|
|
966
|
+
'with --preview, --local, --checklist, --no-checklist, --reel,',
|
|
967
|
+
'--local-skip, or --reel-skip as check-writes.',
|
|
922
968
|
'',
|
|
923
969
|
'Refusals, read off `reason`:',
|
|
924
970
|
' unreadable-checklist --checklist named a path that could not be read,',
|
|
@@ -973,7 +1019,9 @@ export function register(program: Command): void {
|
|
|
973
1019
|
'they are read:',
|
|
974
1020
|
' review the newest `## Review` is closed at the head, off its marker',
|
|
975
1021
|
' ui the newest `## UI review` is closed at the head, bound only',
|
|
976
|
-
' where `canon pr evidence --check` carries a checklist
|
|
1022
|
+
' where `canon pr evidence --check` carries a checklist whose',
|
|
1023
|
+
' `drivable` count is not zero, a missing count binding. An',
|
|
1024
|
+
' open UI review refuses `ui-open` whatever the count. A close',
|
|
977
1025
|
' on an earlier commit still holds when it is an ancestor of the',
|
|
978
1026
|
' head and every path changed since is one no box can observe,',
|
|
979
1027
|
' being tests, stories, snapshots, markdown in the root and in',
|
|
@@ -984,7 +1032,19 @@ export function register(program: Command): void {
|
|
|
984
1032
|
' a box drives behavior a `.ts` file holds too. A close whose',
|
|
985
1033
|
' count line shows boxes not driven and none passed, failed,',
|
|
986
1034
|
' or needing eyes refuses `ui-undriven`, carried or not',
|
|
987
|
-
' evidence the evidence check reads settled',
|
|
1035
|
+
' evidence the evidence check reads settled, and every painted path',
|
|
1036
|
+
' it lists as `uncaptured` is covered. A reel the marked',
|
|
1037
|
+
' comment embeds at the head covers every path, and a',
|
|
1038
|
+
' `- **Waived capture:** `<path>`: <reason>` line outside a code',
|
|
1039
|
+
' fence covers the path it names as the list spells it. Only',
|
|
1040
|
+
' the newest `## Review` and the newest `## UI review` count,',
|
|
1041
|
+
' each closed by its own marker and not dismissed, so a pass',
|
|
1042
|
+
' must restate a waiver for it to hold on a later head. A',
|
|
1043
|
+
' review with no pass heading waives nothing. Any other path',
|
|
1044
|
+
' refuses `painted-uncaptured`,',
|
|
1045
|
+
' the gate entry naming what is left as `uncaptured` and what',
|
|
1046
|
+
' a review waived as `waived`, each sorted. An absent list,',
|
|
1047
|
+
' in a project whose base keeps no captures, passes',
|
|
988
1048
|
' standing the head is fresh against the base, not stale or conflicted',
|
|
989
1049
|
' ci every required check run on the tip passed, waited on up to --wait',
|
|
990
1050
|
'',
|
|
@@ -1014,7 +1074,8 @@ export function register(program: Command): void {
|
|
|
1014
1074
|
' lifted, already-ready, redrafted, clear (--check)',
|
|
1015
1075
|
' pull-closed, review-missing, review-open, review-unmarked,',
|
|
1016
1076
|
' review-behind, ui-missing, ui-open, ui-unmarked, ui-behind,',
|
|
1017
|
-
' ui-undriven, evidence-owed,
|
|
1077
|
+
' ui-undriven, evidence-owed, painted-uncaptured, standing-stale,',
|
|
1078
|
+
' standing-conflict, standing-unknown,',
|
|
1018
1079
|
' ci-pending (--check), ci-failing, ci-conflicted, ci-timeout,',
|
|
1019
1080
|
' head-moved, gh-failed',
|
|
1020
1081
|
'',
|
|
@@ -1257,6 +1318,15 @@ export function register(program: Command): void {
|
|
|
1257
1318
|
"refusal names the lowest page port's first failing path. An absent",
|
|
1258
1319
|
'file or an empty list checks the page alone.',
|
|
1259
1320
|
'',
|
|
1321
|
+
'A page served by a static server answers none of those paths, so',
|
|
1322
|
+
'page-only = ["web"] lists repository-relative folders whose',
|
|
1323
|
+
'listeners skip them and are found on the page probe alone. The match',
|
|
1324
|
+
"reads the listener's working directory on whole path segments, so web",
|
|
1325
|
+
'does not cover webapp, and a server started from the root with a',
|
|
1326
|
+
'working-directory flag still takes the health check. Keep health',
|
|
1327
|
+
'beside page-only until the binary a project runs carries this key,',
|
|
1328
|
+
'since an older one refuses a file holding page-only alone.',
|
|
1329
|
+
'',
|
|
1260
1330
|
'A process in a sibling worktree, in the main checkout seen from a',
|
|
1261
1331
|
'linked worktree, or in a linked worktree seen from the main checkout',
|
|
1262
1332
|
'is never reported, since its server shows another branch. Sockets are',
|
|
@@ -1282,8 +1352,9 @@ export function register(program: Command): void {
|
|
|
1282
1352
|
' unhealthy a page answered but a declared health path did',
|
|
1283
1353
|
' not, with `port`, `path`, and `status`, which',
|
|
1284
1354
|
' is null on a timeout',
|
|
1285
|
-
' invalid-config canon/config/local.toml is not valid TOML,
|
|
1286
|
-
' health is not a list of paths starting with
|
|
1355
|
+
' invalid-config canon/config/local.toml is not valid TOML,',
|
|
1356
|
+
' health is not a list of paths starting with /,',
|
|
1357
|
+
' or page-only is not a list of relative folders',
|
|
1287
1358
|
' removed --remove dropped the line',
|
|
1288
1359
|
' no-comment --remove found no marked comment, a no-op',
|
|
1289
1360
|
' no-line --remove found no address line, a no-op',
|
|
@@ -1367,7 +1438,7 @@ export function register(program: Command): void {
|
|
|
1367
1438
|
' nothing written, with `driven`, `head`, and `drivenPaths`',
|
|
1368
1439
|
' stale-head --head is behind the tip with no readable linear gap,',
|
|
1369
1440
|
' as a force-push or a rebase leaves, nothing written',
|
|
1370
|
-
' taste-box a named box
|
|
1441
|
+
' taste-box a named box carries (taste), nothing written',
|
|
1371
1442
|
' no-box a named number is past the checklist, nothing written',
|
|
1372
1443
|
' no-checklist the evidence comment carries no checklist',
|
|
1373
1444
|
' no-comment no comment carries the evidence marker',
|
|
@@ -1821,6 +1892,8 @@ async function runKeyChanges(
|
|
|
1821
1892
|
const root = resolve(opts.root ?? process.cwd())
|
|
1822
1893
|
const emitJson = opts.json ?? false
|
|
1823
1894
|
|
|
1895
|
+
if (opts.link === true) return runKeyChangesLink(number, opts, root)
|
|
1896
|
+
|
|
1824
1897
|
intro('canon pr key-changes')
|
|
1825
1898
|
|
|
1826
1899
|
const source =
|
|
@@ -1905,6 +1978,14 @@ async function runKeyChanges(
|
|
|
1905
1978
|
for (const claim of report.unresolved) logInfo(claim.path)
|
|
1906
1979
|
}
|
|
1907
1980
|
|
|
1981
|
+
if (report.mislinked.length > 0) {
|
|
1982
|
+
logStep('Mislinked')
|
|
1983
|
+
logWarn(
|
|
1984
|
+
`${plural(report.mislinked.length, 'link')} open another file's diff than the path they name. Re-run with --link to print the body with them corrected.`,
|
|
1985
|
+
)
|
|
1986
|
+
for (const link of report.mislinked) logWarn(link.path)
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1908
1989
|
outro()
|
|
1909
1990
|
|
|
1910
1991
|
if (emitJson) {
|
|
@@ -1922,11 +2003,71 @@ async function runKeyChanges(
|
|
|
1922
2003
|
incidental: report.incidental,
|
|
1923
2004
|
unresolved: report.unresolved,
|
|
1924
2005
|
evidenceUnread: report.evidenceUnread,
|
|
2006
|
+
mislinked: report.mislinked,
|
|
1925
2007
|
})}\n`,
|
|
1926
2008
|
)
|
|
1927
2009
|
}
|
|
1928
2010
|
|
|
1929
|
-
return report.unmet.length === 0 ? 0 : 2
|
|
2011
|
+
return report.unmet.length === 0 && report.mislinked.length === 0 ? 0 : 2
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Prints the body with each changed path in `## Key changes` linked to its
|
|
2016
|
+
* diff, and nothing else, so stdout pipes straight into a body file.
|
|
2017
|
+
*
|
|
2018
|
+
* Skips the frame `intro` opens, which would land in the piped body. Refusals
|
|
2019
|
+
* keep the framed shape on stdout like every other mode.
|
|
2020
|
+
*/
|
|
2021
|
+
async function runKeyChangesLink(
|
|
2022
|
+
number: string | undefined,
|
|
2023
|
+
opts: KeyChangesOptions,
|
|
2024
|
+
root: string,
|
|
2025
|
+
): Promise<number> {
|
|
2026
|
+
const emitJson = opts.json ?? false
|
|
2027
|
+
|
|
2028
|
+
if (emitJson) {
|
|
2029
|
+
return refuseWith(
|
|
2030
|
+
'link-json',
|
|
2031
|
+
'--link prints the body itself, so it takes no --json record.',
|
|
2032
|
+
emitJson,
|
|
2033
|
+
root,
|
|
2034
|
+
)
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
const source =
|
|
2038
|
+
opts.body === undefined
|
|
2039
|
+
? await readFromApi(root, number)
|
|
2040
|
+
: await readFromFile(root, process.cwd(), opts.body, opts.base)
|
|
2041
|
+
if (source.kind === 'refused') return refuse(source.reason, emitJson, root)
|
|
2042
|
+
|
|
2043
|
+
const pull = number ?? source.source.number?.toString()
|
|
2044
|
+
if (pull === undefined) {
|
|
2045
|
+
return refuseWith(
|
|
2046
|
+
'no-number',
|
|
2047
|
+
'The link needs the pull request number, so pass it as the argument.',
|
|
2048
|
+
emitJson,
|
|
2049
|
+
root,
|
|
2050
|
+
)
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
const url = await readRepoUrl(root)
|
|
2054
|
+
if (url === undefined) {
|
|
2055
|
+
return refuseWith(
|
|
2056
|
+
'no-repo-url',
|
|
2057
|
+
'gh could not read the repository address, so no link was built.',
|
|
2058
|
+
emitJson,
|
|
2059
|
+
root,
|
|
2060
|
+
)
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
process.stdout.write(
|
|
2064
|
+
linkKeyChanges(
|
|
2065
|
+
source.source.body,
|
|
2066
|
+
source.source.changed,
|
|
2067
|
+
`${url}/pull/${pull}/files`,
|
|
2068
|
+
),
|
|
2069
|
+
)
|
|
2070
|
+
return 0
|
|
1930
2071
|
}
|
|
1931
2072
|
|
|
1932
2073
|
type IdentityRead =
|
|
@@ -2065,7 +2206,7 @@ async function listReviews(cwd: string, number: string) {
|
|
|
2065
2206
|
return listPages<RestReview>(
|
|
2066
2207
|
cwd,
|
|
2067
2208
|
`repos/{owner}/{repo}/pulls/${number}/reviews`,
|
|
2068
|
-
'id, body, commit_id, submitted_at',
|
|
2209
|
+
'id, body, commit_id, submitted_at, state',
|
|
2069
2210
|
)
|
|
2070
2211
|
}
|
|
2071
2212
|
|
|
@@ -2439,7 +2580,9 @@ function logReviewState(number: number, record: ReviewStateRecord): void {
|
|
|
2439
2580
|
else logWarn(line)
|
|
2440
2581
|
}
|
|
2441
2582
|
if (!record.ui.isBound) {
|
|
2442
|
-
logInfo(
|
|
2583
|
+
logInfo(
|
|
2584
|
+
'ui: the evidence carries no box a driver can run, so the family is unbound',
|
|
2585
|
+
)
|
|
2443
2586
|
}
|
|
2444
2587
|
}
|
|
2445
2588
|
|
|
@@ -2448,14 +2591,18 @@ interface PullFiles {
|
|
|
2448
2591
|
readonly present: ReadonlyMap<string, boolean>
|
|
2449
2592
|
/** Each path the pull request removed, which a deploy's push filter still counts. */
|
|
2450
2593
|
readonly removed: readonly string[]
|
|
2594
|
+
/** Each renamed path mapped to the path it sat at before the pull request. */
|
|
2595
|
+
readonly renamedFrom: ReadonlyMap<string, string>
|
|
2451
2596
|
}
|
|
2452
2597
|
|
|
2453
2598
|
/**
|
|
2454
2599
|
* The paths a pull request changed, read through the paginated files endpoint
|
|
2455
2600
|
* so the set is the same from any checkout and never capped at the first
|
|
2456
|
-
* view. `
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
2601
|
+
* view. `copied` counts as added, since a copy leaves its source in place and
|
|
2602
|
+
* has no single base. `renamed` counts as added in `present` too, so the
|
|
2603
|
+
* changed set stays the same, and `renamedFrom` carries the old path of each
|
|
2604
|
+
* rename for a reader that compares against the base. Returns undefined when
|
|
2605
|
+
* the read fails, which refuses rather than rendering a short set.
|
|
2459
2606
|
*/
|
|
2460
2607
|
async function listPullFiles(
|
|
2461
2608
|
cwd: string,
|
|
@@ -2466,18 +2613,22 @@ async function listPullFiles(
|
|
|
2466
2613
|
'--paginate',
|
|
2467
2614
|
`repos/{owner}/{repo}/pulls/${number}/files`,
|
|
2468
2615
|
'--jq',
|
|
2469
|
-
'.[] | [.status, .filename] | @tsv',
|
|
2616
|
+
'.[] | [.status, .filename, (.previous_filename // "")] | @tsv',
|
|
2470
2617
|
])
|
|
2471
2618
|
if (stdout === null) return undefined
|
|
2472
2619
|
const present = new Map<string, boolean>()
|
|
2473
2620
|
const removed: string[] = []
|
|
2621
|
+
const renamedFrom = new Map<string, string>()
|
|
2474
2622
|
for (const line of stdout.split('\n').filter(Boolean)) {
|
|
2475
|
-
const [status, path] = line.split('\t')
|
|
2623
|
+
const [status, path, previous] = line.split('\t')
|
|
2476
2624
|
if (status === undefined || path === undefined) continue
|
|
2477
2625
|
if (status === 'removed') removed.push(path)
|
|
2478
2626
|
else present.set(path, status !== 'modified' && status !== 'changed')
|
|
2627
|
+
if (status === 'renamed' && previous !== undefined && previous !== '') {
|
|
2628
|
+
renamedFrom.set(path, previous)
|
|
2629
|
+
}
|
|
2479
2630
|
}
|
|
2480
|
-
return { present, removed }
|
|
2631
|
+
return { present, removed, renamedFrom }
|
|
2481
2632
|
}
|
|
2482
2633
|
|
|
2483
2634
|
function everyChangedPath(files: PullFiles): string[] {
|
|
@@ -2625,13 +2776,14 @@ async function measureItem(
|
|
|
2625
2776
|
head: string,
|
|
2626
2777
|
): Promise<MeasuredItem> {
|
|
2627
2778
|
const headRead = await readImage(cwd, item.path, head)
|
|
2779
|
+
const basePath = item.basePath ?? item.path
|
|
2628
2780
|
const baseRead =
|
|
2629
2781
|
item.added || base === undefined
|
|
2630
2782
|
? undefined
|
|
2631
|
-
: await readImage(cwd,
|
|
2783
|
+
: await readImage(cwd, basePath, base)
|
|
2632
2784
|
const unread = [
|
|
2633
2785
|
...(headRead.kind === 'failed' ? [`head:${item.path}`] : []),
|
|
2634
|
-
...(baseRead?.kind === 'failed' ? [`base:${
|
|
2786
|
+
...(baseRead?.kind === 'failed' ? [`base:${basePath}`] : []),
|
|
2635
2787
|
]
|
|
2636
2788
|
const headBytes = headRead.kind === 'read' ? headRead.bytes : undefined
|
|
2637
2789
|
const baseBytes = baseRead?.kind === 'read' ? baseRead.bytes : undefined
|
|
@@ -2645,6 +2797,9 @@ async function measureItem(
|
|
|
2645
2797
|
...(headBytes !== undefined &&
|
|
2646
2798
|
baseBytes !== undefined && {
|
|
2647
2799
|
withinTolerance: isWithinTolerance(baseBytes, headBytes),
|
|
2800
|
+
...(item.basePath !== undefined && {
|
|
2801
|
+
identical: Buffer.from(baseBytes).equals(headBytes),
|
|
2802
|
+
}),
|
|
2648
2803
|
}),
|
|
2649
2804
|
},
|
|
2650
2805
|
unread,
|
|
@@ -2708,7 +2863,7 @@ async function runEvidence(
|
|
|
2708
2863
|
// Read ahead of anything else, so a bad path refuses before a `gh` round
|
|
2709
2864
|
// trip rather than after one.
|
|
2710
2865
|
let suppliedChecklist: string | undefined
|
|
2711
|
-
if (opts.checklist
|
|
2866
|
+
if (typeof opts.checklist === 'string') {
|
|
2712
2867
|
suppliedChecklist = await readFile(resolve(root, opts.checklist), 'utf8')
|
|
2713
2868
|
.then((text) => text.trim() || undefined)
|
|
2714
2869
|
.catch(() => undefined)
|
|
@@ -2765,6 +2920,7 @@ async function runEvidence(
|
|
|
2765
2920
|
const grouped = await groupEvidence(
|
|
2766
2921
|
[...pullFiles.present.keys()],
|
|
2767
2922
|
async (path) => pullFiles.present.get(path) === false,
|
|
2923
|
+
pullFiles.renamedFrom,
|
|
2768
2924
|
)
|
|
2769
2925
|
const uncaptured = readUncaptured(
|
|
2770
2926
|
everyChangedPath(pullFiles),
|
|
@@ -2794,7 +2950,10 @@ async function runEvidence(
|
|
|
2794
2950
|
commentId = findEvidenceCommentId(comments)
|
|
2795
2951
|
existingCases = findEvidenceCaseCount(comments)
|
|
2796
2952
|
carriedPreview = findEvidencePreview(comments)
|
|
2797
|
-
|
|
2953
|
+
// A dropped checklist is not carried, so neither the record nor the render
|
|
2954
|
+
// shows it, while the marked comment holding it still counts below.
|
|
2955
|
+
const postedChecklist = findEvidenceChecklist(comments)
|
|
2956
|
+
carriedChecklist = opts.checklist === false ? undefined : postedChecklist
|
|
2798
2957
|
carriedLocal = findEvidenceLocal(comments)
|
|
2799
2958
|
const carriedReel = findEvidenceReel(comments)
|
|
2800
2959
|
|
|
@@ -2804,10 +2963,7 @@ async function runEvidence(
|
|
|
2804
2963
|
...(carriedPreview !== undefined && { preview: carriedPreview }),
|
|
2805
2964
|
...(carriedLocal !== undefined && { local: carriedLocal }),
|
|
2806
2965
|
...(carriedReel !== undefined && { reel: carriedReel }),
|
|
2807
|
-
...(carriedChecklist !== undefined &&
|
|
2808
|
-
checklist: carriedChecklist,
|
|
2809
|
-
boxes: readChecklistBoxes(carriedChecklist),
|
|
2810
|
-
}),
|
|
2966
|
+
...(carriedChecklist !== undefined && checklistFields(carriedChecklist)),
|
|
2811
2967
|
}
|
|
2812
2968
|
|
|
2813
2969
|
// A deploy that resolves and does not build from any changed path makes a
|
|
@@ -2866,13 +3022,18 @@ async function runEvidence(
|
|
|
2866
3022
|
// A checklist opens a comment on its own, so it always lands in the marked
|
|
2867
3023
|
// comment a later call edits. A local address rides on a checklist or a
|
|
2868
3024
|
// hosted preview and never opens one alone, so a docs-only branch with a
|
|
2869
|
-
// server up posts nothing.
|
|
3025
|
+
// server up posts nothing. An uncaptured painted path opens one too, so the
|
|
3026
|
+
// gap the lift refuses on shows on the thread.
|
|
3027
|
+
// A dropped checklist still opens a render, so the marked comment holding
|
|
3028
|
+
// it is edited without the block rather than left showing it.
|
|
2870
3029
|
const hasChecklist =
|
|
2871
|
-
suppliedChecklist !== undefined ||
|
|
3030
|
+
suppliedChecklist !== undefined || postedChecklist !== undefined
|
|
3031
|
+
const hasUncaptured = uncaptured !== undefined && uncaptured.length > 0
|
|
2872
3032
|
if (
|
|
2873
3033
|
grouped.kind === 'refused' &&
|
|
2874
3034
|
opts.preview === undefined &&
|
|
2875
|
-
!hasChecklist
|
|
3035
|
+
!hasChecklist &&
|
|
3036
|
+
!hasUncaptured
|
|
2876
3037
|
) {
|
|
2877
3038
|
logStep('Skipped')
|
|
2878
3039
|
logInfo(
|
|
@@ -2933,6 +3094,7 @@ async function runEvidence(
|
|
|
2933
3094
|
local,
|
|
2934
3095
|
reel,
|
|
2935
3096
|
skips,
|
|
3097
|
+
uncaptured,
|
|
2936
3098
|
)
|
|
2937
3099
|
|
|
2938
3100
|
const caseCount = states.reduce((n, s) => n + s.items.length, 0)
|
|
@@ -3030,6 +3192,16 @@ async function runEvidence(
|
|
|
3030
3192
|
return 0
|
|
3031
3193
|
}
|
|
3032
3194
|
|
|
3195
|
+
/** The record fields a checklist carries, with `drivable` counting the boxes a driver can run. */
|
|
3196
|
+
function checklistFields(checklist: string): {
|
|
3197
|
+
checklist: string
|
|
3198
|
+
boxes: ChecklistBox[]
|
|
3199
|
+
drivable: number
|
|
3200
|
+
} {
|
|
3201
|
+
const boxes = readChecklistBoxes(checklist)
|
|
3202
|
+
return { checklist, boxes, drivable: countDrivable(boxes) }
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3033
3205
|
async function readWorkflows(root: string): Promise<WorkflowFile[]> {
|
|
3034
3206
|
const dir = join(root, '.github', 'workflows')
|
|
3035
3207
|
if (!existsSync(dir)) return []
|
|
@@ -3350,6 +3522,7 @@ async function runLocal(opts: LocalOptions): Promise<number> {
|
|
|
3350
3522
|
worktree,
|
|
3351
3523
|
machineLocalRunner(),
|
|
3352
3524
|
declaration.kind === 'paths' ? declaration.paths : [],
|
|
3525
|
+
declaration.pageOnly,
|
|
3353
3526
|
)
|
|
3354
3527
|
if (result.kind === 'refused' && result.reason === 'unhealthy') {
|
|
3355
3528
|
const { port, path, status } = result
|
|
@@ -3966,6 +4139,18 @@ async function readRepoName(root: string): Promise<string | undefined> {
|
|
|
3966
4139
|
}
|
|
3967
4140
|
}
|
|
3968
4141
|
|
|
4142
|
+
/** The repository's web address off the REST repository read, for the same reason as `readRepoName`. */
|
|
4143
|
+
async function readRepoUrl(root: string): Promise<string | undefined> {
|
|
4144
|
+
const row = await gh(root, ['api', 'repos/{owner}/{repo}'])
|
|
4145
|
+
if (row === null) return undefined
|
|
4146
|
+
try {
|
|
4147
|
+
const url = (JSON.parse(row) as { html_url?: unknown }).html_url
|
|
4148
|
+
return typeof url === 'string' && url !== '' ? url : undefined
|
|
4149
|
+
} catch {
|
|
4150
|
+
return undefined
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
|
|
3969
4154
|
async function runFrames(
|
|
3970
4155
|
number: string | undefined,
|
|
3971
4156
|
opts: FramesOptions,
|
|
@@ -4122,7 +4307,7 @@ const LIFT_REFUSALS: Record<LiftRefusal | 'bad-wait' | 'gh-missing', string> = {
|
|
|
4122
4307
|
'review-behind':
|
|
4123
4308
|
'The newest code review closed on an earlier commit than the head.',
|
|
4124
4309
|
'ui-missing':
|
|
4125
|
-
'The evidence comment carries a checklist and no UI review has been posted.',
|
|
4310
|
+
'The evidence comment carries a checklist holding a box a driver can run, and no UI review has been posted.',
|
|
4126
4311
|
'ui-open': 'The newest UI review is open, so a box still owes a fix.',
|
|
4127
4312
|
'ui-unmarked':
|
|
4128
4313
|
'The newest UI review carries no head marker, so the commit it covers is a GitHub stamp this gate does not trust.',
|
|
@@ -4132,6 +4317,8 @@ const LIFT_REFUSALS: Record<LiftRefusal | 'bad-wait' | 'gh-missing', string> = {
|
|
|
4132
4317
|
'The newest UI review closed having driven no box, so it looked at nothing. Re-drive the checklist on an address that answers.',
|
|
4133
4318
|
'evidence-owed':
|
|
4134
4319
|
'The evidence check reads owed, so the ship chain left a step undone.',
|
|
4320
|
+
'painted-uncaptured':
|
|
4321
|
+
'A painted path changed with no capture, no reel at the head, and no waiver in a review. Capture it, or have a review waive it.',
|
|
4135
4322
|
'standing-stale':
|
|
4136
4323
|
'The base moved under a path this branch writes since it branched, so CI measured a tree that is gone.',
|
|
4137
4324
|
'standing-conflict': 'The head does not merge cleanly into the base.',
|
|
@@ -4235,7 +4422,14 @@ async function readEvidenceGate(
|
|
|
4235
4422
|
.at(-1)
|
|
4236
4423
|
if (last === undefined) return undefined
|
|
4237
4424
|
|
|
4238
|
-
let record: {
|
|
4425
|
+
let record: {
|
|
4426
|
+
reason?: unknown
|
|
4427
|
+
owed?: unknown
|
|
4428
|
+
checklist?: unknown
|
|
4429
|
+
drivable?: unknown
|
|
4430
|
+
uncaptured?: unknown
|
|
4431
|
+
reel?: unknown
|
|
4432
|
+
}
|
|
4239
4433
|
try {
|
|
4240
4434
|
record = JSON.parse(last)
|
|
4241
4435
|
} catch {
|
|
@@ -4243,12 +4437,27 @@ async function readEvidenceGate(
|
|
|
4243
4437
|
}
|
|
4244
4438
|
if (record.reason !== 'settled' && record.reason !== 'owed') return undefined
|
|
4245
4439
|
|
|
4440
|
+
const reelHead = readReelHead(record.reel)
|
|
4246
4441
|
return {
|
|
4247
4442
|
isSettled: record.reason === 'settled',
|
|
4248
4443
|
owed: Array.isArray(record.owed) ? record.owed.map(String) : [],
|
|
4249
|
-
|
|
4250
|
-
|
|
4444
|
+
hasDrivableBox: readsDrivableBox(record),
|
|
4445
|
+
// Absent stays absent, since it means the base keeps no captures and
|
|
4446
|
+
// the gate passes as it did before the list existed.
|
|
4447
|
+
...(Array.isArray(record.uncaptured) && {
|
|
4448
|
+
uncaptured: record.uncaptured.map(String),
|
|
4449
|
+
}),
|
|
4450
|
+
...(reelHead !== undefined && { reelHead }),
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
function readReelHead(reel: unknown): string | undefined {
|
|
4455
|
+
if (typeof reel !== 'object' || reel === null || !('head' in reel)) {
|
|
4456
|
+
return undefined
|
|
4251
4457
|
}
|
|
4458
|
+
return typeof reel.head === 'string' && reel.head !== ''
|
|
4459
|
+
? reel.head
|
|
4460
|
+
: undefined
|
|
4252
4461
|
}
|
|
4253
4462
|
|
|
4254
4463
|
async function readChecksGate(
|