@erclx/canon 5.26.0 → 5.28.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/README.md +2 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +2 -0
- package/claude/skills/auto-ship/SKILL.md +3 -11
- package/claude/skills/auto-ship/references/review-findings.md +20 -0
- package/claude/skills/backlog-triage/SKILL.md +2 -0
- package/claude/skills/bash-cli-script/SKILL.md +2 -0
- package/claude/skills/build-in-slices/SKILL.md +2 -0
- package/claude/skills/canon-cli/SKILL.md +2 -0
- package/claude/skills/canon-feedback/SKILL.md +2 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +2 -0
- package/claude/skills/canon-operator/SKILL.md +2 -0
- package/claude/skills/canon-rollout/SKILL.md +3 -11
- package/claude/skills/canon-rollout/references/failure-recovery.md +20 -0
- package/claude/skills/canvas/SKILL.md +13 -0
- package/claude/skills/ci-workflow/SKILL.md +2 -0
- package/claude/skills/code-craft/SKILL.md +2 -0
- package/claude/skills/codebase-layout/SKILL.md +2 -0
- package/claude/skills/context-fold/SKILL.md +3 -17
- package/claude/skills/context-fold/references/completion-report.md +26 -0
- package/claude/skills/create-rule/SKILL.md +2 -0
- package/claude/skills/create-skill/SKILL.md +2 -0
- package/claude/skills/create-standard/SKILL.md +2 -0
- package/claude/skills/decision-escalate/SKILL.md +2 -0
- package/claude/skills/deploy-app/SKILL.md +2 -0
- package/claude/skills/deprecation-migration/SKILL.md +2 -0
- package/claude/skills/design-extract/SKILL.md +2 -0
- package/claude/skills/design-taste/SKILL.md +2 -0
- package/claude/skills/docs-sync/SKILL.md +2 -0
- package/claude/skills/document-health/SKILL.md +2 -0
- package/claude/skills/draft-and-pick/SKILL.md +2 -0
- package/claude/skills/draft-doc/SKILL.md +2 -0
- package/claude/skills/draft-doc/references/wiki.md +1 -1
- package/claude/skills/draft-figure/SKILL.md +2 -0
- package/claude/skills/draft-identity/SKILL.md +2 -0
- package/claude/skills/draft-ready/SKILL.md +5 -1
- package/claude/skills/draft-screencast/SKILL.md +2 -0
- package/claude/skills/draft-slides/SKILL.md +2 -0
- package/claude/skills/git-branch/SKILL.md +2 -0
- package/claude/skills/git-commit/SKILL.md +2 -0
- package/claude/skills/git-followup/SKILL.md +2 -0
- package/claude/skills/git-issue/SKILL.md +2 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/evidence.md +7 -7
- package/claude/skills/git-ship/SKILL.md +2 -0
- package/claude/skills/git-split/SKILL.md +2 -0
- package/claude/skills/git-stage/SKILL.md +2 -0
- package/claude/skills/git-worktree/SKILL.md +2 -0
- package/claude/skills/index-lookup/SKILL.md +2 -0
- package/claude/skills/markdown-propose/SKILL.md +2 -0
- package/claude/skills/memory-capture/SKILL.md +2 -0
- package/claude/skills/memory-review/SKILL.md +2 -0
- package/claude/skills/plan-feature/SKILL.md +2 -0
- package/claude/skills/plan-groundwork/SKILL.md +2 -0
- package/claude/skills/plan-intake/SKILL.md +2 -0
- package/claude/skills/plan-intake-answer/SKILL.md +2 -0
- package/claude/skills/project-commands/SKILL.md +2 -0
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +2 -0
- package/claude/skills/repo-metadata/SKILL.md +2 -0
- package/claude/skills/restate-plainly/SKILL.md +2 -0
- package/claude/skills/review-address/REQUIREMENT.md +4 -2
- package/claude/skills/review-address/SKILL.md +19 -14
- package/claude/skills/review-address/references/post-review-findings.md +16 -0
- package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
- package/claude/skills/review-branch/SKILL.md +2 -0
- package/claude/skills/review-craft/SKILL.md +2 -0
- package/claude/skills/review-craft/references/ui.md +6 -6
- package/claude/skills/review-pr/SKILL.md +4 -8
- package/claude/skills/review-pr/references/body-variants.md +52 -0
- package/claude/skills/review-pr/references/key-changes.md +16 -0
- package/claude/skills/review-pr/references/post.md +15 -43
- package/claude/skills/review-ui/REQUIREMENT.md +2 -2
- package/claude/skills/review-ui/SKILL.md +16 -12
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +13 -3
- package/claude/skills/review-ui/references/tick.md +22 -0
- package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
- package/claude/skills/role-orchestrator/SKILL.md +31 -42
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +13 -3
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +6 -34
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +12 -23
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
- package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +4 -2
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +3 -1
- package/claude/skills/role-worker/SKILL.md +5 -2
- package/claude/skills/search-craft/SKILL.md +2 -0
- package/claude/skills/seed-sync/SKILL.md +2 -0
- package/claude/skills/session-compact/SKILL.md +2 -0
- package/claude/skills/session-map/SKILL.md +2 -0
- package/claude/skills/session-relay/SKILL.md +2 -0
- package/claude/skills/session-resume/SKILL.md +2 -0
- package/claude/skills/session-worktree/SKILL.md +3 -11
- package/claude/skills/session-worktree/references/install-command.md +20 -0
- package/claude/skills/sketch-design/SKILL.md +2 -0
- package/claude/skills/systematic-debugging/SKILL.md +2 -0
- package/claude/skills/target-check/SKILL.md +2 -0
- package/claude/skills/target-setup/SKILL.md +2 -0
- package/claude/skills/task-board/SKILL.md +2 -0
- package/claude/skills/teach-workspace/SKILL.md +5 -15
- package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
- package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
- package/claude/skills/teach-workspace/references/revisit.md +22 -0
- package/claude/skills/test-craft/SKILL.md +2 -0
- package/claude/skills/test-first/SKILL.md +2 -0
- package/claude/skills/ui-checklist/SKILL.md +3 -1
- package/claude/skills/ux-audit/SKILL.md +2 -0
- package/claude/skills/ux-measure/SKILL.md +2 -0
- package/claude/skills/ux-walkthrough/SKILL.md +2 -0
- package/claude/skills/video-craft/SKILL.md +2 -0
- package/claude/skills/write-human/SKILL.md +2 -0
- package/claude/skills/youtube-transcripts/SKILL.md +2 -0
- package/docs/agents/canvas.md +24 -8
- package/docs/agents/capture.md +5 -5
- package/docs/agents/commands.md +2 -2
- package/docs/agents/gate.md +3 -1
- package/docs/agents/index.md +5 -4
- package/docs/agents/install-and-sync.md +7 -0
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/pr-evidence.md +47 -23
- package/docs/agents/pr-local.md +19 -13
- package/docs/agents/pr-preview.md +44 -21
- package/docs/agents/pr-tick.md +72 -0
- package/docs/agents/sandbox.md +30 -6
- package/docs/agents/scripting.md +8 -1
- package/docs/agents/skills-audit.md +2 -1
- package/docs/agents/tasks-plans.md +1 -1
- package/docs/agents/tasks-validate.md +18 -18
- package/docs/agents/tasks.md +1 -1
- package/docs/workflow/operating-model.md +15 -21
- package/docs/workflow/parallel-features.md +24 -3
- package/docs/workflow/skill-map.md +3 -1
- package/docs/workflow/zshrc-aliases.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-hero.ts +1 -1
- package/scripts/lib/sandbox-path.sh +6 -162
- package/src/audits/baseline.ts +1 -1
- package/src/audits/run.ts +2 -2
- package/src/canvas/address.ts +12 -3
- package/src/canvas/capture.ts +1 -1
- package/src/canvas/client/inspector/align-grid.tsx +162 -0
- package/src/canvas/client/inspector/color-field.tsx +201 -171
- package/src/canvas/client/inspector/color-inputs.tsx +139 -0
- package/src/canvas/client/inspector/color-picker.tsx +668 -0
- package/src/canvas/client/inspector/color-space.ts +240 -0
- package/src/canvas/client/inspector/field.tsx +66 -3
- package/src/canvas/client/inspector/fields.css +473 -6
- package/src/canvas/client/inspector/size-field.tsx +69 -0
- package/src/canvas/client/inspector/token-select.tsx +109 -0
- package/src/canvas/client/inspector/values.ts +117 -3
- package/src/canvas/client/inspector.tsx +311 -15
- package/src/canvas/client/keys.ts +54 -0
- package/src/canvas/client/selection.tsx +199 -0
- package/src/canvas/client/shell.css +210 -12
- package/src/canvas/client/state.ts +165 -14
- package/src/canvas/client/surface.tsx +341 -39
- package/src/canvas/client/tools.tsx +64 -0
- package/src/canvas/content.ts +51 -2
- package/src/canvas/edit.ts +49 -6
- package/src/canvas/editing.ts +203 -0
- package/src/canvas/global.d.ts +15 -0
- package/src/canvas/server.ts +149 -12
- package/src/canvas/tokens.ts +60 -18
- package/src/census/count.ts +1 -1
- package/src/claude/seeds.ts +3 -3
- package/src/claude/skills-audit.ts +28 -0
- package/src/claude/skills-drift.ts +1 -1
- package/src/claude/skills-families.ts +36 -0
- package/src/claude/skills-list.ts +76 -15
- package/src/claude/skills-reach.ts +1 -1
- package/src/commands/audits.ts +2 -2
- package/src/commands/canvas.ts +180 -3
- package/src/commands/claude.ts +70 -12
- package/src/commands/design.ts +145 -5
- package/src/commands/docs.ts +1 -1
- package/src/commands/feedback.ts +2 -2
- package/src/commands/gate.ts +9 -2
- package/src/commands/gov.ts +3 -3
- package/src/commands/hooks.ts +1 -1
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +2 -2
- package/src/commands/intake.ts +1 -1
- package/src/commands/migrate.ts +2 -2
- package/src/commands/pr.ts +288 -65
- package/src/commands/records.ts +1 -1
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +64 -15
- package/src/commands/sessions.ts +1 -1
- package/src/commands/slides.ts +1 -1
- package/src/commands/sync.ts +2 -2
- package/src/commands/tasks.ts +21 -4
- package/src/commands/teach.ts +1 -1
- package/src/commands/tooling.ts +1 -1
- package/src/commands/transcripts.ts +2 -2
- package/src/commands/upgrade.ts +1 -1
- package/src/commands/wiki.ts +1 -1
- package/src/commands/worktrees.ts +1 -1
- package/src/comments/scan.ts +1 -1
- package/src/comments/trend.ts +1 -1
- package/src/context/architecture.ts +1 -1
- package/src/context/citations.ts +3 -3
- package/src/context/classify/extract.ts +3 -3
- package/src/context/folders.ts +2 -2
- package/src/context/requirements.ts +1 -1
- package/src/design/project-fonts.ts +392 -0
- package/src/docs/read.ts +1 -1
- package/src/exec.ts +1 -1
- package/src/feedback/github.ts +2 -2
- package/src/gate/measures.ts +48 -10
- package/src/gate/sequencer.ts +56 -4
- package/src/gate/stages.ts +17 -0
- package/src/gate/suite-lock.ts +214 -0
- package/src/{git-files.ts → git/files.ts} +2 -2
- package/src/{git-ignore.ts → git/ignore.ts} +1 -1
- package/src/{worktree.ts → git/worktree.ts} +1 -1
- package/src/gov/citations.ts +1 -1
- package/src/gov/install.ts +1 -1
- package/src/gov/superseded.ts +1 -1
- package/src/gov/test-order.ts +2 -2
- package/src/help.ts +1 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/indexes/walk.ts +1 -1
- package/src/intake/folder.ts +1 -1
- package/src/labels/audit.ts +1 -1
- package/src/labels/map.ts +1 -1
- package/src/labels/phase.ts +3 -3
- package/src/markdown/files.ts +1 -1
- package/src/migrate/apply.ts +1 -1
- package/src/migrate/record-layout.ts +1 -1
- package/src/migrate/records.ts +11 -11
- package/src/migrate/scratch-evidence.ts +1 -1
- package/src/migrate/surface-roots.ts +6 -6
- package/src/pr/bijection.ts +1 -1
- package/src/pr/evidence.ts +250 -59
- package/src/pr/local.ts +16 -10
- package/src/pr/preview.ts +78 -2
- package/src/pr/tick.ts +56 -0
- package/src/process/harness.ts +1 -1
- package/src/records/backup.ts +2 -2
- package/src/records/ordinal.ts +1 -1
- package/src/records/prune.ts +1 -1
- package/src/records/size.ts +1 -1
- package/src/records/stale-canonical.ts +2 -2
- package/src/records/stale.ts +1 -1
- package/src/records/validate.ts +2 -2
- package/src/{project-root.ts → roots/project.ts} +1 -1
- package/src/{record-root.ts → roots/record.ts} +1 -1
- package/src/sandbox/coverage.ts +11 -12
- package/src/sandbox/equivalence.ts +57 -26
- package/src/sandbox/headless/dispatch.ts +264 -0
- package/src/sandbox/headless/run.ts +653 -0
- package/src/sandbox/headless/watch.ts +145 -0
- package/src/sandbox/hooks.ts +1 -1
- package/src/sandbox/provision.ts +90 -29
- package/src/sandbox/scenario.ts +588 -0
- package/src/sandbox/tree.ts +4 -8
- package/src/secrets/scan.ts +1 -1
- package/src/sessions/claim.ts +1 -1
- package/src/sessions/live.ts +4 -1
- package/src/sessions/resolve.ts +1 -1
- package/src/sessions/transfer/bundle.ts +2 -2
- package/src/slides/decks.ts +1 -1
- package/src/slides/package/fonts.ts +6 -2
- package/src/standards/audit.ts +2 -2
- package/src/standards/read.ts +1 -1
- package/src/sync/check.ts +2 -2
- package/src/sync/engine.ts +3 -3
- package/src/sync/git.ts +1 -1
- package/src/sync/history.ts +1 -1
- package/src/sync/layout.ts +2 -2
- package/src/sync/reverse.ts +1 -1
- package/src/sync/stamp.ts +1 -1
- package/src/sync/target.ts +1 -1
- package/src/sync/workflow.ts +1 -1
- package/src/targets/pulls.ts +2 -2
- package/src/targets/sweep.ts +2 -2
- package/src/{target.ts → targets/validate.ts} +1 -1
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/label.ts +1 -1
- package/src/tasks/reach.ts +2 -2
- package/src/tasks/trunk.ts +1 -1
- package/src/tasks/validate.ts +38 -14
- package/src/teach/nav.ts +1 -1
- package/src/teach/workspace.ts +1 -1
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/read.ts +1 -1
- package/src/tooling/scan.ts +1 -1
- package/src/tooling/subfolder.ts +1 -1
- package/src/version/installed.ts +1 -1
- package/src/version/skew.ts +1 -1
- package/src/web/readme-citations.ts +1 -1
- package/src/worktrees/bare-flag.ts +1 -1
- package/src/worktrees/reclaim.ts +2 -2
- package/src/worktrees/remove.ts +1 -1
- package/standards/board.md +3 -3
- package/standards/canvas.md +2 -1
- package/standards/plan.md +13 -7
- package/standards/skill.md +1 -0
- package/standards/wiki.md +12 -3
- package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
- package/tsconfig.json +1 -1
- package/scripts/lib/sandbox-dispatch.sh +0 -190
- /package/src/{client-commands.ts → gate/client-commands.ts} +0 -0
- /package/src/{git-base.ts → git/base.ts} +0 -0
- /package/src/{git-env.ts → git/env.ts} +0 -0
- /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
- /package/src/{surface-root.ts → roots/surface.ts} +0 -0
- /package/src/{copy.ts → sync/copy.ts} +0 -0
package/src/pr/evidence.ts
CHANGED
|
@@ -10,18 +10,36 @@ const SHORT_SHA_LENGTH = 7
|
|
|
10
10
|
/** Prefix of the trailing marker this module writes and reads back. */
|
|
11
11
|
const MARKER_PREFIX = '<!-- pr-evidence:'
|
|
12
12
|
|
|
13
|
-
/** How the
|
|
13
|
+
/** How the address line names the branch's preview deployment. */
|
|
14
14
|
const PREVIEW_PREFIX = '**Preview:**'
|
|
15
15
|
|
|
16
|
-
/** How the
|
|
17
|
-
|
|
16
|
+
/** How the address line names the branch's server on the operator's own machine, after any hosted segment. */
|
|
17
|
+
const LOCAL_PREFIX = '**Local preview:**'
|
|
18
|
+
|
|
19
|
+
/** What joins the hosted and local segments on the one address line. */
|
|
20
|
+
const ADDRESS_SEPARATOR = ' · '
|
|
21
|
+
|
|
22
|
+
/** The heading every body opens with. */
|
|
23
|
+
const EVIDENCE_HEADING = '## Evidence'
|
|
24
|
+
|
|
25
|
+
/** How the visible commit line opens. */
|
|
26
|
+
const BASE_PREFIX = '**Base:**'
|
|
18
27
|
|
|
19
28
|
/** The heading the checklist sits under, printed outside the delimiters so a carried checklist never doubles it. */
|
|
20
29
|
const CHECKLIST_HEADING = '## What to look at'
|
|
21
30
|
|
|
22
31
|
/** Delimiters around the checklist, so a re-render can read back what a reviewer has already ticked. */
|
|
23
|
-
const CHECKLIST_START = '<!-- pr-checklist:start -->'
|
|
24
|
-
const CHECKLIST_END = '<!-- pr-checklist:end -->'
|
|
32
|
+
export const CHECKLIST_START = '<!-- pr-checklist:start -->'
|
|
33
|
+
export const CHECKLIST_END = '<!-- pr-checklist:end -->'
|
|
34
|
+
|
|
35
|
+
/** One checklist line, a list marker then a box that GitHub renders for `x` in either case. */
|
|
36
|
+
const BOX_LINE = /^(\s*[-*+] \[)([ xX])(\] )(.*)$/
|
|
37
|
+
|
|
38
|
+
/** The trailing segment a tick carries, naming the commit the box passed at. Matched only at the end of a line, so a box whose own text says `passed at` is left alone. */
|
|
39
|
+
const STAMP = / · passed at `([0-9a-f]{7,40})`$/
|
|
40
|
+
|
|
41
|
+
/** What ends the text of a box that a person has to judge by eye. */
|
|
42
|
+
const TASTE_SUFFIX = '(taste)'
|
|
25
43
|
|
|
26
44
|
/** A `#issuecomment-<id>` suffix, which is the REST comment id `gh pr view` never returns directly. */
|
|
27
45
|
const ISSUE_COMMENT_ID = /#issuecomment-(\d+)$/
|
|
@@ -29,8 +47,8 @@ const ISSUE_COMMENT_ID = /#issuecomment-(\d+)$/
|
|
|
29
47
|
/**
|
|
30
48
|
* The image extensions this module embeds. An `evidence/` folder holds
|
|
31
49
|
* whatever else a project keeps beside its captures, such as a README, a
|
|
32
|
-
* capture script, or a raw data file, and none of those render as
|
|
33
|
-
*
|
|
50
|
+
* capture script, or a raw data file, and none of those render as an
|
|
51
|
+
* `<img>` without producing a broken embed.
|
|
34
52
|
*/
|
|
35
53
|
const IMAGE_EXTENSION = /\.(png|jpe?g|gif|webp|avif|svg)$/i
|
|
36
54
|
|
|
@@ -121,6 +139,35 @@ function rawUrl(repo: string, sha: string, path: string): string {
|
|
|
121
139
|
return `https://github.com/${repo}/blob/${sha}/${path}?raw=true`
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The pixel width every screenshot asks for. GitHub sizes a comment table to
|
|
144
|
+
* its content and clamps an image to its cell, so a width past any comment
|
|
145
|
+
* fills the cell, where `width="100%"` leaves a narrow screenshot narrow.
|
|
146
|
+
*/
|
|
147
|
+
const IMAGE_WIDTH = 1000
|
|
148
|
+
|
|
149
|
+
/** A table cell and an attribute value share one escape, since a stem is a path segment placed in both. */
|
|
150
|
+
function escapeCell(text: string): string {
|
|
151
|
+
return text
|
|
152
|
+
.replaceAll('&', '&')
|
|
153
|
+
.replaceAll('<', '<')
|
|
154
|
+
.replaceAll('>', '>')
|
|
155
|
+
.replaceAll('"', '"')
|
|
156
|
+
.replaceAll('|', '|')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function evidenceImage(url: string, alt: string): string {
|
|
160
|
+
return `<img src="${url}" width="${IMAGE_WIDTH}" alt="${alt}">`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const EVIDENCE_IMAGE = String.raw`(?:!\[\]\([^)]*\)|<img [^>]*>)`
|
|
164
|
+
|
|
165
|
+
/** A case row in any shape a render has posted: a Base cell before the head image, or the head image alone. */
|
|
166
|
+
const EVIDENCE_ROW_PATTERN = new RegExp(
|
|
167
|
+
String.raw`^\| .+ \| (?:(?:\*\(new\)\*|${EVIDENCE_IMAGE}) \| )?${EVIDENCE_IMAGE} \|$`,
|
|
168
|
+
'gm',
|
|
169
|
+
)
|
|
170
|
+
|
|
124
171
|
export function evidenceMarker(head: string): string {
|
|
125
172
|
return `${MARKER_PREFIX} head=${head} -->`
|
|
126
173
|
}
|
|
@@ -135,15 +182,15 @@ export function evidenceMarker(head: string): string {
|
|
|
135
182
|
* pinned to a commit sha rather than a branch, so the comment keeps showing
|
|
136
183
|
* what it claimed even after the branch moves.
|
|
137
184
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
185
|
+
* The body opens with `## Evidence` and, under it, one address line joining
|
|
186
|
+
* whichever of the hosted and local previews apply, so the comment reads as
|
|
187
|
+
* evidence first and a preview never stands where the heading should. With no
|
|
188
|
+
* states the heading and the address are the whole body apart from the
|
|
189
|
+
* marker, so a pull request whose screenshots did not change still gets one
|
|
190
|
+
* comment a later call can find and edit in place.
|
|
141
191
|
*
|
|
142
192
|
* A checklist closes the body, below the comparison it annotates, so one
|
|
143
|
-
* comment carries the
|
|
144
|
-
*
|
|
145
|
-
* A local address sits under the hosted one, never above it, so the hosted
|
|
146
|
-
* line keeps the first-line position `findEvidencePreview` reads.
|
|
193
|
+
* comment carries the addresses, the screenshots, and what to look at.
|
|
147
194
|
*/
|
|
148
195
|
export function renderEvidenceBody(
|
|
149
196
|
states: readonly EvidenceState[],
|
|
@@ -154,11 +201,15 @@ export function renderEvidenceBody(
|
|
|
154
201
|
checklist?: string,
|
|
155
202
|
local?: string,
|
|
156
203
|
): string {
|
|
157
|
-
const
|
|
204
|
+
const segments = [
|
|
158
205
|
...(preview === undefined ? [] : [`${PREVIEW_PREFIX} ${preview}`]),
|
|
159
206
|
...(local === undefined ? [] : [`${LOCAL_PREFIX} ${local}`]),
|
|
160
207
|
]
|
|
161
|
-
const opening =
|
|
208
|
+
const opening = [
|
|
209
|
+
EVIDENCE_HEADING,
|
|
210
|
+
'',
|
|
211
|
+
...(segments.length === 0 ? [] : [segments.join(ADDRESS_SEPARATOR), '']),
|
|
212
|
+
]
|
|
162
213
|
const closing =
|
|
163
214
|
checklist === undefined
|
|
164
215
|
? []
|
|
@@ -176,20 +227,24 @@ export function renderEvidenceBody(
|
|
|
176
227
|
imageCount <= EVIDENCE_OPEN_LIMIT ? '<details open>' : '<details>'
|
|
177
228
|
|
|
178
229
|
const sections = states.map((entry) => {
|
|
230
|
+
const hasBase = entry.items.some((item) => !item.added)
|
|
179
231
|
const rows = entry.items.map((item) => {
|
|
232
|
+
const stem = escapeCell(item.stem)
|
|
180
233
|
const before = item.added
|
|
181
234
|
? '*(new)*'
|
|
182
|
-
:
|
|
183
|
-
const after =
|
|
184
|
-
return
|
|
235
|
+
: evidenceImage(rawUrl(repo, base, item.path), `${stem} base`)
|
|
236
|
+
const after = evidenceImage(rawUrl(repo, head, item.path), `${stem} head`)
|
|
237
|
+
return hasBase
|
|
238
|
+
? `| ${stem} | ${before} | ${after} |`
|
|
239
|
+
: `| ${stem} | ${after} |`
|
|
185
240
|
})
|
|
186
241
|
|
|
187
242
|
return [
|
|
188
243
|
opener,
|
|
189
244
|
`<summary>${entry.state === '' ? 'evidence' : entry.state} (${entry.items.length})</summary>`,
|
|
190
245
|
'',
|
|
191
|
-
'| Case | Base | Head |',
|
|
192
|
-
'| --- | --- | --- |',
|
|
246
|
+
hasBase ? '| Case | Base | Head |' : '| Case | Head |',
|
|
247
|
+
hasBase ? '| --- | --- | --- |' : '| --- | --- |',
|
|
193
248
|
...rows,
|
|
194
249
|
'',
|
|
195
250
|
'</details>',
|
|
@@ -198,9 +253,7 @@ export function renderEvidenceBody(
|
|
|
198
253
|
|
|
199
254
|
return [
|
|
200
255
|
...opening,
|
|
201
|
-
|
|
202
|
-
'',
|
|
203
|
-
`**Base:** \`${base.slice(0, SHORT_SHA_LENGTH)}\` · **Head:** \`${head.slice(0, SHORT_SHA_LENGTH)}\``,
|
|
256
|
+
`${BASE_PREFIX} \`${base.slice(0, SHORT_SHA_LENGTH)}\` · **Head:** \`${head.slice(0, SHORT_SHA_LENGTH)}\``,
|
|
204
257
|
'',
|
|
205
258
|
...sections,
|
|
206
259
|
'',
|
|
@@ -210,16 +263,14 @@ export function renderEvidenceBody(
|
|
|
210
263
|
}
|
|
211
264
|
|
|
212
265
|
/**
|
|
213
|
-
* How many case rows a rendered body carries, read off the
|
|
266
|
+
* How many case rows a rendered body carries, read off the row shapes
|
|
214
267
|
* `renderEvidenceBody` writes. A row always ends in a head image, which a
|
|
215
|
-
* checklist line or a preview address never does.
|
|
268
|
+
* checklist line or a preview address never does. The pattern also reads the
|
|
269
|
+
* `` three-column shape an earlier render posted, so a comment posted
|
|
270
|
+
* before the change still counts its cases and the `would-empty` guard holds.
|
|
216
271
|
*/
|
|
217
272
|
export function countEvidenceCases(body: string): number {
|
|
218
|
-
return (
|
|
219
|
-
body.match(
|
|
220
|
-
/^\| .+ \| (?:\*\(new\)\*|!\[\]\([^)]*\)) \| !\[\]\([^)]*\) \|$/gm,
|
|
221
|
-
)?.length ?? 0
|
|
222
|
-
)
|
|
273
|
+
return body.match(EVIDENCE_ROW_PATTERN)?.length ?? 0
|
|
223
274
|
}
|
|
224
275
|
|
|
225
276
|
export interface EvidenceComment {
|
|
@@ -249,7 +300,8 @@ export interface OwedInput {
|
|
|
249
300
|
readonly hasEvidenceChange: boolean
|
|
250
301
|
readonly hasMarkedComment: boolean
|
|
251
302
|
readonly carriedPreview: boolean
|
|
252
|
-
|
|
303
|
+
/** A deploy workflow resolves and its push filter matches a path the pull request changed. */
|
|
304
|
+
readonly deployServesChange: boolean
|
|
253
305
|
}
|
|
254
306
|
|
|
255
307
|
/**
|
|
@@ -262,7 +314,7 @@ export function readOwed(input: OwedInput): OwedStep[] {
|
|
|
262
314
|
if (input.hasEvidenceChange && !input.hasMarkedComment) owed.push('evidence')
|
|
263
315
|
if (
|
|
264
316
|
(input.hasMarkedComment || input.hasEvidenceChange) &&
|
|
265
|
-
input.
|
|
317
|
+
input.deployServesChange &&
|
|
266
318
|
!input.carriedPreview
|
|
267
319
|
) {
|
|
268
320
|
owed.push('preview')
|
|
@@ -295,33 +347,78 @@ export function findEvidenceCaseCount(
|
|
|
295
347
|
return marked === undefined ? 0 : countEvidenceCases(marked.body)
|
|
296
348
|
}
|
|
297
349
|
|
|
350
|
+
/** Lines that close the address region, being whatever a body carries below its addresses. */
|
|
351
|
+
const REGION_ENDS = [
|
|
352
|
+
BASE_PREFIX,
|
|
353
|
+
CHECKLIST_HEADING,
|
|
354
|
+
'<details',
|
|
355
|
+
CHECKLIST_START,
|
|
356
|
+
MARKER_PREFIX,
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
export interface AddressReading {
|
|
360
|
+
/** Indexes of the lines carrying a hosted or local segment. */
|
|
361
|
+
readonly lines: readonly number[]
|
|
362
|
+
readonly preview?: string
|
|
363
|
+
readonly local?: string
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function readSegment(segment: string, prefix: string): string | undefined {
|
|
367
|
+
return segment.startsWith(prefix)
|
|
368
|
+
? segment.slice(prefix.length).trim() || undefined
|
|
369
|
+
: undefined
|
|
370
|
+
}
|
|
371
|
+
|
|
298
372
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
373
|
+
* Reads the addresses a body carries in the region above its commit line,
|
|
374
|
+
* checklist, comparison, or marker, matching each prefix wherever it sits in
|
|
375
|
+
* that region. One reader covers the layout that opens on the addresses as
|
|
376
|
+
* well as the one that opens on the heading, so an open pull request keeps
|
|
377
|
+
* its links across the first re-render, and a line quoted further down in a
|
|
378
|
+
* checklist is never taken for an address.
|
|
302
379
|
*/
|
|
303
|
-
export function
|
|
380
|
+
export function readAddresses(body: string): AddressReading {
|
|
381
|
+
const lines: number[] = []
|
|
382
|
+
let preview: string | undefined
|
|
383
|
+
let local: string | undefined
|
|
384
|
+
const bodyLines = body.split('\n')
|
|
385
|
+
for (let index = 0; index < bodyLines.length; index += 1) {
|
|
386
|
+
const line = (bodyLines[index] ?? '').trim()
|
|
387
|
+
if (REGION_ENDS.some((end) => line.startsWith(end))) break
|
|
388
|
+
let isAddress = false
|
|
389
|
+
for (const segment of line.split(ADDRESS_SEPARATOR.trim())) {
|
|
390
|
+
const trimmed = segment.trim()
|
|
391
|
+
const hosted = readSegment(trimmed, PREVIEW_PREFIX)
|
|
392
|
+
const own = readSegment(trimmed, LOCAL_PREFIX)
|
|
393
|
+
if (hosted !== undefined) preview ??= hosted
|
|
394
|
+
if (own !== undefined) local ??= own
|
|
395
|
+
if (hosted !== undefined || own !== undefined) isAddress = true
|
|
396
|
+
}
|
|
397
|
+
if (isAddress) lines.push(index)
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
lines,
|
|
401
|
+
...(preview !== undefined && { preview }),
|
|
402
|
+
...(local !== undefined && { local }),
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function findMarked(
|
|
304
407
|
comments: readonly EvidenceComment[],
|
|
305
|
-
):
|
|
306
|
-
|
|
307
|
-
const first = marked?.body.split('\n')[0]?.trim() ?? ''
|
|
308
|
-
if (!first.startsWith(PREVIEW_PREFIX)) return undefined
|
|
309
|
-
return first.slice(PREVIEW_PREFIX.length).trim() || undefined
|
|
408
|
+
): EvidenceComment | undefined {
|
|
409
|
+
return comments.find((comment) => hasEvidenceMarker(comment.body))
|
|
310
410
|
}
|
|
311
411
|
|
|
312
412
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
413
|
+
* The preview address the marked comment already carries. A re-render after
|
|
414
|
+
* a later push passes no address of its own, and carrying this one forward
|
|
415
|
+
* keeps that edit from deleting the link a reviewer is using.
|
|
316
416
|
*/
|
|
317
|
-
export function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (line.startsWith(LOCAL_PREFIX)) return index
|
|
323
|
-
}
|
|
324
|
-
return -1
|
|
417
|
+
export function findEvidencePreview(
|
|
418
|
+
comments: readonly EvidenceComment[],
|
|
419
|
+
): string | undefined {
|
|
420
|
+
const marked = findMarked(comments)
|
|
421
|
+
return marked === undefined ? undefined : readAddresses(marked.body).preview
|
|
325
422
|
}
|
|
326
423
|
|
|
327
424
|
/**
|
|
@@ -331,12 +428,8 @@ export function findLocalLineIndex(body: string): number {
|
|
|
331
428
|
export function findEvidenceLocal(
|
|
332
429
|
comments: readonly EvidenceComment[],
|
|
333
430
|
): string | undefined {
|
|
334
|
-
const marked = comments
|
|
335
|
-
|
|
336
|
-
const index = findLocalLineIndex(marked.body)
|
|
337
|
-
if (index === -1) return undefined
|
|
338
|
-
const line = marked.body.split('\n')[index]?.trim() ?? ''
|
|
339
|
-
return line.slice(LOCAL_PREFIX.length).trim() || undefined
|
|
431
|
+
const marked = findMarked(comments)
|
|
432
|
+
return marked === undefined ? undefined : readAddresses(marked.body).local
|
|
340
433
|
}
|
|
341
434
|
|
|
342
435
|
/**
|
|
@@ -357,3 +450,101 @@ export function findEvidenceChecklist(
|
|
|
357
450
|
marked.body.slice(start + CHECKLIST_START.length, end).trim() || undefined
|
|
358
451
|
)
|
|
359
452
|
}
|
|
453
|
+
|
|
454
|
+
export interface ChecklistBox {
|
|
455
|
+
/** One-based position among the checklist's boxes, in the order they appear. */
|
|
456
|
+
readonly number: number
|
|
457
|
+
/** Index of the box's line within the checklist. */
|
|
458
|
+
readonly line: number
|
|
459
|
+
/** The box text with any stamp removed. */
|
|
460
|
+
readonly text: string
|
|
461
|
+
readonly isTicked: boolean
|
|
462
|
+
/** The short sha a tick names, absent on an empty box or a tick nothing stamped. */
|
|
463
|
+
readonly stamp?: string
|
|
464
|
+
/** Whether the box ends in `(taste)`, which no driver ticks. */
|
|
465
|
+
readonly isTaste: boolean
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export interface BoxLine {
|
|
469
|
+
readonly before: string
|
|
470
|
+
readonly isTicked: boolean
|
|
471
|
+
readonly text: string
|
|
472
|
+
readonly stamp?: string
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Splits one checklist line into its box parts, or undefined when the line is not a box. */
|
|
476
|
+
export function parseBoxLine(line: string): BoxLine | undefined {
|
|
477
|
+
const match = BOX_LINE.exec(line)
|
|
478
|
+
if (match === null) return undefined
|
|
479
|
+
const rest = (match[4] ?? '').trimEnd()
|
|
480
|
+
const stamped = STAMP.exec(rest)
|
|
481
|
+
return {
|
|
482
|
+
before: match[1] ?? '',
|
|
483
|
+
isTicked: (match[2] ?? ' ') !== ' ',
|
|
484
|
+
text: stamped === null ? rest : rest.slice(0, stamped.index),
|
|
485
|
+
...(stamped?.[1] !== undefined && { stamp: stamped[1] }),
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** The box line for the given parts, written back in the one shape the reader accepts. */
|
|
490
|
+
export function writeBoxLine(
|
|
491
|
+
box: Pick<BoxLine, 'before' | 'text'>,
|
|
492
|
+
stampedAt?: string,
|
|
493
|
+
): string {
|
|
494
|
+
return stampedAt === undefined
|
|
495
|
+
? `${box.before} ] ${box.text}`
|
|
496
|
+
: `${box.before}x] ${box.text} · passed at \`${stampedAt.slice(0, SHORT_SHA_LENGTH)}\``
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Every box of a checklist in order, numbered the way `canon pr tick` takes them. */
|
|
500
|
+
export function readChecklistBoxes(checklist: string): ChecklistBox[] {
|
|
501
|
+
const boxes: ChecklistBox[] = []
|
|
502
|
+
checklist.split('\n').forEach((line, index) => {
|
|
503
|
+
const parsed = parseBoxLine(line)
|
|
504
|
+
if (parsed === undefined) return
|
|
505
|
+
boxes.push({
|
|
506
|
+
number: boxes.length + 1,
|
|
507
|
+
line: index,
|
|
508
|
+
text: parsed.text,
|
|
509
|
+
isTicked: parsed.isTicked,
|
|
510
|
+
...(parsed.stamp !== undefined && { stamp: parsed.stamp }),
|
|
511
|
+
isTaste: parsed.text.endsWith(TASTE_SUFFIX),
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
return boxes
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** The head the marked comment's trailing marker names. */
|
|
518
|
+
export function findEvidenceHead(
|
|
519
|
+
comments: readonly EvidenceComment[],
|
|
520
|
+
): string | undefined {
|
|
521
|
+
const marked = comments.find((comment) => hasEvidenceMarker(comment.body))
|
|
522
|
+
if (marked === undefined) return undefined
|
|
523
|
+
const lines = marked.body.trimEnd().split('\n')
|
|
524
|
+
const match = /head=(\S+)/.exec(lines[lines.length - 1] ?? '')
|
|
525
|
+
return match?.[1]
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Clears every tick the render's head has not earned. A stamped tick stays
|
|
530
|
+
* while its sha is a prefix of the head. An unstamped one names no head, so
|
|
531
|
+
* it stays only while the comment it came from described this same head.
|
|
532
|
+
*/
|
|
533
|
+
export function settleChecklist(
|
|
534
|
+
checklist: string,
|
|
535
|
+
head: string,
|
|
536
|
+
carriedHead: string | undefined,
|
|
537
|
+
): string {
|
|
538
|
+
return checklist
|
|
539
|
+
.split('\n')
|
|
540
|
+
.map((line) => {
|
|
541
|
+
const parsed = parseBoxLine(line)
|
|
542
|
+
if (parsed === undefined || !parsed.isTicked) return line
|
|
543
|
+
const isEarned =
|
|
544
|
+
parsed.stamp === undefined
|
|
545
|
+
? carriedHead === head
|
|
546
|
+
: head.startsWith(parsed.stamp)
|
|
547
|
+
return isEarned ? line : writeBoxLine(parsed)
|
|
548
|
+
})
|
|
549
|
+
.join('\n')
|
|
550
|
+
}
|
package/src/pr/local.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readAddresses } from '@/pr/evidence'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The folder linked worktrees sit under. A listener there belongs to another
|
|
@@ -111,15 +111,21 @@ export function parseProcNetTcp(text: string): ProcSocket[] {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* and
|
|
117
|
-
*
|
|
114
|
+
* Drops the address line of a marked body, hosted and local segments alike,
|
|
115
|
+
* since the close that runs this also deletes the branch's preview
|
|
116
|
+
* deployments and a link left behind points at nothing. Every other byte stays
|
|
117
|
+
* in place, so the trailing marker, the checklist delimiters, and any ticked
|
|
118
|
+
* box survive, and the blank line the address sat on is dropped with it so no
|
|
119
|
+
* gap doubles. Undefined when the body carries no address, which is also what
|
|
120
|
+
* a second drop of the same body returns.
|
|
118
121
|
*/
|
|
119
|
-
export function
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
export function dropAddressLine(body: string): string | undefined {
|
|
123
|
+
const { lines: indexes } = readAddresses(body)
|
|
124
|
+
const first = indexes[0]
|
|
125
|
+
if (first === undefined) return undefined
|
|
126
|
+
const dropped = new Set(indexes)
|
|
127
|
+
const lines = body.split('\n').filter((_, index) => !dropped.has(index))
|
|
128
|
+
const isGapAbove = first === 0 || (lines[first - 1] ?? '').trim() === ''
|
|
129
|
+
if (isGapAbove && (lines[first] ?? '').trim() === '') lines.splice(first, 1)
|
|
124
130
|
return lines.join('\n')
|
|
125
131
|
}
|
package/src/pr/preview.ts
CHANGED
|
@@ -16,6 +16,7 @@ const BRANCH_FENCE = '--branch=${{ github.ref_name }}'
|
|
|
16
16
|
|
|
17
17
|
export type PreviewRefusal =
|
|
18
18
|
| 'no-deploy'
|
|
19
|
+
| 'unserved'
|
|
19
20
|
| 'unfenced'
|
|
20
21
|
| 'no-alias'
|
|
21
22
|
| 'gh-failed'
|
|
@@ -27,8 +28,77 @@ export interface WorkflowFile {
|
|
|
27
28
|
readonly text: string
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Which changed paths a deploy builds from, read off its own push trigger, so
|
|
33
|
+
* the workflow that decides when production deploys is also the one statement
|
|
34
|
+
* of what a branch preview serves.
|
|
35
|
+
*/
|
|
36
|
+
export type ServedFilter =
|
|
37
|
+
| { readonly kind: 'all' }
|
|
38
|
+
| { readonly kind: 'paths'; readonly patterns: readonly string[] }
|
|
39
|
+
| { readonly kind: 'paths-ignore'; readonly patterns: readonly string[] }
|
|
40
|
+
|
|
41
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
42
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readPatterns(value: unknown): string[] | undefined {
|
|
46
|
+
if (!Array.isArray(value)) return undefined
|
|
47
|
+
return value.filter((entry): entry is string => typeof entry === 'string')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reads `on.push.paths` or `on.push.paths-ignore`. A workflow with neither,
|
|
52
|
+
* with no push trigger, or that does not parse serves every change, since
|
|
53
|
+
* reading unserved by mistake stops every reader asking for a preview.
|
|
54
|
+
*/
|
|
55
|
+
export function readServedPaths(workflowText: string): ServedFilter {
|
|
56
|
+
let parsed: unknown
|
|
57
|
+
try {
|
|
58
|
+
parsed = Bun.YAML.parse(workflowText)
|
|
59
|
+
} catch {
|
|
60
|
+
return { kind: 'all' }
|
|
61
|
+
}
|
|
62
|
+
const on = isRecord(parsed) ? parsed.on : undefined
|
|
63
|
+
const push = isRecord(on) ? on.push : undefined
|
|
64
|
+
if (!isRecord(push)) return { kind: 'all' }
|
|
65
|
+
|
|
66
|
+
const paths = readPatterns(push.paths)
|
|
67
|
+
if (paths !== undefined) return { kind: 'paths', patterns: paths }
|
|
68
|
+
const ignored = readPatterns(push['paths-ignore'])
|
|
69
|
+
if (ignored !== undefined) return { kind: 'paths-ignore', patterns: ignored }
|
|
70
|
+
return { kind: 'all' }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether the patterns match a path, applied in order so a later `!` pattern overrides an earlier one, as GitHub applies them. */
|
|
74
|
+
function matchesInOrder(patterns: readonly string[], path: string): boolean {
|
|
75
|
+
let isMatched = false
|
|
76
|
+
for (const pattern of patterns) {
|
|
77
|
+
const isNegated = pattern.startsWith('!')
|
|
78
|
+
const glob = new Bun.Glob(isNegated ? pattern.slice(1) : pattern)
|
|
79
|
+
if (glob.match(path)) isMatched = !isNegated
|
|
80
|
+
}
|
|
81
|
+
return isMatched
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Whether a push carrying `changedPaths` would run the deploy, by the filter its trigger declares. */
|
|
85
|
+
export function servesChange(
|
|
86
|
+
filter: ServedFilter,
|
|
87
|
+
changedPaths: readonly string[],
|
|
88
|
+
): boolean {
|
|
89
|
+
if (filter.kind === 'all') return true
|
|
90
|
+
if (filter.kind === 'paths') {
|
|
91
|
+
return changedPaths.some((path) => matchesInOrder(filter.patterns, path))
|
|
92
|
+
}
|
|
93
|
+
return changedPaths.some((path) => !matchesInOrder(filter.patterns, path))
|
|
94
|
+
}
|
|
95
|
+
|
|
30
96
|
export type WorkflowPick =
|
|
31
|
-
| {
|
|
97
|
+
| {
|
|
98
|
+
readonly kind: 'found'
|
|
99
|
+
readonly path: string
|
|
100
|
+
readonly filter: ServedFilter
|
|
101
|
+
}
|
|
32
102
|
| {
|
|
33
103
|
readonly kind: 'refused'
|
|
34
104
|
readonly reason: 'no-deploy' | 'unfenced' | 'no-alias'
|
|
@@ -36,6 +106,7 @@ export type WorkflowPick =
|
|
|
36
106
|
|
|
37
107
|
interface WorkflowShape {
|
|
38
108
|
readonly path: string
|
|
109
|
+
readonly text: string
|
|
39
110
|
readonly deployLines: readonly string[]
|
|
40
111
|
readonly isDispatchable: boolean
|
|
41
112
|
readonly hasMarker: boolean
|
|
@@ -51,6 +122,7 @@ function readShape(file: WorkflowFile): WorkflowShape {
|
|
|
51
122
|
.filter((line) => !line.trimStart().startsWith('#'))
|
|
52
123
|
return {
|
|
53
124
|
path: file.path,
|
|
125
|
+
text: file.text,
|
|
54
126
|
deployLines: lines.filter((line) => line.includes('pages deploy')),
|
|
55
127
|
isDispatchable: lines.some((line) => /^\s*workflow_dispatch:/.test(line)),
|
|
56
128
|
hasMarker: lines.some((line) => line.includes(ALIAS_MARKER)),
|
|
@@ -80,7 +152,11 @@ export function findDeployWorkflow(
|
|
|
80
152
|
const marked = fenced.find((shape) => shape.hasMarker)
|
|
81
153
|
if (marked === undefined) return { kind: 'refused', reason: 'no-alias' }
|
|
82
154
|
|
|
83
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
kind: 'found',
|
|
157
|
+
path: marked.path,
|
|
158
|
+
filter: readServedPaths(marked.text),
|
|
159
|
+
}
|
|
84
160
|
}
|
|
85
161
|
|
|
86
162
|
export function readPreviewAlias(log: string): string | undefined {
|
package/src/pr/tick.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHECKLIST_END,
|
|
3
|
+
CHECKLIST_START,
|
|
4
|
+
parseBoxLine,
|
|
5
|
+
readChecklistBoxes,
|
|
6
|
+
writeBoxLine,
|
|
7
|
+
} from '@/pr/evidence'
|
|
8
|
+
|
|
9
|
+
export type TickRefusal = 'no-checklist' | 'no-box' | 'taste-box'
|
|
10
|
+
|
|
11
|
+
export type TickResult =
|
|
12
|
+
| { readonly kind: 'ticked'; readonly body: string }
|
|
13
|
+
| { readonly kind: 'refused'; readonly reason: TickRefusal }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ticks the named boxes of a marked body's checklist and stamps each with the
|
|
17
|
+
* commit it passed at. A refusal writes nothing, so a taste box named among
|
|
18
|
+
* passed ones leaves every box as it was. A box already ticked is restamped,
|
|
19
|
+
* which makes a repeat at one head a no-op.
|
|
20
|
+
*/
|
|
21
|
+
export function tickBoxes(
|
|
22
|
+
body: string,
|
|
23
|
+
numbers: readonly number[],
|
|
24
|
+
head: string,
|
|
25
|
+
): TickResult {
|
|
26
|
+
const start = body.indexOf(CHECKLIST_START)
|
|
27
|
+
const end = body.indexOf(CHECKLIST_END, start)
|
|
28
|
+
if (start === -1 || end === -1) {
|
|
29
|
+
return { kind: 'refused', reason: 'no-checklist' }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const from = start + CHECKLIST_START.length
|
|
33
|
+
const region = body.slice(from, end)
|
|
34
|
+
const boxes = readChecklistBoxes(region)
|
|
35
|
+
|
|
36
|
+
const named = numbers.map((number) =>
|
|
37
|
+
boxes.find((box) => box.number === number),
|
|
38
|
+
)
|
|
39
|
+
if (named.some((box) => box === undefined)) {
|
|
40
|
+
return { kind: 'refused', reason: 'no-box' }
|
|
41
|
+
}
|
|
42
|
+
if (named.some((box) => box?.isTaste === true)) {
|
|
43
|
+
return { kind: 'refused', reason: 'taste-box' }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines = region.split('\n')
|
|
47
|
+
for (const box of named) {
|
|
48
|
+
if (box === undefined) continue
|
|
49
|
+
const parsed = parseBoxLine(lines[box.line] ?? '')
|
|
50
|
+
if (parsed !== undefined) lines[box.line] = writeBoxLine(parsed, head)
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
kind: 'ticked',
|
|
54
|
+
body: `${body.slice(0, from)}${lines.join('\n')}${body.slice(end)}`,
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/process/harness.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
2
|
import { readdirSync, statSync } from 'node:fs'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
-
import { PROJECT_ROOT } from '@/project
|
|
4
|
+
import { PROJECT_ROOT } from '@/roots/project'
|
|
5
5
|
import { stateDir } from '@/targets/registry'
|
|
6
6
|
|
|
7
7
|
const CLI = join(PROJECT_ROOT, 'src/cli.ts')
|
package/src/records/backup.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readdirSync } from 'node:fs'
|
|
2
2
|
import { basename, join, relative, resolve } from 'node:path'
|
|
3
3
|
import { $ } from 'bun'
|
|
4
|
-
import { gitEnv } from '@/git
|
|
5
|
-
import { RECORD_ROOTS, recordRoot, SCRATCH } from '@/record
|
|
4
|
+
import { gitEnv } from '@/git/env'
|
|
5
|
+
import { RECORD_ROOTS, recordRoot, SCRATCH } from '@/roots/record'
|
|
6
6
|
import { type BlockedPath, guardPayload } from '@/records/push-guard'
|
|
7
7
|
|
|
8
8
|
/**
|
package/src/records/ordinal.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, rm, stat } from 'node:fs/promises'
|
|
2
2
|
import { extractOrdinal } from '@/intake/folder'
|
|
3
|
-
import { recordDir } from '@/record
|
|
3
|
+
import { recordDir } from '@/roots/record'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The two record folders that share one ordinal sequence, per
|
package/src/records/prune.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { readdir, rm, stat } from 'node:fs/promises'
|
|
|
3
3
|
import { join, relative } from 'node:path'
|
|
4
4
|
import { RECORD_LAYOUT_MOVES } from '@/migrate/record-layout'
|
|
5
5
|
import { PROMOTED_FOLDERS } from '@/migrate/scratch-evidence'
|
|
6
|
-
import { RECORD_ROOTS, recordDir, SCRATCH } from '@/record
|
|
6
|
+
import { RECORD_ROOTS, recordDir, SCRATCH } from '@/roots/record'
|
|
7
7
|
import { day, newestMtime } from '@/records/size'
|
|
8
8
|
|
|
9
9
|
const DAY_MS = 24 * 60 * 60 * 1000
|