@erclx/canon 5.32.0 → 5.34.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/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/REQUIREMENT.md +1 -2
- package/claude/skills/api-design/SKILL.md +2 -3
- package/claude/skills/api-design/references/adopted.md +1 -1
- package/claude/skills/auto-ship/SKILL.md +1 -1
- package/claude/skills/auto-ship/references/worktree-name.md +1 -1
- package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
- package/claude/skills/backlog-triage/SKILL.md +1 -1
- package/claude/skills/canon-cli/REQUIREMENT.md +16 -13
- package/claude/skills/canon-cli/SKILL.md +75 -32
- package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
- package/claude/skills/canon-feedback/SKILL.md +11 -29
- package/claude/skills/canon-feedback/references/template.md +53 -0
- package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
- package/claude/skills/{canvas → canvas-board}/SKILL.md +6 -3
- package/claude/skills/{canvas → canvas-board}/references/canvas.md +26 -3
- package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
- package/claude/skills/code-craft/REQUIREMENT.md +1 -1
- package/claude/skills/code-craft/SKILL.md +0 -1
- package/claude/skills/context-fold/REQUIREMENT.md +25 -33
- package/claude/skills/context-fold/SKILL.md +25 -70
- package/claude/skills/context-fold/references/classify.md +3 -3
- package/claude/skills/context-fold/references/completion-report.md +4 -4
- package/claude/skills/context-fold/references/context-refresh.md +5 -5
- package/{standards → claude/skills/context-fold/references}/context.md +16 -5
- package/claude/skills/context-fold/references/unusable-baseline.md +3 -5
- package/claude/skills/create-rule/SKILL.md +3 -3
- package/{standards → claude/skills/create-rule/references}/rule.md +15 -6
- package/claude/skills/create-skill/SKILL.md +2 -2
- package/{standards → claude/skills/create-skill/references}/skill.md +18 -15
- package/claude/skills/design-taste/REQUIREMENT.md +3 -3
- package/claude/skills/design-taste/SKILL.md +1 -1
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
- package/claude/skills/docs-sync/SKILL.md +3 -3
- package/{standards → claude/skills/docs-sync/references}/docs.md +11 -0
- package/{standards → claude/skills/docs-sync/references}/readme.md +9 -2
- package/claude/skills/draft-and-pick/SKILL.md +5 -5
- package/claude/skills/draft-doc/SKILL.md +6 -6
- package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
- package/claude/skills/draft-figure/SKILL.md +2 -2
- package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
- package/claude/skills/draft-figure/references/mermaid-path.md +1 -1
- package/claude/skills/draft-identity/REQUIREMENT.md +2 -2
- package/claude/skills/draft-identity/SKILL.md +2 -2
- package/claude/skills/draft-ready/SKILL.md +1 -1
- package/claude/skills/git-branch/SKILL.md +1 -1
- package/{standards → claude/skills/git-branch/references}/branch.md +1 -1
- package/claude/skills/git-commit/SKILL.md +2 -2
- package/claude/skills/git-followup/REQUIREMENT.md +1 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/git-issue/references/issue.md +4 -1
- package/claude/skills/git-pr/SKILL.md +13 -13
- package/claude/skills/git-pr/references/evidence.md +2 -2
- package/claude/skills/git-pr/references/final-command.md +38 -14
- package/claude/skills/git-pr/references/labels.md +5 -1
- package/{standards → claude/skills/git-pr/references}/pr.md +19 -9
- package/{standards → claude/skills/git-pr/references}/publish.md +4 -2
- package/claude/skills/git-pr/references/task-number.md +1 -1
- package/claude/skills/git-ship/SKILL.md +15 -14
- package/claude/skills/git-split/SKILL.md +3 -3
- package/claude/skills/git-stage/SKILL.md +1 -1
- package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +1 -1
- package/claude/skills/review-address/references/post-review-findings.md +1 -1
- package/claude/skills/review-craft/references/ui.md +1 -1
- package/claude/skills/review-pr/REQUIREMENT.md +1 -1
- package/claude/skills/review-pr/SKILL.md +3 -3
- package/claude/skills/review-pr/references/body-variants.md +2 -2
- package/claude/skills/review-pr/references/key-changes.md +3 -3
- package/claude/skills/review-pr/references/post.md +4 -2
- package/claude/skills/review-ui/REQUIREMENT.md +1 -1
- package/claude/skills/review-ui/SKILL.md +8 -6
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +53 -17
- package/claude/skills/role-orchestrator/REQUIREMENT.md +3 -0
- package/claude/skills/role-orchestrator/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +8 -5
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +32 -1
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-resume.md +3 -0
- package/claude/skills/role-planner/REQUIREMENT.md +2 -1
- package/claude/skills/role-planner/SKILL.md +13 -2
- package/claude/skills/role-reviewer/SKILL.md +1 -1
- package/claude/skills/role-worker/REQUIREMENT.md +1 -0
- package/claude/skills/role-worker/SKILL.md +9 -1
- package/claude/skills/session-map/SKILL.md +1 -1
- package/claude/skills/session-resume/REQUIREMENT.md +2 -2
- package/claude/skills/session-worktree/SKILL.md +5 -5
- package/claude/skills/target-setup/SKILL.md +10 -4
- package/claude/skills/target-setup/references/detect.md +20 -3
- package/claude/skills/task-board/REQUIREMENT.md +7 -12
- package/claude/skills/task-board/SKILL.md +5 -17
- package/claude/skills/task-board/references/tasks.md +2 -2
- package/claude/skills/task-complete/REQUIREMENT.md +47 -0
- package/claude/skills/task-complete/SKILL.md +86 -0
- package/claude/skills/{task-board → task-complete}/references/archive.md +4 -4
- package/claude/skills/{context-fold → task-complete}/references/receipt-sweep.md +5 -5
- package/claude/skills/task-complete/references/unusable-baseline.md +19 -0
- package/claude/skills/teach-workspace/REQUIREMENT.md +6 -2
- package/claude/skills/teach-workspace/SKILL.md +8 -4
- package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
- package/claude/skills/{context-fold/references/promoted-pages.md → teach-workspace/references/promotion-fold.md} +3 -3
- package/claude/skills/teach-workspace/references/promotion.md +3 -3
- package/claude/skills/test-craft/REQUIREMENT.md +2 -3
- package/claude/skills/test-craft/SKILL.md +15 -3
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/ux-audit/SKILL.md +1 -1
- package/claude/skills/ux-measure/SKILL.md +1 -1
- package/docs/agents/canvas.md +6 -2
- package/docs/agents/commands.md +4 -3
- package/docs/agents/context-audit-checks.md +4 -4
- package/docs/agents/gov-detect.md +47 -0
- package/docs/agents/index.md +7 -4
- package/docs/agents/indexes.md +1 -1
- package/docs/agents/install-and-sync.md +10 -11
- package/docs/agents/key-changes.md +5 -5
- package/docs/agents/label-coverage.md +11 -0
- package/docs/agents/migrate.md +1 -1
- package/docs/agents/pr-evidence-layout.md +70 -0
- package/docs/agents/pr-evidence.md +8 -9
- package/docs/agents/pr-frames.md +7 -2
- package/docs/agents/pr-lift.md +132 -0
- package/docs/agents/records-push.md +2 -2
- package/docs/agents/records-stale.md +12 -47
- package/docs/agents/records-validate.md +3 -7
- package/docs/agents/records.md +5 -27
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/sandbox.md +4 -4
- package/docs/agents/scripting.md +2 -1
- package/docs/agents/skills-audit.md +1 -1
- package/docs/agents/tasks-plans.md +2 -2
- package/docs/agents/tasks.md +2 -2
- package/docs/target/migrations.md +10 -2
- package/docs/target/projects.md +4 -4
- package/docs/target/sync.md +1 -1
- package/docs/workflow/operating-model.md +2 -2
- package/docs/workflow/parallel-features.md +7 -7
- package/docs/workflow/skill-map-on-request.md +9 -9
- package/docs/workflow/skill-map.md +12 -11
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/detect.toml +201 -0
- package/governance/renames.toml +0 -11
- package/governance/rules/canon/context.md +13 -0
- package/governance/rules/claude/skill.md +17 -0
- package/governance/rules/code/000-code.md +1 -13
- package/governance/rules/code/010-testing.md +2 -30
- package/governance/rules/ui/400-ui.md +1 -1
- package/governance/rules/ui/480-canvas.md +13 -0
- package/governance/stacks/base.toml +2 -4
- package/package.json +4 -4
- package/scripts/core/install-check.sh +3 -3
- package/scripts/core/regen-hero.ts +0 -6
- package/scripts/lib/ui.sh +0 -168
- package/src/audits/catalog.ts +0 -1
- package/src/canvas/arrange.ts +66 -0
- package/src/canvas/capture.ts +20 -6
- package/src/canvas/content.ts +39 -0
- package/src/canvas/fit.ts +238 -0
- package/src/canvas/tokens.ts +28 -10
- package/src/capture/frame-values.ts +2 -32
- package/src/claude/cases/misc.ts +1 -1
- package/src/claude/cases/setup.ts +4 -0
- package/src/claude/cases/workflow.ts +6 -1
- package/src/claude/seeds.ts +1 -1
- package/src/claude/skills-audit.ts +2 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/ui.ts +2 -3
- package/src/commands/canvas.ts +179 -1
- package/src/commands/claude.ts +1 -1
- package/src/commands/gov.ts +146 -16
- package/src/commands/labels.ts +326 -7
- package/src/commands/migrate.ts +6 -1
- package/src/commands/pr.ts +377 -2
- package/src/commands/records.ts +13 -399
- package/src/commands/sandbox.ts +3 -6
- package/src/commands/tasks.ts +3 -3
- package/src/context/audit.ts +5 -5
- package/src/context/folders.ts +1 -1
- package/src/design/budget.ts +1 -1
- package/src/design/components.ts +1 -1
- package/src/design/document.ts +1 -1
- package/src/design/regen.ts +1 -1
- package/src/design/render.ts +1 -1
- package/src/feedback/body.ts +26 -5
- package/src/gate/measures.ts +59 -45
- package/src/gate/sequencer.ts +56 -1
- package/src/gate/stages.ts +4 -11
- package/src/gov/detect.ts +301 -0
- package/src/gov/install.ts +2 -3
- package/src/gov/list.ts +23 -2
- package/src/gov/stacks.ts +10 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/labels/apply.ts +39 -0
- package/src/labels/format.ts +3 -3
- package/src/labels/phase.ts +3 -3
- package/src/labels/sections.ts +43 -0
- package/src/migrate/skill-names.ts +20 -1
- package/src/pr/evidence.ts +218 -45
- package/src/pr/frames.ts +23 -3
- package/src/pr/lift.ts +418 -0
- package/src/pr/paths.ts +4 -4
- package/src/pr/rest.ts +3 -0
- package/src/pr/review-scope.ts +76 -0
- package/src/records/backup.ts +16 -6
- package/src/records/stale-canonical.ts +31 -1
- package/src/records/validate.ts +2 -188
- package/src/sandbox/census.ts +1 -1
- package/src/sandbox/coverage.ts +2 -6
- package/src/sandbox/equivalence.ts +19 -51
- package/src/sandbox/expect.ts +4 -4
- package/src/sandbox/provision.ts +13 -36
- package/src/sandbox/scenario.ts +26 -48
- package/src/sandbox/tree.ts +2 -6
- package/src/standards/read.ts +14 -1
- package/src/sync/git.ts +1 -1
- package/src/tasks/archive.ts +5 -5
- package/src/tasks/branch.ts +3 -3
- package/src/tasks/label.ts +1 -1
- package/src/tasks/record.ts +2 -2
- package/src/tasks/validate.ts +1 -1
- package/standards/index.md +0 -13
- package/standards/standard.md +1 -7
- package/tooling/base/reference.md +1 -1
- package/tooling/base/seeds/.github/pull_request_template.md +2 -2
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +9 -1
- package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +171 -0
- package/tooling/claude/seeds/.claude/settings.json +9 -0
- package/governance/rules/standards/context.md +0 -14
- package/governance/rules/standards/design.md +0 -12
- package/governance/rules/standards/readme.md +0 -12
- package/governance/rules/standards/rule.md +0 -13
- package/governance/rules/standards/skill.md +0 -21
- package/governance/rules/standards/standard.md +0 -13
- package/scripts/config.sh +0 -11
- package/scripts/lib/gov.sh +0 -14
- package/scripts/lib/sandbox-fixtures.sh +0 -191
- package/scripts/lib/sandbox-git.sh +0 -125
- package/scripts/lib/sandbox-path.sh +0 -50
- package/scripts/sandbox-hook.sh +0 -99
- package/src/gov/standard-rules.ts +0 -179
- package/src/records/migrate.ts +0 -78
- package/src/records/stale.ts +0 -243
- package/src/sandbox/hooks.ts +0 -128
- /package/{standards → claude/skills/draft-figure/references}/figures.md +0 -0
- /package/{standards → claude/skills/git-branch/references}/slug.md +0 -0
- /package/{standards → claude/skills/git-commit/references}/commit.md +0 -0
- /package/{standards → claude/skills/git-commit/references}/versioning.md +0 -0
package/src/gov/list.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { basename, join } from 'node:path'
|
|
3
|
+
import { loadDetectEntries, staleDetectKeys } from '@/gov/detect'
|
|
3
4
|
import { listRuleSourcePaths, rulesSourceDir } from '@/gov/install'
|
|
4
5
|
import {
|
|
5
6
|
expandStackEntry,
|
|
@@ -15,17 +16,26 @@ export interface StackEntry {
|
|
|
15
16
|
readonly rules: string[]
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** The evidence `governance/detect.toml` records for one opt-in rule. */
|
|
20
|
+
export interface RuleDetect {
|
|
21
|
+
readonly deps: readonly string[]
|
|
22
|
+
readonly files: readonly string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export interface RuleEntry {
|
|
19
26
|
readonly name: string
|
|
20
27
|
readonly domain: string
|
|
21
28
|
readonly description: string
|
|
22
29
|
readonly paths: string[] | null
|
|
30
|
+
readonly detect: RuleDetect | null
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export interface GovCatalog {
|
|
26
34
|
readonly stacks: StackEntry[]
|
|
27
35
|
readonly rules: RuleEntry[]
|
|
28
36
|
readonly unreferenced: string[]
|
|
37
|
+
/** `detect.toml` keys naming no live rule, which the gate fails on. */
|
|
38
|
+
readonly staleDetect: string[]
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
/**
|
|
@@ -64,24 +74,34 @@ export function buildStackEntries(root: string): StackEntry[] {
|
|
|
64
74
|
|
|
65
75
|
/**
|
|
66
76
|
* Reads the catalog straight off the source tree. A rule's domain is the
|
|
67
|
-
* subdirectory it sits in, which is the band grouping install preserves
|
|
77
|
+
* subdirectory it sits in, which is the band grouping install preserves, and
|
|
78
|
+
* its `detect` entry is the evidence that proposes it, null for a rule
|
|
79
|
+
* `detect.toml` does not name.
|
|
68
80
|
*/
|
|
69
81
|
export function buildRuleEntries(root: string): RuleEntry[] {
|
|
70
82
|
const rulesRoot = rulesSourceDir(root)
|
|
83
|
+
const detect = new Map(
|
|
84
|
+
loadDetectEntries(root).map((entry) => [
|
|
85
|
+
entry.rule,
|
|
86
|
+
{ deps: entry.deps, files: entry.files },
|
|
87
|
+
]),
|
|
88
|
+
)
|
|
71
89
|
|
|
72
90
|
return listRuleSourcePaths(root).map((rel) => {
|
|
73
91
|
const frontmatter = parseFrontmatter(
|
|
74
92
|
readFileSync(join(rulesRoot, rel), 'utf8'),
|
|
75
93
|
)
|
|
76
94
|
const paths = frontmatter?.fields.paths
|
|
95
|
+
const name = basename(rel, '.md')
|
|
77
96
|
|
|
78
97
|
return {
|
|
79
|
-
name
|
|
98
|
+
name,
|
|
80
99
|
domain: rel.includes('/') ? rel.slice(0, rel.indexOf('/')) : '',
|
|
81
100
|
description: readField(frontmatter, 'description') ?? '',
|
|
82
101
|
paths: Array.isArray(paths)
|
|
83
102
|
? paths.filter((entry): entry is string => typeof entry === 'string')
|
|
84
103
|
: null,
|
|
104
|
+
detect: detect.get(name) ?? null,
|
|
85
105
|
}
|
|
86
106
|
})
|
|
87
107
|
}
|
|
@@ -91,6 +111,7 @@ export function buildGovCatalog(root: string): GovCatalog {
|
|
|
91
111
|
stacks: buildStackEntries(root),
|
|
92
112
|
rules: buildRuleEntries(root),
|
|
93
113
|
unreferenced: unreferencedRules(root),
|
|
114
|
+
staleDetect: staleDetectKeys(root),
|
|
94
115
|
}
|
|
95
116
|
}
|
|
96
117
|
|
package/src/gov/stacks.ts
CHANGED
|
@@ -79,7 +79,7 @@ export function loadGovStack(
|
|
|
79
79
|
* shape rather than two the caller has to tell apart.
|
|
80
80
|
*
|
|
81
81
|
* The directory wins over a rule file of the same name. They cannot collide
|
|
82
|
-
* while `
|
|
82
|
+
* while `claude/skills/create-rule/references/rule.md` requires a numeric prefix on a rule slug, since a
|
|
83
83
|
* band folder carries none.
|
|
84
84
|
*/
|
|
85
85
|
export function expandStackEntry(root: string, entry: string): string[] {
|
|
@@ -207,3 +207,12 @@ export function mergeExtraRules(
|
|
|
207
207
|
|
|
208
208
|
return merged
|
|
209
209
|
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Resolves `--add` names with no stack under them. It runs the same merge as
|
|
213
|
+
* the stack path against an empty list, so an add with no stack and a layered one
|
|
214
|
+
* split and dedupe their names identically.
|
|
215
|
+
*/
|
|
216
|
+
export function resolveExtraRules(add: string): string[] {
|
|
217
|
+
return mergeExtraRules([], add)
|
|
218
|
+
}
|
package/src/hooks/post-merge.ts
CHANGED
|
@@ -163,7 +163,7 @@ function archiveLines(
|
|
|
163
163
|
}
|
|
164
164
|
return (
|
|
165
165
|
`\n📋 Task not archived for #${number}: ${text(fields, 'message') ?? reason}\n` +
|
|
166
|
-
|
|
166
|
+
`Run /task-complete archive #${number} to resolve it by hand.\n\n`
|
|
167
167
|
)
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** What a label apply decided to do, before any write. */
|
|
2
|
+
export interface LabelDiff {
|
|
3
|
+
/** Earned labels the pull request lacks and the remote carries, so addable. */
|
|
4
|
+
readonly toAdd: readonly string[]
|
|
5
|
+
/** Every earned label the pull request lacks, whether or not it is addable. */
|
|
6
|
+
readonly owed: readonly string[]
|
|
7
|
+
/** Owed labels the remote does not carry, which are reported and never created. */
|
|
8
|
+
readonly missingOnRemote: readonly string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Diffs what a branch earns against what its pull request already carries and
|
|
13
|
+
* what the remote can attach.
|
|
14
|
+
*
|
|
15
|
+
* The diff only adds. A label on the pull request that the map does not earn
|
|
16
|
+
* is a person's decision and never appears here, so nothing in the result can
|
|
17
|
+
* be turned into a removal. Order follows `earned`, which the map declares, so
|
|
18
|
+
* two runs over one branch produce one list.
|
|
19
|
+
*
|
|
20
|
+
* A label the remote lacks is separated from the addable set rather than
|
|
21
|
+
* dropped, because the REST add endpoint creates any label it is handed and
|
|
22
|
+
* that write to the repository settings is not the map's call.
|
|
23
|
+
*/
|
|
24
|
+
export function diffLabels(
|
|
25
|
+
earned: readonly string[],
|
|
26
|
+
present: readonly string[],
|
|
27
|
+
remote: readonly string[],
|
|
28
|
+
): LabelDiff {
|
|
29
|
+
const has = new Set(present)
|
|
30
|
+
const carried = new Set(remote)
|
|
31
|
+
|
|
32
|
+
const owed = earned.filter((label) => !has.has(label))
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
owed,
|
|
36
|
+
toAdd: owed.filter((label) => carried.has(label)),
|
|
37
|
+
missingOnRemote: owed.filter((label) => !carried.has(label)),
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/labels/format.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which rule in `
|
|
2
|
+
* Which rule in `claude/skills/git-pr/references/pr.md`'s `## Title` section a title breaks.
|
|
3
3
|
*
|
|
4
4
|
* `structure` stands alone rather than beside the other three, because a
|
|
5
5
|
* title failing the shape match has no parsed `<type>`, `<scope>`, or
|
|
@@ -28,9 +28,9 @@ const LEADING_LETTERS = /^[A-Za-z]+/
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Grades a pull request title, or a commit subject sharing the same form,
|
|
31
|
-
* against `
|
|
31
|
+
* against `claude/skills/git-pr/references/pr.md`'s structure, casing, and length rules.
|
|
32
32
|
*
|
|
33
|
-
* Does not check `<type>` against `
|
|
33
|
+
* Does not check `<type>` against `claude/skills/git-commit/references/commit.md`'s fixed enum.
|
|
34
34
|
* `pr.md`'s own `## Title` section states format, casing, and length only,
|
|
35
35
|
* and names no type list of its own to check against.
|
|
36
36
|
*/
|
package/src/labels/phase.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { RECORD_ENTRIES, RECORD_ROOTS } from '@/roots/record'
|
|
|
3
3
|
import type { RecordRoot } from '@/roots/record'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* The two version namespaces `
|
|
6
|
+
* The two version namespaces `claude/skills/git-commit/references/versioning.md` keeps apart, and why a
|
|
7
7
|
* pull request cannot be sorted between them by pattern alone.
|
|
8
8
|
*
|
|
9
9
|
* A phase label (`v68.5`) and a semver reference (`v3.44`) both match
|
|
@@ -186,7 +186,7 @@ const RELEASE_TITLE_PREFIX = 'chore(main): release '
|
|
|
186
186
|
* phase label past this check. Together they name release-please's own act of
|
|
187
187
|
* opening the pull request, which nothing else on the remote can perform.
|
|
188
188
|
*/
|
|
189
|
-
function isReleasePullRequest(input: PhaseScanInput): boolean {
|
|
189
|
+
export function isReleasePullRequest(input: PhaseScanInput): boolean {
|
|
190
190
|
return (
|
|
191
191
|
input.headRefName.startsWith(RELEASE_HEAD_PREFIX) &&
|
|
192
192
|
input.title.startsWith(RELEASE_TITLE_PREFIX)
|
|
@@ -265,7 +265,7 @@ function sessionLinks(text: string): string[] {
|
|
|
265
265
|
* The split runs on the pull request rather than on each token. A release
|
|
266
266
|
* pull request's tokens are read as the semver references its generated body
|
|
267
267
|
* legitimately carries, and every other pull request's tokens are read as
|
|
268
|
-
* leaked phase labels, which is what `
|
|
268
|
+
* leaked phase labels, which is what `claude/skills/git-commit/references/versioning.md` names the
|
|
269
269
|
* defect this exists to catch.
|
|
270
270
|
*
|
|
271
271
|
* A release pull request reports no board reference either, and the ground is
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { linesOutsideFences } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The closed list of section headings a pull request body may carry, in the
|
|
5
|
+
* sentence case `claude/skills/git-pr/references/pr.md` states.
|
|
6
|
+
*/
|
|
7
|
+
const ALLOWED_HEADINGS: readonly string[] = [
|
|
8
|
+
'## Summary',
|
|
9
|
+
'## Key changes',
|
|
10
|
+
'## Technical context',
|
|
11
|
+
'## Testing',
|
|
12
|
+
'## For the reviewer',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
const REQUIRED_HEADINGS: readonly string[] = ['## Summary', '## Key changes']
|
|
16
|
+
|
|
17
|
+
const H2 = /^##\s/
|
|
18
|
+
|
|
19
|
+
export interface SectionsCheck {
|
|
20
|
+
/** Level-two headings the closed list does not hold, as written. */
|
|
21
|
+
readonly outside: readonly string[]
|
|
22
|
+
/** Required headings the body never carries. */
|
|
23
|
+
readonly missing: readonly string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Grades the level-two headings of a pull request body against the closed list.
|
|
28
|
+
*
|
|
29
|
+
* The comparison is exact, so `## Key Changes` fails. `canon pr key-changes`
|
|
30
|
+
* reads its section case-insensitively, which keeps a body posted before the
|
|
31
|
+
* rename parsing while the scan holds new text to one spelling. Subheadings and
|
|
32
|
+
* fenced lines are not headings, so the list governs level two alone.
|
|
33
|
+
*/
|
|
34
|
+
export function checkSections(body: string): SectionsCheck {
|
|
35
|
+
const headings = linesOutsideFences(body)
|
|
36
|
+
.map((line) => line.trim())
|
|
37
|
+
.filter((line) => H2.test(line))
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
outside: headings.filter((heading) => !ALLOWED_HEADINGS.includes(heading)),
|
|
41
|
+
missing: REQUIRED_HEADINGS.filter((heading) => !headings.includes(heading)),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -99,6 +99,25 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
99
99
|
'draft-diagram': 'draft-figure',
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The skills that took a two-word name from a single word, which the map above
|
|
104
|
+
* may not carry as a bare key.
|
|
105
|
+
*
|
|
106
|
+
* `canvas` became `canvas-board`, and a bare `canvas` key would rewrite every
|
|
107
|
+
* unrelated use of the word: `canon canvas serve`, `src/canvas/`,
|
|
108
|
+
* `.canon/canvas/`, and the `internal-canvas` skill. So each row keys on a form
|
|
109
|
+
* only the skill spells, a plugin invocation or a skill folder path. The tail
|
|
110
|
+
* guard rejects a following `-`, so a rewritten `canon:canvas-board` is not
|
|
111
|
+
* matched again.
|
|
112
|
+
*
|
|
113
|
+
* What this leaves behind is a prose mention of the skill by its bare name,
|
|
114
|
+
* which has no token for a sweep to find and is fixed by hand.
|
|
115
|
+
*/
|
|
116
|
+
export const SKILL_CITATION_MAP: Readonly<Record<string, string>> = {
|
|
117
|
+
'canon:canvas': 'canon:canvas-board',
|
|
118
|
+
'skills/canvas': 'skills/canvas-board',
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
/**
|
|
103
122
|
* The skill rename.
|
|
104
123
|
*
|
|
@@ -132,7 +151,7 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
132
151
|
* listing every sweep reads never reaches them.
|
|
133
152
|
*/
|
|
134
153
|
export const SKILL_NAME_RULES: RenameRules = defineRenameRules({
|
|
135
|
-
replacements: SKILL_NAME_MAP,
|
|
154
|
+
replacements: { ...SKILL_NAME_MAP, ...SKILL_CITATION_MAP },
|
|
136
155
|
keepMarker: 'canon-keep-retired',
|
|
137
156
|
wholeToken: true,
|
|
138
157
|
excludedPrefixes: ['scripts/eval/result-'],
|
package/src/pr/evidence.ts
CHANGED
|
@@ -188,23 +188,218 @@ function evidenceImage(url: string, alt: string, width?: number): string {
|
|
|
188
188
|
|
|
189
189
|
const EVIDENCE_IMAGE = String.raw`(?:!\[\]\([^)]*\)|<img [^>]*>)`
|
|
190
190
|
|
|
191
|
-
/** A case row in
|
|
192
|
-
const
|
|
191
|
+
/** A case row in a shape before the head image carried an alt: a Base cell before the head image, or the head image alone. */
|
|
192
|
+
const LEGACY_ROW_PATTERN = new RegExp(
|
|
193
193
|
String.raw`^\| .+ \| (?:(?:\*\(new\)\*|${EVIDENCE_IMAGE}) \| )?${EVIDENCE_IMAGE} \|$`,
|
|
194
|
-
'gm',
|
|
195
194
|
)
|
|
196
195
|
|
|
196
|
+
/** A head image, which every render since the alt text was added names with a trailing `head`. A row carries one per case it holds. */
|
|
197
|
+
const HEAD_IMAGE_PATTERN = /<img [^>]*alt="[^"]* head">/g
|
|
198
|
+
|
|
199
|
+
/** The form a case name takes when it names a view, a theme, and a width. Anchored on the right, since a view may carry hyphens. */
|
|
200
|
+
const CASE_NAME = /^(.+)-(dark|light)-(\d+)$/
|
|
201
|
+
|
|
202
|
+
const THEMES = ['dark', 'light'] as const
|
|
203
|
+
|
|
204
|
+
type Theme = (typeof THEMES)[number]
|
|
205
|
+
|
|
206
|
+
interface ViewCase {
|
|
207
|
+
readonly item: EvidenceItem
|
|
208
|
+
readonly theme: Theme
|
|
209
|
+
readonly width: string
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isTheme(value: string): value is Theme {
|
|
213
|
+
return value === 'dark' || value === 'light'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function readCaseName(stem: string):
|
|
217
|
+
| {
|
|
218
|
+
readonly view: string
|
|
219
|
+
readonly theme: Theme
|
|
220
|
+
readonly width: string
|
|
221
|
+
}
|
|
222
|
+
| undefined {
|
|
223
|
+
const match = CASE_NAME.exec(stem)
|
|
224
|
+
const view = match?.[1]
|
|
225
|
+
const theme = match?.[2]
|
|
226
|
+
const width = match?.[3]
|
|
227
|
+
if (view === undefined || theme === undefined || width === undefined) {
|
|
228
|
+
return undefined
|
|
229
|
+
}
|
|
230
|
+
return isTheme(theme) ? { view, theme, width } : undefined
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function compareWidths(a: string, b: string): number {
|
|
234
|
+
return Number(a) - Number(b) || a.localeCompare(b)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function commitLink(repo: string, sha: string): string {
|
|
238
|
+
return `[\`${sha.slice(0, SHORT_SHA_LENGTH)}\`](https://github.com/${repo}/commit/${sha})`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function baseImage(item: EvidenceItem, repo: string, base: string): string {
|
|
242
|
+
return item.added
|
|
243
|
+
? '*(new)*'
|
|
244
|
+
: evidenceImage(
|
|
245
|
+
rawUrl(repo, base, item.path),
|
|
246
|
+
`${escapeCell(item.stem)} base`,
|
|
247
|
+
item.baseWidth,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function headImage(item: EvidenceItem, repo: string, head: string): string {
|
|
252
|
+
return evidenceImage(
|
|
253
|
+
rawUrl(repo, head, item.path),
|
|
254
|
+
`${escapeCell(item.stem)} head`,
|
|
255
|
+
item.width,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function block(
|
|
260
|
+
opener: string,
|
|
261
|
+
summary: string,
|
|
262
|
+
header: readonly string[],
|
|
263
|
+
rows: readonly string[],
|
|
264
|
+
): string {
|
|
265
|
+
return [
|
|
266
|
+
opener,
|
|
267
|
+
`<summary>${summary}</summary>`,
|
|
268
|
+
'',
|
|
269
|
+
`| ${header.join(' | ')} |`,
|
|
270
|
+
`| ${header.map(() => '---').join(' | ')} |`,
|
|
271
|
+
...rows,
|
|
272
|
+
'',
|
|
273
|
+
'</details>',
|
|
274
|
+
].join('\n')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** One block for the cases of a state that did not name a view, one case per row. */
|
|
278
|
+
function renderFlat(
|
|
279
|
+
label: string,
|
|
280
|
+
items: readonly EvidenceItem[],
|
|
281
|
+
repo: string,
|
|
282
|
+
base: string,
|
|
283
|
+
head: string,
|
|
284
|
+
opener: string,
|
|
285
|
+
): string {
|
|
286
|
+
const hasBase = items.some((item) => !item.added)
|
|
287
|
+
const rows = items.map((item) => {
|
|
288
|
+
const stem = escapeCell(item.stem)
|
|
289
|
+
const after = headImage(item, repo, head)
|
|
290
|
+
return hasBase
|
|
291
|
+
? `| ${stem} | ${baseImage(item, repo, base)} | ${after} |`
|
|
292
|
+
: `| ${stem} | ${after} |`
|
|
293
|
+
})
|
|
294
|
+
return block(
|
|
295
|
+
opener,
|
|
296
|
+
`${label} (${items.length})`,
|
|
297
|
+
hasBase ? ['Case', 'Base', 'Head'] : ['Case', 'Head'],
|
|
298
|
+
rows,
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** One block for a view, a width per row and a theme per column, each theme taking a base column when the view already exists at the base. */
|
|
303
|
+
function renderView(
|
|
304
|
+
label: string,
|
|
305
|
+
cases: readonly ViewCase[],
|
|
306
|
+
repo: string,
|
|
307
|
+
base: string,
|
|
308
|
+
head: string,
|
|
309
|
+
opener: string,
|
|
310
|
+
): string {
|
|
311
|
+
const themes = THEMES.filter((theme) =>
|
|
312
|
+
cases.some((entry) => entry.theme === theme),
|
|
313
|
+
)
|
|
314
|
+
const hasBase = cases.some((entry) => !entry.item.added)
|
|
315
|
+
const widths = [...new Set(cases.map((entry) => entry.width))].sort(
|
|
316
|
+
compareWidths,
|
|
317
|
+
)
|
|
318
|
+
const header = [
|
|
319
|
+
'Width',
|
|
320
|
+
...themes.flatMap((theme) => {
|
|
321
|
+
const name = theme === 'dark' ? 'Dark' : 'Light'
|
|
322
|
+
return hasBase ? [`${name} base`, `${name} head`] : [name]
|
|
323
|
+
}),
|
|
324
|
+
]
|
|
325
|
+
const rows = widths.map((width) => {
|
|
326
|
+
const cells = themes.flatMap((theme) => {
|
|
327
|
+
const found = cases.find(
|
|
328
|
+
(entry) => entry.theme === theme && entry.width === width,
|
|
329
|
+
)
|
|
330
|
+
if (found === undefined) return hasBase ? ['', ''] : ['']
|
|
331
|
+
const after = headImage(found.item, repo, head)
|
|
332
|
+
return hasBase ? [baseImage(found.item, repo, base), after] : [after]
|
|
333
|
+
})
|
|
334
|
+
return `| ${[width, ...cells].join(' | ')} |`
|
|
335
|
+
})
|
|
336
|
+
return block(opener, `${label} (${cases.length})`, header, rows)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Renders the blocks of one state: a block per view the case names split
|
|
341
|
+
* into, then one flat block for the names that did not. Nothing is dropped,
|
|
342
|
+
* so a state of unparsed names reads as the flat table it always was.
|
|
343
|
+
*/
|
|
344
|
+
function renderState(
|
|
345
|
+
entry: EvidenceState,
|
|
346
|
+
repo: string,
|
|
347
|
+
base: string,
|
|
348
|
+
head: string,
|
|
349
|
+
opener: string,
|
|
350
|
+
): string[] {
|
|
351
|
+
const views = new Map<string, ViewCase[]>()
|
|
352
|
+
const flat: EvidenceItem[] = []
|
|
353
|
+
const taken = new Set<string>()
|
|
354
|
+
for (const item of entry.items) {
|
|
355
|
+
const name = readCaseName(item.stem)
|
|
356
|
+
// Two files can share a stem across extensions, and a view cell holds one.
|
|
357
|
+
const cell =
|
|
358
|
+
name === undefined ? '' : `${name.view}|${name.theme}|${name.width}`
|
|
359
|
+
if (name === undefined || taken.has(cell)) {
|
|
360
|
+
flat.push(item)
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
taken.add(cell)
|
|
364
|
+
const cases = views.get(name.view)
|
|
365
|
+
const viewCase = { item, theme: name.theme, width: name.width }
|
|
366
|
+
if (cases === undefined) views.set(name.view, [viewCase])
|
|
367
|
+
else cases.push(viewCase)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const prefix = entry.state === '' ? '' : `${escapeCell(entry.state)} / `
|
|
371
|
+
const blocks = [...views.entries()]
|
|
372
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
373
|
+
.map(([view, cases]) =>
|
|
374
|
+
renderView(
|
|
375
|
+
`${prefix}${escapeCell(view)}`,
|
|
376
|
+
cases,
|
|
377
|
+
repo,
|
|
378
|
+
base,
|
|
379
|
+
head,
|
|
380
|
+
opener,
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
if (flat.length > 0) {
|
|
384
|
+
const label = entry.state === '' ? 'evidence' : escapeCell(entry.state)
|
|
385
|
+
blocks.push(renderFlat(label, flat, repo, base, head, opener))
|
|
386
|
+
}
|
|
387
|
+
return blocks
|
|
388
|
+
}
|
|
389
|
+
|
|
197
390
|
export function evidenceMarker(head: string): string {
|
|
198
391
|
return `${MARKER_PREFIX} head=${head} -->`
|
|
199
392
|
}
|
|
200
393
|
|
|
201
394
|
/**
|
|
202
|
-
* Renders the whole comment body: one `<details>` block per
|
|
203
|
-
*
|
|
395
|
+
* Renders the whole comment body: one `<details>` block per view within each
|
|
396
|
+
* state, a width per row with a column per theme, a flat Base/Head table for
|
|
397
|
+
* the case names that do not split into a view, and the trailing marker naming
|
|
398
|
+
* the head this body describes.
|
|
204
399
|
* Every state opens while the comment carries at most `EVIDENCE_OPEN_LIMIT`
|
|
205
400
|
* images, one per row for the head plus one per row not added, and every state
|
|
206
401
|
* closes past it, so a comment is never half open. A visible line under
|
|
207
|
-
* `## Evidence` names the base and head as short shas. Every image URL is
|
|
402
|
+
* `## Evidence` names the base and head as short shas, each linking its commit. Every image URL is
|
|
208
403
|
* pinned to a commit sha rather than a branch, so the comment keeps showing
|
|
209
404
|
* what it claimed even after the branch moves.
|
|
210
405
|
*
|
|
@@ -252,42 +447,13 @@ export function renderEvidenceBody(
|
|
|
252
447
|
const opener =
|
|
253
448
|
imageCount <= EVIDENCE_OPEN_LIMIT ? '<details open>' : '<details>'
|
|
254
449
|
|
|
255
|
-
const sections = states.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const stem = escapeCell(item.stem)
|
|
259
|
-
const before = item.added
|
|
260
|
-
? '*(new)*'
|
|
261
|
-
: evidenceImage(
|
|
262
|
-
rawUrl(repo, base, item.path),
|
|
263
|
-
`${stem} base`,
|
|
264
|
-
item.baseWidth,
|
|
265
|
-
)
|
|
266
|
-
const after = evidenceImage(
|
|
267
|
-
rawUrl(repo, head, item.path),
|
|
268
|
-
`${stem} head`,
|
|
269
|
-
item.width,
|
|
270
|
-
)
|
|
271
|
-
return hasBase
|
|
272
|
-
? `| ${stem} | ${before} | ${after} |`
|
|
273
|
-
: `| ${stem} | ${after} |`
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
return [
|
|
277
|
-
opener,
|
|
278
|
-
`<summary>${entry.state === '' ? 'evidence' : entry.state} (${entry.items.length})</summary>`,
|
|
279
|
-
'',
|
|
280
|
-
hasBase ? '| Case | Base | Head |' : '| Case | Head |',
|
|
281
|
-
hasBase ? '| --- | --- | --- |' : '| --- | --- |',
|
|
282
|
-
...rows,
|
|
283
|
-
'',
|
|
284
|
-
'</details>',
|
|
285
|
-
].join('\n')
|
|
286
|
-
})
|
|
450
|
+
const sections = states.flatMap((entry) =>
|
|
451
|
+
renderState(entry, repo, base, head, opener),
|
|
452
|
+
)
|
|
287
453
|
|
|
288
454
|
return [
|
|
289
455
|
...opening,
|
|
290
|
-
`${BASE_PREFIX}
|
|
456
|
+
`${BASE_PREFIX} ${commitLink(repo, base)} · **Head:** ${commitLink(repo, head)}`,
|
|
291
457
|
'',
|
|
292
458
|
...sections,
|
|
293
459
|
'',
|
|
@@ -297,14 +463,21 @@ export function renderEvidenceBody(
|
|
|
297
463
|
}
|
|
298
464
|
|
|
299
465
|
/**
|
|
300
|
-
* How many
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* before the change still counts its cases and the
|
|
466
|
+
* How many cases a rendered body carries. Every case has exactly one head
|
|
467
|
+
* image, so a row counts the head images it holds, which is one in the flat
|
|
468
|
+
* table and up to two in a view's row. A row from before the alt text named
|
|
469
|
+
* its head image counts as one case when it matches the old row shape, so a
|
|
470
|
+
* comment posted before the change still counts its cases and the
|
|
471
|
+
* `would-empty` guard holds.
|
|
305
472
|
*/
|
|
306
473
|
export function countEvidenceCases(body: string): number {
|
|
307
|
-
|
|
474
|
+
let total = 0
|
|
475
|
+
for (const line of body.split('\n')) {
|
|
476
|
+
if (!line.startsWith('| ')) continue
|
|
477
|
+
const heads = line.match(HEAD_IMAGE_PATTERN)?.length ?? 0
|
|
478
|
+
total += heads > 0 ? heads : LEGACY_ROW_PATTERN.test(line) ? 1 : 0
|
|
479
|
+
}
|
|
480
|
+
return total
|
|
308
481
|
}
|
|
309
482
|
|
|
310
483
|
export interface EvidenceComment {
|
package/src/pr/frames.ts
CHANGED
|
@@ -20,6 +20,10 @@ const FOLDER_PATTERN = /^pr-(\d+)$/
|
|
|
20
20
|
|
|
21
21
|
const DAY_MS = 86_400_000
|
|
22
22
|
|
|
23
|
+
const THEMES = ['dark', 'light'] as const
|
|
24
|
+
|
|
25
|
+
export type FrameTheme = (typeof THEMES)[number]
|
|
26
|
+
|
|
23
27
|
export interface TreeEntry {
|
|
24
28
|
readonly path: string
|
|
25
29
|
readonly mode: string
|
|
@@ -71,6 +75,7 @@ export interface FrameInput {
|
|
|
71
75
|
readonly head: string
|
|
72
76
|
readonly pass: string
|
|
73
77
|
readonly box: number
|
|
78
|
+
readonly theme?: FrameTheme
|
|
74
79
|
readonly bytes: Uint8Array
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -113,18 +118,27 @@ export function isFramePass(pass: string): boolean {
|
|
|
113
118
|
return PASS_PATTERN.test(pass)
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
/** Whether a value names a theme a box can be shot at, the only theme segment the path takes. */
|
|
122
|
+
export function isFrameTheme(theme: string): theme is FrameTheme {
|
|
123
|
+
return (THEMES as readonly string[]).includes(theme)
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
/**
|
|
117
127
|
* Where a frame lands. The pass segment keeps a second pass at the same head
|
|
118
128
|
* and box from replacing the image an earlier review comment shows, since a
|
|
119
|
-
* branch-path link reads whatever the path holds now.
|
|
129
|
+
* branch-path link reads whatever the path holds now. The theme segment keeps
|
|
130
|
+
* two shots of one box in one pass apart, and a frame without one keeps the
|
|
131
|
+
* plain `box-<n>.png` form.
|
|
120
132
|
*/
|
|
121
133
|
export function framePath(
|
|
122
134
|
number: number,
|
|
123
135
|
head: string,
|
|
124
136
|
pass: string,
|
|
125
137
|
box: number,
|
|
138
|
+
theme?: FrameTheme,
|
|
126
139
|
): string {
|
|
127
|
-
|
|
140
|
+
const name = theme === undefined ? `box-${box}` : `box-${box}-${theme}`
|
|
141
|
+
return `pr-${number}/${head.slice(0, SHORT_HEAD_LENGTH)}/${pass}/${name}.png`
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
/**
|
|
@@ -160,7 +174,13 @@ export async function pushFrame(
|
|
|
160
174
|
api: FramesApi,
|
|
161
175
|
input: FrameInput,
|
|
162
176
|
): Promise<PushResult> {
|
|
163
|
-
const path = framePath(
|
|
177
|
+
const path = framePath(
|
|
178
|
+
input.number,
|
|
179
|
+
input.head,
|
|
180
|
+
input.pass,
|
|
181
|
+
input.box,
|
|
182
|
+
input.theme,
|
|
183
|
+
)
|
|
164
184
|
const blob = await api.createBlob(Buffer.from(input.bytes).toString('base64'))
|
|
165
185
|
if (blob === undefined) return { kind: 'refused', reason: 'push-failed' }
|
|
166
186
|
const entry: TreeEntry = { path, mode: '100644', type: 'blob', sha: blob }
|