@erclx/canon 5.25.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 +4 -3
- 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 +14 -1
- 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/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +9 -21
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- 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/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +6 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/docs-sync/SKILL.md +2 -0
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +23 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +3 -1
- 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 +3 -1
- package/claude/skills/draft-ready/SKILL.md +2 -0
- package/claude/skills/draft-screencast/SKILL.md +11 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +33 -20
- 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-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/evidence.md +14 -6
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-ship/SKILL.md +2 -0
- package/claude/skills/git-split/SKILL.md +21 -8
- 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/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-capture/SKILL.md +2 -0
- package/claude/skills/memory-review/SKILL.md +2 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -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/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/SKILL.md +5 -14
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- 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/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +5 -1
- package/claude/skills/search-craft/references/adopted.md +30 -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/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -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 +4 -0
- package/claude/skills/test-first/references/adopted.md +36 -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 +3 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/claude/skills/youtube-transcripts/SKILL.md +2 -0
- package/docs/agents/canvas.md +24 -6
- package/docs/agents/capture.md +6 -6
- package/docs/agents/commands.md +5 -5
- package/docs/agents/index.md +5 -5
- 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/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +29 -5
- package/docs/agents/scripting.md +8 -1
- package/docs/agents/skills-audit.md +11 -3
- package/docs/agents/slides.md +15 -3
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/operating-model.md +5 -6
- package/docs/workflow/parallel-features.md +23 -2
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +3 -34
- 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-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +6 -162
- package/scripts/sandbox-hook.sh +99 -0
- package/src/audits/run.ts +1 -1
- package/src/canvas/address.ts +12 -3
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/align-grid.tsx +162 -0
- package/src/canvas/client/inspector/color-field.tsx +398 -0
- 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 +547 -17
- package/src/canvas/client/inspector/size-field.tsx +69 -0
- package/src/canvas/client/inspector/values.ts +166 -1
- package/src/canvas/client/inspector.tsx +236 -73
- 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/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +2 -2
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/seeds.ts +2 -2
- package/src/claude/skills-audit.ts +96 -14
- 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 +94 -11
- package/src/commands/design.ts +142 -2
- package/src/commands/feedback.ts +3 -3
- 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 +89 -7
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +103 -11
- package/src/commands/sessions.ts +1 -1
- package/src/commands/slides.ts +31 -77
- 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/audit.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/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +2 -2
- package/src/{client-commands.ts → gate/client-commands.ts} +1 -1
- package/src/gate/measures.ts +53 -13
- 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 +2 -2
- package/src/gov/install.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +3 -3
- 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/ceiling.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/record-root.ts +1 -0
- package/src/records/backup.ts +2 -1
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.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/hooks.ts +128 -0
- package/src/sandbox/provision.ts +607 -0
- package/src/sandbox/tree.ts +99 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +2 -2
- package/src/sessions/claim.ts +1 -1
- package/src/sessions/resolve.ts +1 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- 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 +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/seeds-report.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/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/trunk.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/subfolder.ts +1 -1
- package/src/web/readme-citations.ts +2 -2
- package/src/worktrees/bare-flag.ts +1 -1
- package/src/worktrees/reclaim.ts +2 -2
- package/src/worktrees/remove.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +3 -2
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +2 -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/scripts/manage-sandbox.sh +0 -472
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.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/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{copy.ts → sync/copy.ts} +0 -0
- /package/src/{target.ts → targets/validate.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
package/src/gov/superseded.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { isBinary } from '@/binary'
|
|
4
|
-
import { isMarked } from '@/exempt-marker'
|
|
5
|
-
import { listRepositoryFiles } from '@/git
|
|
3
|
+
import { isBinary } from '@/sweep/binary'
|
|
4
|
+
import { isMarked } from '@/sweep/exempt-marker'
|
|
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/ceiling.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { CHECKPOINTS, documentHeight } from '@/markdown/structure'
|
|
|
8
8
|
*
|
|
9
9
|
* The whole line rather than anywhere on it, so a standard quoting the marker
|
|
10
10
|
* inside a code span to document it does not exempt itself. A marker with no
|
|
11
|
-
* reason does not count, for the reason `isMarked` in `src/exempt-marker.ts`
|
|
11
|
+
* reason does not count, for the reason `isMarked` in `src/sweep/exempt-marker.ts`
|
|
12
12
|
* gives: a bare token reads as a line that meant to say something and did not.
|
|
13
13
|
*/
|
|
14
14
|
const EXEMPT_MARKER = /^<!--\s*canon-length-exempt:[ \t]*(\S.*?)\s*-->$/
|
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/record-root.ts
CHANGED
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
|
|
|
@@ -43,6 +43,7 @@ export const BACKED_FOLDERS = [
|
|
|
43
43
|
'plans',
|
|
44
44
|
'proposals',
|
|
45
45
|
'review',
|
|
46
|
+
'slides',
|
|
46
47
|
'tasks',
|
|
47
48
|
'teach',
|
|
48
49
|
'transcripts',
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { relative } from 'node:path'
|
|
5
|
+
import { gitEnv } from '@/git/env'
|
|
6
|
+
import { readReviewed } from '@/records/stale'
|
|
7
|
+
import { surfaceDir } from '@/surface-root'
|
|
8
|
+
|
|
9
|
+
/** The two records whose drift is identity rather than a stale path. */
|
|
10
|
+
const CANONICAL_DOCS = ['REQUIREMENTS.md', 'ARCHITECTURE.md'] as const
|
|
11
|
+
|
|
12
|
+
export interface CanonicalDoc {
|
|
13
|
+
/** Relative to the root, at whichever surface root carries the doc. */
|
|
14
|
+
readonly path: string
|
|
15
|
+
/** `YYYY-MM-DD`, or null when the doc carries no readable review date. */
|
|
16
|
+
readonly reviewed: string | null
|
|
17
|
+
/** The raw value of a `reviewed` field that is not a date, present only then. */
|
|
18
|
+
readonly invalidReviewed?: string
|
|
19
|
+
/** Release tags merged into HEAD after the reviewed day, or null when never reviewed. */
|
|
20
|
+
readonly releasesSince: number | null
|
|
21
|
+
/** The newest release tag merged into HEAD, or null when there is none. */
|
|
22
|
+
readonly latestRelease: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CanonicalReport {
|
|
26
|
+
readonly ok: true
|
|
27
|
+
readonly root: string
|
|
28
|
+
/** False when HEAD has merged no tag, so a zero count measured nothing. */
|
|
29
|
+
readonly tagged: boolean
|
|
30
|
+
readonly docs: readonly CanonicalDoc[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CanonicalRefused {
|
|
34
|
+
readonly ok: false
|
|
35
|
+
readonly reason: 'unreadable-tags'
|
|
36
|
+
readonly message: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type CanonicalOutcome = CanonicalReport | CanonicalRefused
|
|
40
|
+
|
|
41
|
+
interface Release {
|
|
42
|
+
readonly tag: string
|
|
43
|
+
readonly day: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A tag ending in a dotted version, bare or behind a component prefix such as
|
|
48
|
+
* `canon-v2.1.0`. An eval, deploy, or snapshot tag on the trunk names no
|
|
49
|
+
* release and would otherwise inflate the count.
|
|
50
|
+
*/
|
|
51
|
+
const RELEASE_TAG = /(?:^|[-/@_])v?\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?$/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A repository before its first commit has no HEAD for `--merged` to resolve,
|
|
55
|
+
* which git reports as a failed read. It has no tags either, so it reads as an
|
|
56
|
+
* empty history rather than an unreadable one.
|
|
57
|
+
*/
|
|
58
|
+
async function isUnborn(root: string): Promise<boolean> {
|
|
59
|
+
const [gitDir, head] = await Promise.all([
|
|
60
|
+
$`git -C ${root} rev-parse --git-dir`.env(gitEnv()).quiet().nothrow(),
|
|
61
|
+
$`git -C ${root} rev-parse --verify -q HEAD`
|
|
62
|
+
.env(gitEnv())
|
|
63
|
+
.quiet()
|
|
64
|
+
.nothrow(),
|
|
65
|
+
])
|
|
66
|
+
return gitDir.exitCode === 0 && head.exitCode !== 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every release tag HEAD has merged, newest first, with the day it was created.
|
|
71
|
+
*
|
|
72
|
+
* The creator date is the tagging date on an annotated tag and the commit date
|
|
73
|
+
* on a lightweight one, so a history mixing both counts by two clocks.
|
|
74
|
+
*/
|
|
75
|
+
async function mergedReleases(root: string): Promise<Release[] | undefined> {
|
|
76
|
+
// Interpolated rather than inline, since Bun's shell parses the bare
|
|
77
|
+
// parentheses in `%(refname)` as syntax of its own.
|
|
78
|
+
const args = [
|
|
79
|
+
'--merged',
|
|
80
|
+
'HEAD',
|
|
81
|
+
'--sort=-creatordate',
|
|
82
|
+
'--format=%(creatordate:short) %(refname:short)',
|
|
83
|
+
'refs/tags',
|
|
84
|
+
]
|
|
85
|
+
const result = await $`git -C ${root} for-each-ref ${args}`
|
|
86
|
+
.env(gitEnv())
|
|
87
|
+
.quiet()
|
|
88
|
+
.nothrow()
|
|
89
|
+
if (result.exitCode !== 0) return (await isUnborn(root)) ? [] : undefined
|
|
90
|
+
|
|
91
|
+
return result.stdout
|
|
92
|
+
.toString()
|
|
93
|
+
.split('\n')
|
|
94
|
+
.filter((line) => line.trim().length > 0)
|
|
95
|
+
.map((line) => {
|
|
96
|
+
const [day, tag] = line.trim().split(' ')
|
|
97
|
+
return { day, tag }
|
|
98
|
+
})
|
|
99
|
+
.filter((release) => RELEASE_TAG.test(release.tag))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reports when each canonical doc was last reviewed and how many releases
|
|
104
|
+
* have shipped since, read from an optional `reviewed:` frontmatter field.
|
|
105
|
+
*
|
|
106
|
+
* The review point is a date rather than a commit, since a commit written on a
|
|
107
|
+
* feature branch never reaches a trunk that squash-merges. A release tagged on
|
|
108
|
+
* the reviewed day itself is not counted, as the review is read as covering
|
|
109
|
+
* that day. It reports and never writes.
|
|
110
|
+
*/
|
|
111
|
+
export async function staleCanonical(root: string): Promise<CanonicalOutcome> {
|
|
112
|
+
const releases = await mergedReleases(root)
|
|
113
|
+
|
|
114
|
+
if (releases === undefined) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
reason: 'unreadable-tags',
|
|
118
|
+
message: `Could not read the release tags at ${root}, so no count can be taken.`,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const latestRelease = releases[0]?.tag ?? null
|
|
123
|
+
const present = CANONICAL_DOCS.map((entry) => surfaceDir(root, entry)).filter(
|
|
124
|
+
(path) => existsSync(path),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const docs = await Promise.all(
|
|
128
|
+
present.map(async (path): Promise<CanonicalDoc> => {
|
|
129
|
+
const text = (await readFile(path, 'utf8')).replaceAll('\r\n', '\n')
|
|
130
|
+
const review = readReviewed(text)
|
|
131
|
+
const { reviewed } = review
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
path: relative(root, path),
|
|
135
|
+
...review,
|
|
136
|
+
releasesSince:
|
|
137
|
+
reviewed === null
|
|
138
|
+
? null
|
|
139
|
+
: releases.filter((release) => release.day > reviewed).length,
|
|
140
|
+
latestRelease,
|
|
141
|
+
}
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return { ok: true, root, tagged: releases.length > 0, docs }
|
|
146
|
+
}
|
package/src/records/stale.ts
CHANGED
|
@@ -65,7 +65,7 @@ const RAW_FRONTMATTER = /^---\n([\s\S]*?)\n---/
|
|
|
65
65
|
* A value that is not a calendar date reads as never reviewed and is carried
|
|
66
66
|
* back so the review can see it, rather than throwing over one hand edit.
|
|
67
67
|
*/
|
|
68
|
-
function readReviewed(
|
|
68
|
+
export function readReviewed(
|
|
69
69
|
text: string,
|
|
70
70
|
): Pick<StaleEntry, 'reviewed' | 'invalidReviewed'> {
|
|
71
71
|
const block = RAW_FRONTMATTER.exec(text)?.[1]
|