@erclx/canon 5.26.0 → 5.27.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 +2 -0
- 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 +6 -6
- 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/SKILL.md +3 -7
- package/claude/skills/review-address/references/post-review-findings.md +16 -0
- 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 +2 -2
- 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/SKILL.md +3 -1
- package/claude/skills/role-orchestrator/SKILL.md +5 -14
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +5 -33
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -21
- 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-watch.md +26 -0
- package/claude/skills/role-planner/SKILL.md +2 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +2 -0
- package/claude/skills/role-worker/SKILL.md +2 -0
- 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 +2 -0
- 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 +23 -7
- package/docs/agents/capture.md +5 -5
- package/docs/agents/commands.md +2 -2
- package/docs/agents/index.md +3 -3
- package/docs/agents/install-and-sync.md +7 -0
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/pr-evidence.md +38 -15
- package/docs/agents/pr-local.md +19 -13
- package/docs/agents/pr-preview.md +44 -21
- package/docs/agents/sandbox.md +28 -4
- package/docs/agents/scripting.md +8 -1
- package/docs/agents/skills-audit.md +2 -1
- package/docs/workflow/operating-model.md +5 -6
- package/docs/workflow/parallel-features.md +22 -1
- 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/run.ts +1 -1
- package/src/canvas/address.ts +12 -3
- 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 +5 -1
- package/src/canvas/client/inspector/fields.css +395 -6
- package/src/canvas/client/inspector/size-field.tsx +69 -0
- package/src/canvas/client/inspector/values.ts +43 -1
- package/src/canvas/client/inspector.tsx +190 -4
- package/src/canvas/client/selection.tsx +197 -0
- package/src/canvas/client/shell.css +104 -12
- package/src/canvas/client/state.ts +69 -9
- package/src/canvas/client/surface.tsx +147 -22
- package/src/canvas/content.ts +50 -1
- package/src/canvas/edit.ts +37 -4
- 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 +59 -17
- package/src/census/count.ts +1 -1
- package/src/claude/seeds.ts +1 -1
- 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/commands/audits.ts +2 -2
- package/src/commands/canvas.ts +179 -2
- package/src/commands/claude.ts +67 -9
- package/src/commands/design.ts +142 -2
- package/src/commands/gov.ts +1 -1
- package/src/commands/hooks.ts +1 -1
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/intake.ts +1 -1
- package/src/commands/migrate.ts +1 -1
- package/src/commands/pr.ts +105 -58
- package/src/commands/records.ts +1 -1
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +54 -2
- package/src/commands/sessions.ts +1 -1
- package/src/commands/sync.ts +1 -1
- package/src/commands/tasks.ts +2 -2
- package/src/commands/teach.ts +1 -1
- package/src/commands/transcripts.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/citations.ts +1 -1
- package/src/context/classify/extract.ts +2 -2
- package/src/design/project-fonts.ts +392 -0
- package/src/feedback/github.ts +1 -1
- package/src/gate/measures.ts +47 -9
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +9 -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/labels/audit.ts +1 -1
- package/src/markdown/files.ts +1 -1
- package/src/migrate/apply.ts +1 -1
- package/src/pr/bijection.ts +1 -1
- package/src/pr/evidence.ts +96 -43
- package/src/pr/local.ts +16 -10
- package/src/pr/preview.ts +78 -2
- package/src/records/backup.ts +1 -1
- package/src/records/stale-canonical.ts +1 -1
- 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/provision.ts +12 -1
- package/src/sandbox/tree.ts +4 -8
- package/src/secrets/scan.ts +1 -1
- package/src/sessions/claim.ts +1 -1
- package/src/sessions/resolve.ts +1 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/slides/package/fonts.ts +6 -2
- package/src/standards/audit.ts +2 -2
- package/src/sync/check.ts +2 -2
- package/src/sync/engine.ts +2 -2
- package/src/sync/git.ts +1 -1
- package/src/sync/history.ts +1 -1
- package/src/sync/reverse.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/tasks/reach.ts +1 -1
- package/src/tasks/trunk.ts +1 -1
- package/src/tooling/inject.ts +1 -1
- package/src/tooling/subfolder.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/canvas.md +2 -1
- 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/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/{copy.ts → sync/copy.ts} +0 -0
- /package/src/{target.ts → targets/validate.ts} +0 -0
package/src/gate/measures.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
CLIENT_COMMANDS,
|
|
7
7
|
type ClientCommand,
|
|
8
8
|
clientCommandCitationsIn,
|
|
9
|
-
} from '@/client-commands'
|
|
9
|
+
} from '@/gate/client-commands'
|
|
10
10
|
import {
|
|
11
11
|
isOverCount,
|
|
12
12
|
isOverLength,
|
|
@@ -23,9 +23,10 @@ import {
|
|
|
23
23
|
PRACTICE_SKILLS,
|
|
24
24
|
SHIPPED_CORPUS,
|
|
25
25
|
} from '@/claude/skills-audit'
|
|
26
|
+
import { familyKeys } from '@/claude/skills-families'
|
|
26
27
|
import { measureDesignProse } from '@/design/budget'
|
|
27
28
|
import { DESIGN_DOCUMENT } from '@/design/regen'
|
|
28
|
-
import { listRepositoryFiles } from '@/git
|
|
29
|
+
import { listRepositoryFiles } from '@/git/files'
|
|
29
30
|
import { ceilingFindings } from '@/markdown/ceiling'
|
|
30
31
|
import { resolveMarkdown } from '@/markdown/files'
|
|
31
32
|
import { CHECKPOINTS } from '@/markdown/structure'
|
|
@@ -166,7 +167,7 @@ export function auditsBaselineRel(root: string): string {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
export const CAPTURE_STAMP_FAILURE =
|
|
169
|
-
'A capture set disagrees with the stamp written when its image was captured. Run canon capture assets/captures --selector .window --out assets/
|
|
170
|
+
'A capture set disagrees with the stamp written when its image was captured. Run canon capture assets/captures --selector .window --out assets/frames and commit each frame with its image and its stamp.'
|
|
170
171
|
|
|
171
172
|
function parseJson(payload: string): unknown {
|
|
172
173
|
try {
|
|
@@ -449,6 +450,42 @@ export const skillPracticeShape: Measure = async (ctx) => {
|
|
|
449
450
|
}
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Every shipped skill against the family vocabulary, since the landing page
|
|
455
|
+
* groups the skill map by the field and a skill without one has no group.
|
|
456
|
+
*
|
|
457
|
+
* Read in-process for the reason `skillProvenance` is. Only the shipped corpus
|
|
458
|
+
* takes a family, so a tree without `claude/skills/` reports unmeasured rather
|
|
459
|
+
* than a vacuous pass.
|
|
460
|
+
*/
|
|
461
|
+
export const skillFamily: Measure = async (ctx) => {
|
|
462
|
+
const report = await auditSkills(ctx.root)
|
|
463
|
+
const shipped = report.corpora.find((corpus) => corpus.rel === SHIPPED_CORPUS)
|
|
464
|
+
if (shipped === undefined) {
|
|
465
|
+
return {
|
|
466
|
+
emissions: [],
|
|
467
|
+
unmeasured: `no skill corpus at ${SHIPPED_CORPUS} resolved, so no family was read.`,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const found = report.family
|
|
472
|
+
if (found.length > 0) {
|
|
473
|
+
const where = found
|
|
474
|
+
.map((finding) => `${finding.rel} ${finding.detail}`)
|
|
475
|
+
.join(', ')
|
|
476
|
+
return {
|
|
477
|
+
emissions: [],
|
|
478
|
+
failure: `${plural(found.length, 'skill family finding')}: ${where}. Add metadata.family to the frontmatter, one of ${familyKeys().join(', ')}, matching the skill map group its row sits under.`,
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
emissions: [
|
|
484
|
+
info(`${plural(shipped.skills, 'shipped skill')} declare a known family`),
|
|
485
|
+
],
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
452
489
|
/**
|
|
453
490
|
* Every tracked markdown document against the whole-document ceiling.
|
|
454
491
|
*
|
|
@@ -982,7 +1019,7 @@ export const clientCommandCitations = async (
|
|
|
982
1019
|
return {
|
|
983
1020
|
emissions: [],
|
|
984
1021
|
failure:
|
|
985
|
-
'The client command table is empty, so the corpus was walked and nothing was looked for. Check src/client-commands.ts.',
|
|
1022
|
+
'The client command table is empty, so the corpus was walked and nothing was looked for. Check src/gate/client-commands.ts.',
|
|
986
1023
|
}
|
|
987
1024
|
}
|
|
988
1025
|
|
|
@@ -1023,8 +1060,8 @@ export const clientCommandCitations = async (
|
|
|
1023
1060
|
),
|
|
1024
1061
|
failure:
|
|
1025
1062
|
found.length === 1
|
|
1026
|
-
? `One tracked citation quotes a client command with the wrong argument. Match it against the canonical form in src/client-commands.ts, or mark the line ${CLIENT_COMMAND_MARKER}: <reason> where the argument differs on purpose.`
|
|
1027
|
-
: `${found.length} tracked citations quote a client command with the wrong argument. Match each against the canonical form in src/client-commands.ts, or mark the line ${CLIENT_COMMAND_MARKER}: <reason> where the argument differs on purpose.`,
|
|
1063
|
+
? `One tracked citation quotes a client command with the wrong argument. Match it against the canonical form in src/gate/client-commands.ts, or mark the line ${CLIENT_COMMAND_MARKER}: <reason> where the argument differs on purpose.`
|
|
1064
|
+
: `${found.length} tracked citations quote a client command with the wrong argument. Match each against the canonical form in src/gate/client-commands.ts, or mark the line ${CLIENT_COMMAND_MARKER}: <reason> where the argument differs on purpose.`,
|
|
1028
1065
|
}
|
|
1029
1066
|
}
|
|
1030
1067
|
|
|
@@ -1478,11 +1515,12 @@ async function collectPluginManifests(ctx: MeasureContext): Promise<string[]> {
|
|
|
1478
1515
|
* A capture set spans two folders. The markup and the template beside it are
|
|
1479
1516
|
* the authored half and sit under `assets/captures/`, while the image a
|
|
1480
1517
|
* document points at and the stamp answering for it sit under
|
|
1481
|
-
* `assets/
|
|
1482
|
-
* `
|
|
1518
|
+
* `assets/frames/`, which is where `--out assets/frames` sends them. They sit
|
|
1519
|
+
* outside `assets/evidence/` so a changed marketing image is not read as dev
|
|
1520
|
+
* evidence.
|
|
1483
1521
|
*/
|
|
1484
1522
|
const CAPTURE_MARKUP_DIR = 'assets/captures'
|
|
1485
|
-
const CAPTURE_OUTPUT_DIR = 'assets/
|
|
1523
|
+
const CAPTURE_OUTPUT_DIR = 'assets/frames'
|
|
1486
1524
|
|
|
1487
1525
|
/**
|
|
1488
1526
|
* Every capture under `assets/captures/`, named by the base its three files
|
package/src/gate/sequencer.ts
CHANGED
package/src/gate/stages.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
seedStandards,
|
|
23
23
|
rawFieldFileReference,
|
|
24
24
|
shippedReferences,
|
|
25
|
+
skillFamily,
|
|
25
26
|
skillPracticeShape,
|
|
26
27
|
skillProvenance,
|
|
27
28
|
standardCriteria,
|
|
@@ -496,6 +497,14 @@ export const STAGES: readonly Stage[] = [
|
|
|
496
497
|
label: 'Skill practice shape',
|
|
497
498
|
checks: [{ kind: 'measure', measure: skillPracticeShape }],
|
|
498
499
|
},
|
|
500
|
+
{
|
|
501
|
+
// The audit verb only reports a shipped skill with a missing or unknown
|
|
502
|
+
// family, so a target is told without failing. The landing page groups the
|
|
503
|
+
// skill map by the field, so the stage holds this corpus at zero.
|
|
504
|
+
id: 'skill-family',
|
|
505
|
+
label: 'Skill family',
|
|
506
|
+
checks: [{ kind: 'measure', measure: skillFamily }],
|
|
507
|
+
},
|
|
499
508
|
{
|
|
500
509
|
id: 'standard-criteria',
|
|
501
510
|
label: 'Standard success criteria',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $ } from 'bun'
|
|
2
|
-
import { baseCandidates, isMergeBase } from '@/git
|
|
3
|
-
import { gitEnv } from '@/git
|
|
2
|
+
import { baseCandidates, isMergeBase } from '@/git/base'
|
|
3
|
+
import { gitEnv } from '@/git/env'
|
|
4
4
|
|
|
5
5
|
/** Runs git under `root` with the resolution variables a hook exports stripped. */
|
|
6
6
|
async function git(
|
package/src/gov/citations.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { dirname, join, resolve } from 'node:path'
|
|
4
4
|
import { $ } from 'bun'
|
|
5
5
|
import { isMarked } from '@/sweep/exempt-marker'
|
|
6
|
-
import { gitEnv } from '@/git
|
|
6
|
+
import { gitEnv } from '@/git/env'
|
|
7
7
|
import { listRuleFiles } from '@/gov/payload'
|
|
8
8
|
import { parseFrontmatter } from '@/indexes/frontmatter'
|
|
9
9
|
import { findBacktickSpans } from '@/markdown/backticks'
|
package/src/gov/install.ts
CHANGED
package/src/gov/superseded.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { isBinary } from '@/sweep/binary'
|
|
4
4
|
import { isMarked } from '@/sweep/exempt-marker'
|
|
5
|
-
import { listRepositoryFiles } from '@/git
|
|
5
|
+
import { listRepositoryFiles } from '@/git/files'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The inline token exempting one line from this sweep, shaped on the
|
package/src/gov/test-order.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execaSync } from 'execa'
|
|
2
|
-
import { baseCandidates, isMergeBase } from '@/git
|
|
3
|
-
import { gitEnv } from '@/git
|
|
2
|
+
import { baseCandidates, isMergeBase } from '@/git/base'
|
|
3
|
+
import { gitEnv } from '@/git/env'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The extensions this check can pair. A test sits beside its subject under one
|
package/src/help.ts
CHANGED
|
@@ -105,7 +105,7 @@ const EXAMPLES = [
|
|
|
105
105
|
'canon canvas list --json',
|
|
106
106
|
'canon slides render',
|
|
107
107
|
'canon slides list --json',
|
|
108
|
-
'canon capture assets/captures/install.html --selector .window --out assets/
|
|
108
|
+
'canon capture assets/captures/install.html --selector .window --out assets/frames',
|
|
109
109
|
'canon serve .canon/teach',
|
|
110
110
|
'canon inventory focus --json',
|
|
111
111
|
'canon drive http://localhost:4173 run.json --json',
|
package/src/hooks/post-merge.ts
CHANGED
package/src/indexes/walk.ts
CHANGED
package/src/labels/audit.ts
CHANGED
package/src/markdown/files.ts
CHANGED
package/src/migrate/apply.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { $ } from 'bun'
|
|
2
2
|
import { readFile, mkdir, rename, rmdir, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { dirname, join, relative } from 'node:path'
|
|
4
|
-
import { gitEnv } from '@/git
|
|
4
|
+
import { gitEnv } from '@/git/env'
|
|
5
5
|
import type { RenamePlan, RenameSource } from '@/migrate/plan'
|
|
6
6
|
import { FROM_ROOT as OLD_ROOT, type RecordsPlan } from '@/migrate/records'
|
|
7
7
|
|
package/src/pr/bijection.ts
CHANGED
package/src/pr/evidence.ts
CHANGED
|
@@ -10,11 +10,20 @@ 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'
|
|
@@ -135,15 +144,15 @@ export function evidenceMarker(head: string): string {
|
|
|
135
144
|
* pinned to a commit sha rather than a branch, so the comment keeps showing
|
|
136
145
|
* what it claimed even after the branch moves.
|
|
137
146
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
147
|
+
* The body opens with `## Evidence` and, under it, one address line joining
|
|
148
|
+
* whichever of the hosted and local previews apply, so the comment reads as
|
|
149
|
+
* evidence first and a preview never stands where the heading should. With no
|
|
150
|
+
* states the heading and the address are the whole body apart from the
|
|
151
|
+
* marker, so a pull request whose screenshots did not change still gets one
|
|
152
|
+
* comment a later call can find and edit in place.
|
|
141
153
|
*
|
|
142
154
|
* 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.
|
|
155
|
+
* comment carries the addresses, the screenshots, and what to look at.
|
|
147
156
|
*/
|
|
148
157
|
export function renderEvidenceBody(
|
|
149
158
|
states: readonly EvidenceState[],
|
|
@@ -154,11 +163,15 @@ export function renderEvidenceBody(
|
|
|
154
163
|
checklist?: string,
|
|
155
164
|
local?: string,
|
|
156
165
|
): string {
|
|
157
|
-
const
|
|
166
|
+
const segments = [
|
|
158
167
|
...(preview === undefined ? [] : [`${PREVIEW_PREFIX} ${preview}`]),
|
|
159
168
|
...(local === undefined ? [] : [`${LOCAL_PREFIX} ${local}`]),
|
|
160
169
|
]
|
|
161
|
-
const opening =
|
|
170
|
+
const opening = [
|
|
171
|
+
EVIDENCE_HEADING,
|
|
172
|
+
'',
|
|
173
|
+
...(segments.length === 0 ? [] : [segments.join(ADDRESS_SEPARATOR), '']),
|
|
174
|
+
]
|
|
162
175
|
const closing =
|
|
163
176
|
checklist === undefined
|
|
164
177
|
? []
|
|
@@ -198,9 +211,7 @@ export function renderEvidenceBody(
|
|
|
198
211
|
|
|
199
212
|
return [
|
|
200
213
|
...opening,
|
|
201
|
-
|
|
202
|
-
'',
|
|
203
|
-
`**Base:** \`${base.slice(0, SHORT_SHA_LENGTH)}\` · **Head:** \`${head.slice(0, SHORT_SHA_LENGTH)}\``,
|
|
214
|
+
`${BASE_PREFIX} \`${base.slice(0, SHORT_SHA_LENGTH)}\` · **Head:** \`${head.slice(0, SHORT_SHA_LENGTH)}\``,
|
|
204
215
|
'',
|
|
205
216
|
...sections,
|
|
206
217
|
'',
|
|
@@ -249,7 +260,8 @@ export interface OwedInput {
|
|
|
249
260
|
readonly hasEvidenceChange: boolean
|
|
250
261
|
readonly hasMarkedComment: boolean
|
|
251
262
|
readonly carriedPreview: boolean
|
|
252
|
-
|
|
263
|
+
/** A deploy workflow resolves and its push filter matches a path the pull request changed. */
|
|
264
|
+
readonly deployServesChange: boolean
|
|
253
265
|
}
|
|
254
266
|
|
|
255
267
|
/**
|
|
@@ -262,7 +274,7 @@ export function readOwed(input: OwedInput): OwedStep[] {
|
|
|
262
274
|
if (input.hasEvidenceChange && !input.hasMarkedComment) owed.push('evidence')
|
|
263
275
|
if (
|
|
264
276
|
(input.hasMarkedComment || input.hasEvidenceChange) &&
|
|
265
|
-
input.
|
|
277
|
+
input.deployServesChange &&
|
|
266
278
|
!input.carriedPreview
|
|
267
279
|
) {
|
|
268
280
|
owed.push('preview')
|
|
@@ -295,33 +307,78 @@ export function findEvidenceCaseCount(
|
|
|
295
307
|
return marked === undefined ? 0 : countEvidenceCases(marked.body)
|
|
296
308
|
}
|
|
297
309
|
|
|
310
|
+
/** Lines that close the address region, being whatever a body carries below its addresses. */
|
|
311
|
+
const REGION_ENDS = [
|
|
312
|
+
BASE_PREFIX,
|
|
313
|
+
CHECKLIST_HEADING,
|
|
314
|
+
'<details',
|
|
315
|
+
CHECKLIST_START,
|
|
316
|
+
MARKER_PREFIX,
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
export interface AddressReading {
|
|
320
|
+
/** Indexes of the lines carrying a hosted or local segment. */
|
|
321
|
+
readonly lines: readonly number[]
|
|
322
|
+
readonly preview?: string
|
|
323
|
+
readonly local?: string
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function readSegment(segment: string, prefix: string): string | undefined {
|
|
327
|
+
return segment.startsWith(prefix)
|
|
328
|
+
? segment.slice(prefix.length).trim() || undefined
|
|
329
|
+
: undefined
|
|
330
|
+
}
|
|
331
|
+
|
|
298
332
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
333
|
+
* Reads the addresses a body carries in the region above its commit line,
|
|
334
|
+
* checklist, comparison, or marker, matching each prefix wherever it sits in
|
|
335
|
+
* that region. One reader covers the layout that opens on the addresses as
|
|
336
|
+
* well as the one that opens on the heading, so an open pull request keeps
|
|
337
|
+
* its links across the first re-render, and a line quoted further down in a
|
|
338
|
+
* checklist is never taken for an address.
|
|
302
339
|
*/
|
|
303
|
-
export function
|
|
340
|
+
export function readAddresses(body: string): AddressReading {
|
|
341
|
+
const lines: number[] = []
|
|
342
|
+
let preview: string | undefined
|
|
343
|
+
let local: string | undefined
|
|
344
|
+
const bodyLines = body.split('\n')
|
|
345
|
+
for (let index = 0; index < bodyLines.length; index += 1) {
|
|
346
|
+
const line = (bodyLines[index] ?? '').trim()
|
|
347
|
+
if (REGION_ENDS.some((end) => line.startsWith(end))) break
|
|
348
|
+
let isAddress = false
|
|
349
|
+
for (const segment of line.split(ADDRESS_SEPARATOR.trim())) {
|
|
350
|
+
const trimmed = segment.trim()
|
|
351
|
+
const hosted = readSegment(trimmed, PREVIEW_PREFIX)
|
|
352
|
+
const own = readSegment(trimmed, LOCAL_PREFIX)
|
|
353
|
+
if (hosted !== undefined) preview ??= hosted
|
|
354
|
+
if (own !== undefined) local ??= own
|
|
355
|
+
if (hosted !== undefined || own !== undefined) isAddress = true
|
|
356
|
+
}
|
|
357
|
+
if (isAddress) lines.push(index)
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
lines,
|
|
361
|
+
...(preview !== undefined && { preview }),
|
|
362
|
+
...(local !== undefined && { local }),
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function findMarked(
|
|
304
367
|
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
|
|
368
|
+
): EvidenceComment | undefined {
|
|
369
|
+
return comments.find((comment) => hasEvidenceMarker(comment.body))
|
|
310
370
|
}
|
|
311
371
|
|
|
312
372
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
373
|
+
* The preview address the marked comment already carries. A re-render after
|
|
374
|
+
* a later push passes no address of its own, and carrying this one forward
|
|
375
|
+
* keeps that edit from deleting the link a reviewer is using.
|
|
316
376
|
*/
|
|
317
|
-
export function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (line.startsWith(LOCAL_PREFIX)) return index
|
|
323
|
-
}
|
|
324
|
-
return -1
|
|
377
|
+
export function findEvidencePreview(
|
|
378
|
+
comments: readonly EvidenceComment[],
|
|
379
|
+
): string | undefined {
|
|
380
|
+
const marked = findMarked(comments)
|
|
381
|
+
return marked === undefined ? undefined : readAddresses(marked.body).preview
|
|
325
382
|
}
|
|
326
383
|
|
|
327
384
|
/**
|
|
@@ -331,12 +388,8 @@ export function findLocalLineIndex(body: string): number {
|
|
|
331
388
|
export function findEvidenceLocal(
|
|
332
389
|
comments: readonly EvidenceComment[],
|
|
333
390
|
): 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
|
|
391
|
+
const marked = findMarked(comments)
|
|
392
|
+
return marked === undefined ? undefined : readAddresses(marked.body).local
|
|
340
393
|
}
|
|
341
394
|
|
|
342
395
|
/**
|
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/records/backup.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
4
|
+
import { gitEnv } from '@/git/env'
|
|
5
5
|
import { RECORD_ROOTS, recordRoot, SCRATCH } from '@/record-root'
|
|
6
6
|
import { type BlockedPath, guardPayload } from '@/records/push-guard'
|
|
7
7
|
|
|
@@ -2,7 +2,7 @@ import { $ } from 'bun'
|
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { relative } from 'node:path'
|
|
5
|
-
import { gitEnv } from '@/git
|
|
5
|
+
import { gitEnv } from '@/git/env'
|
|
6
6
|
import { readReviewed } from '@/records/stale'
|
|
7
7
|
import { surfaceDir } from '@/surface-root'
|
|
8
8
|
|