@erclx/canon 5.77.0 → 5.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/app-drive/REQUIREMENT.md +12 -0
- package/claude/skills/app-drive/SKILL.md +44 -8
- package/claude/skills/app-drive/references/config.md +18 -8
- package/claude/skills/app-drive/references/drive-record.md +4 -0
- package/claude/skills/auto-ship/REQUIREMENT.md +1 -1
- package/claude/skills/auto-ship/SKILL.md +5 -4
- package/claude/skills/auto-ship/references/draft-mark.md +33 -15
- package/claude/skills/auto-ship/references/failure-recovery.md +4 -2
- package/claude/skills/auto-ship/references/ui-checklist.md +9 -1
- package/claude/skills/code-craft/REQUIREMENT.md +1 -1
- package/claude/skills/code-craft/SKILL.md +7 -0
- package/claude/skills/code-craft/references/adopted.md +8 -0
- package/claude/skills/git-followup/SKILL.md +1 -0
- package/claude/skills/git-pr/REQUIREMENT.md +2 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/final-command.md +34 -2
- package/claude/skills/git-pr/references/pr.md +1 -1
- package/claude/skills/git-ship/REQUIREMENT.md +2 -2
- package/claude/skills/git-ship/SKILL.md +16 -11
- package/claude/skills/git-ship/references/ci-watch.md +28 -4
- package/claude/skills/markdown-craft/references/markdown.md +4 -4
- package/claude/skills/plan-feature/references/plan.md +2 -0
- package/claude/skills/pr-evidence/SKILL.md +6 -1
- package/claude/skills/pr-evidence/references/evidence.md +1 -0
- package/claude/skills/pr-evidence/references/reel.md +9 -1
- package/claude/skills/review-address/SKILL.md +2 -1
- package/claude/skills/review-craft/references/ui.md +13 -1
- package/claude/skills/review-pr/references/body-variants.md +2 -0
- package/claude/skills/review-pr/references/close-out.md +2 -0
- package/claude/skills/review-pr/references/key-changes.md +2 -1
- package/claude/skills/review-pr/references/post.md +1 -0
- package/claude/skills/review-ui/REQUIREMENT.md +1 -0
- package/claude/skills/review-ui/SKILL.md +2 -2
- package/claude/skills/review-ui/references/driving.md +32 -4
- package/claude/skills/review-ui/references/post.md +6 -2
- package/claude/skills/role-orchestrator/REQUIREMENT.md +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-hand-launch.md +2 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-limit-stops.md +47 -0
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +24 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +3 -2
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +35 -4
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +3 -1
- package/claude/skills/role-worker/SKILL.md +2 -1
- package/claude/skills/task-board/SKILL.md +1 -0
- package/claude/skills/task-board/references/tasks.md +1 -0
- package/claude/skills/test-craft/SKILL.md +1 -0
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
- package/claude/skills/ui-checklist/SKILL.md +2 -1
- package/claude/skills/ui-craft/references/capture.md +16 -0
- package/claude/skills/ui-film/REQUIREMENT.md +2 -2
- package/claude/skills/ui-film/SKILL.md +6 -1
- package/claude/skills/ui-film/references/compose-craft.md +68 -11
- package/claude/skills/ui-film/references/config.md +2 -2
- package/claude/skills/ui-film/references/handoff.md +1 -1
- package/claude/skills/ui-film/references/harness.md +71 -0
- package/claude/skills/ui-film/references/take-craft.md +42 -17
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/operating-model.md +6 -5
- package/docs/workflow/parallel-features.md +2 -1
- package/docs/workflow/visual-design-workflow.md +2 -1
- package/governance/ledger/skills/code-craft/SKILL.md +9 -0
- package/governance/ledger/skills/test-craft/SKILL.md +1 -0
- package/governance/ledger/skills/ui-craft/capture.md +18 -5
- package/package.json +4 -3
- package/src/commands/pr.ts +255 -46
- package/src/commands/sessions.ts +88 -6
- package/src/commands/take.ts +12 -1
- package/src/commands/tasks.ts +41 -2
- package/src/design/components.ts +5 -107
- package/src/design/favicon.ts +21 -17
- package/src/design/render.ts +9 -27
- package/src/design/template.ts +42 -0
- package/src/design/templates/favicon.svg +17 -0
- package/src/design/templates/hand-drawn-figure.css +54 -0
- package/src/design/templates/preview.html +27 -0
- package/src/design/templates/scrollbar.css +29 -0
- package/src/design/templates/status.css +24 -0
- package/src/gate/embedded.ts +400 -0
- package/src/gate/stages.ts +8 -0
- package/src/git/files.ts +5 -2
- package/src/indexes/frontmatter.ts +27 -6
- package/src/indexes/render.ts +21 -3
- package/src/markdown/colons.ts +14 -4
- package/src/markdown/heading-case.ts +9 -2
- package/src/pr/bijection.ts +8 -0
- package/src/pr/evidence.ts +81 -13
- package/src/pr/frames.ts +1 -6
- package/src/pr/health.ts +63 -15
- package/src/pr/lift.ts +77 -9
- package/src/pr/links.ts +160 -0
- package/src/pr/local.ts +17 -4
- package/src/pr/paths.ts +20 -2
- package/src/pr/rest.ts +2 -0
- package/src/pr/review-scope.ts +28 -0
- package/src/pr/review-state.ts +5 -5
- package/src/pr/waiver.ts +45 -0
- package/src/sessions/stop.ts +145 -0
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/take/camera.ts +337 -208
- package/src/take/encode.ts +16 -42
- package/src/take/film.ts +7 -1
- package/src/take/frame-clock.ts +46 -0
- package/src/take/log.ts +43 -2
- package/src/take/perform.ts +85 -13
- package/src/take/plan.ts +26 -5
- package/src/take/reel.ts +30 -5
- package/src/take/screencast.ts +223 -13
- package/src/take/stepper.ts +24 -4
- package/src/tasks/holders.ts +198 -0
- package/src/tasks/validate.ts +148 -0
- package/src/commands/fixtures/gh.sh +0 -64
- package/src/take/fixtures/busy.html +0 -107
- package/src/take/fixtures/fade.html +0 -49
- package/src/take/fixtures/fetch.html +0 -38
- package/src/take/fixtures/input.html +0 -138
- package/src/take/zoom/NOTICE.md +0 -45
- package/src/take/zoom/math-utils.ts +0 -69
- package/src/take/zoom/region-utils.ts +0 -55
- package/src/take/zoom/spring.ts +0 -133
- /package/src/sessions/{transfer/transcript.ts → transcript.ts} +0 -0
package/src/tasks/validate.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs'
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises'
|
|
3
3
|
import { join, resolve } from 'node:path'
|
|
4
|
+
import { readFrontmatterSource } from '@/indexes/frontmatter'
|
|
4
5
|
import { codeSpans, unbrokenRun } from '@/markdown/tables'
|
|
5
6
|
import {
|
|
6
7
|
archiveDir,
|
|
@@ -21,6 +22,8 @@ import {
|
|
|
21
22
|
renderedLength,
|
|
22
23
|
WAITING_LIMIT,
|
|
23
24
|
} from '@/tasks/board'
|
|
25
|
+
import { branchOfPlan } from '@/tasks/branch'
|
|
26
|
+
import type { HolderReader, LiveReaders, MergedReader } from '@/tasks/holders'
|
|
24
27
|
import { gitTrunkReader, type TrunkReader } from '@/tasks/trunk'
|
|
25
28
|
|
|
26
29
|
const ORDERING_FILE = 'priority.md'
|
|
@@ -61,6 +64,7 @@ export const FINDING_KINDS = [
|
|
|
61
64
|
'row-untabled',
|
|
62
65
|
'row-misordered',
|
|
63
66
|
'row-unranked',
|
|
67
|
+
'row-held',
|
|
64
68
|
'touches-unstated',
|
|
65
69
|
'touches-stale',
|
|
66
70
|
'columns-mismatched',
|
|
@@ -69,6 +73,7 @@ export const FINDING_KINDS = [
|
|
|
69
73
|
'blocker-unresolved',
|
|
70
74
|
'blocker-declined',
|
|
71
75
|
'sessions-misshapen',
|
|
76
|
+
'frontmatter-invalid',
|
|
72
77
|
] as const
|
|
73
78
|
|
|
74
79
|
export type FindingKind = (typeof FINDING_KINDS)[number]
|
|
@@ -177,6 +182,18 @@ export interface BoardRow {
|
|
|
177
182
|
readonly cells: readonly string[]
|
|
178
183
|
}
|
|
179
184
|
|
|
185
|
+
/**
|
|
186
|
+
* What each source behind the held-row check answered. `unread` is a source the
|
|
187
|
+
* run asked and could not read, so a board carrying no `row-held` finding beside
|
|
188
|
+
* it is untested rather than clean. `not-asked` is a run without the live read.
|
|
189
|
+
*/
|
|
190
|
+
export type LiveState = 'read' | 'unread' | 'not-asked'
|
|
191
|
+
|
|
192
|
+
export interface LiveReading {
|
|
193
|
+
readonly sessions: LiveState
|
|
194
|
+
readonly pulls: LiveState
|
|
195
|
+
}
|
|
196
|
+
|
|
180
197
|
export interface ValidateReport {
|
|
181
198
|
readonly ok: true
|
|
182
199
|
readonly rows: number
|
|
@@ -189,6 +206,7 @@ export interface ValidateReport {
|
|
|
189
206
|
readonly shared: readonly SharedFiles[]
|
|
190
207
|
readonly wide: readonly WideToken[]
|
|
191
208
|
readonly unplaced: readonly Unplaced[]
|
|
209
|
+
readonly live: LiveReading
|
|
192
210
|
}
|
|
193
211
|
|
|
194
212
|
export interface ValidateRefused {
|
|
@@ -877,6 +895,34 @@ async function checkSessions(
|
|
|
877
895
|
return found.filter((finding): finding is Finding => finding !== undefined)
|
|
878
896
|
}
|
|
879
897
|
|
|
898
|
+
/**
|
|
899
|
+
* Reports a task file whose frontmatter block will not parse, which the index
|
|
900
|
+
* regeneration would otherwise surface only after the row was placed. A file
|
|
901
|
+
* with no block is left alone, since the index walker owns that report.
|
|
902
|
+
*/
|
|
903
|
+
async function checkFrontmatter(
|
|
904
|
+
stems: readonly string[],
|
|
905
|
+
dir: string,
|
|
906
|
+
): Promise<Finding[]> {
|
|
907
|
+
const found = await Promise.all(
|
|
908
|
+
stems.map(async (stem): Promise<Finding | undefined> => {
|
|
909
|
+
const reading = readFrontmatterSource(
|
|
910
|
+
await readFile(join(dir, `${stem}.md`), 'utf8'),
|
|
911
|
+
)
|
|
912
|
+
if (reading.kind !== 'invalid') return undefined
|
|
913
|
+
|
|
914
|
+
return {
|
|
915
|
+
kind: 'frontmatter-invalid',
|
|
916
|
+
group: undefined,
|
|
917
|
+
subject: stem,
|
|
918
|
+
message: `frontmatter is not valid YAML: ${reading.message}. Double any apostrophe inside the single-quoted title.`,
|
|
919
|
+
}
|
|
920
|
+
}),
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
return found.filter((finding): finding is Finding => finding !== undefined)
|
|
924
|
+
}
|
|
925
|
+
|
|
880
926
|
/**
|
|
881
927
|
* Tests the claim a group name makes about a plan. `## Needs a plan` says none
|
|
882
928
|
* is live and `## Up next` says one is written, and neither group has a `Plan`
|
|
@@ -1611,6 +1657,95 @@ export async function groupRuleFindings(
|
|
|
1611
1657
|
]
|
|
1612
1658
|
}
|
|
1613
1659
|
|
|
1660
|
+
/**
|
|
1661
|
+
* Reports a row outside `## Run now` whose task a live session or an open pull
|
|
1662
|
+
* request holds, tied through the branch each live plan on its `Plan:` line
|
|
1663
|
+
* derives. A row whose worker renamed its branch has nothing to match, and an
|
|
1664
|
+
* archived plan's branch is finished work, so neither is reported.
|
|
1665
|
+
*
|
|
1666
|
+
* A session outlives its merge, so a session still on a branch a merged pull
|
|
1667
|
+
* request names is finished work too, until the merge reaches this clone and the
|
|
1668
|
+
* hook archives the row. A merged read that fails leaves the hold standing, since
|
|
1669
|
+
* the session is still there, and reads `pulls` as unread, since that hold may be
|
|
1670
|
+
* finished work the open list could not rule out.
|
|
1671
|
+
*/
|
|
1672
|
+
async function checkHeld(
|
|
1673
|
+
rows: readonly BoardRow[],
|
|
1674
|
+
live: LiveRead,
|
|
1675
|
+
dir: string,
|
|
1676
|
+
root: string,
|
|
1677
|
+
): Promise<{ readonly findings: Finding[]; readonly mergedUnread: boolean }> {
|
|
1678
|
+
let mergedUnread = false
|
|
1679
|
+
|
|
1680
|
+
const holdersOf = async (branch: string): Promise<string[]> => {
|
|
1681
|
+
const session = live.sessions?.get(branch)
|
|
1682
|
+
const merged = session ? await live.merged?.(branch) : false
|
|
1683
|
+
if (merged === undefined) mergedUnread = true
|
|
1684
|
+
|
|
1685
|
+
return [session && !merged ? session : undefined, live.pulls?.get(branch)]
|
|
1686
|
+
.filter((name): name is string => name !== undefined)
|
|
1687
|
+
.map((name) => `${name} on ${branch}`)
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const found = await Promise.all(
|
|
1691
|
+
rows.map(async (row): Promise<Finding | undefined> => {
|
|
1692
|
+
if (row.group === 'Run now' || !row.stem) return undefined
|
|
1693
|
+
const file = join(dir, `${row.stem}.md`)
|
|
1694
|
+
if (!existsSync(file)) return undefined
|
|
1695
|
+
|
|
1696
|
+
const targets = readPlanTargets(await readFile(file, 'utf8'))
|
|
1697
|
+
const branches = targets.flatMap((target) => {
|
|
1698
|
+
const path = target.split('#')[0] || target
|
|
1699
|
+
return resolveLivePlan(path, dir, root) === undefined
|
|
1700
|
+
? []
|
|
1701
|
+
: [branchOfPlan(path)]
|
|
1702
|
+
})
|
|
1703
|
+
const held = (await Promise.all(branches.map(holdersOf))).flat()
|
|
1704
|
+
if (held.length === 0) return undefined
|
|
1705
|
+
|
|
1706
|
+
return {
|
|
1707
|
+
kind: 'row-held',
|
|
1708
|
+
group: row.group,
|
|
1709
|
+
subject: row.stem,
|
|
1710
|
+
message: `sits under ${row.group} and is held by ${held.join(' and ')}. A row with a worker belongs under Run now.`,
|
|
1711
|
+
}
|
|
1712
|
+
}),
|
|
1713
|
+
)
|
|
1714
|
+
|
|
1715
|
+
return {
|
|
1716
|
+
findings: found.filter(
|
|
1717
|
+
(finding): finding is Finding => finding !== undefined,
|
|
1718
|
+
),
|
|
1719
|
+
mergedUnread,
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** What each source answered, `undefined` where it was not asked or failed. */
|
|
1724
|
+
interface LiveRead {
|
|
1725
|
+
readonly reading: LiveReading
|
|
1726
|
+
readonly sessions?: Awaited<ReturnType<HolderReader>>
|
|
1727
|
+
readonly pulls?: Awaited<ReturnType<HolderReader>>
|
|
1728
|
+
readonly merged?: MergedReader
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/** Takes each source once, since a failed read answers undefined rather than throws. */
|
|
1732
|
+
async function readLive(live: LiveReaders | undefined): Promise<LiveRead> {
|
|
1733
|
+
if (!live) {
|
|
1734
|
+
return { reading: { sessions: 'not-asked', pulls: 'not-asked' } }
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
const stateOf = (read: Awaited<ReturnType<HolderReader>>): LiveState =>
|
|
1738
|
+
read ? 'read' : 'unread'
|
|
1739
|
+
const [sessions, pulls] = await Promise.all([live.sessions(), live.pulls()])
|
|
1740
|
+
|
|
1741
|
+
return {
|
|
1742
|
+
reading: { sessions: stateOf(sessions), pulls: stateOf(pulls) },
|
|
1743
|
+
sessions,
|
|
1744
|
+
pulls,
|
|
1745
|
+
merged: live.merged,
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1614
1749
|
function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
|
|
1615
1750
|
return { ok: false, reason, message }
|
|
1616
1751
|
}
|
|
@@ -1618,6 +1753,12 @@ function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
|
|
|
1618
1753
|
export interface ValidateOptions {
|
|
1619
1754
|
/** Overridden by tests, which supply the trunk rather than reaching for git. */
|
|
1620
1755
|
readonly trunk?: TrunkReader
|
|
1756
|
+
/**
|
|
1757
|
+
* The roster and GitHub reads behind the held-row check. Absent by default,
|
|
1758
|
+
* since the board's write hook runs this on every edit and a network read
|
|
1759
|
+
* there costs every write.
|
|
1760
|
+
*/
|
|
1761
|
+
readonly live?: LiveReaders
|
|
1621
1762
|
}
|
|
1622
1763
|
|
|
1623
1764
|
/**
|
|
@@ -1668,6 +1809,8 @@ export async function validateBoard(
|
|
|
1668
1809
|
? await listTaskStems(declinedPath)
|
|
1669
1810
|
: []
|
|
1670
1811
|
const parked = await checkParked(rows, root, trunk)
|
|
1812
|
+
const live = await readLive(options.live)
|
|
1813
|
+
const held = await checkHeld(rows, live, dir, root)
|
|
1671
1814
|
|
|
1672
1815
|
const findings = [
|
|
1673
1816
|
...shapeFindings,
|
|
@@ -1675,10 +1818,12 @@ export async function validateBoard(
|
|
|
1675
1818
|
...(await groupRuleFindings(rows, root)),
|
|
1676
1819
|
...(await checkPlanAgreement(rows, dir, root)),
|
|
1677
1820
|
...(await checkSessions(rows, dir)),
|
|
1821
|
+
...(await checkFrontmatter(stems, dir)),
|
|
1678
1822
|
...(await checkStaleTouches(rows, root)),
|
|
1679
1823
|
...checkCellWidth(rows),
|
|
1680
1824
|
...checkOrdering(rows),
|
|
1681
1825
|
...parked.findings,
|
|
1826
|
+
...held.findings,
|
|
1682
1827
|
]
|
|
1683
1828
|
|
|
1684
1829
|
return {
|
|
@@ -1693,5 +1838,8 @@ export async function validateBoard(
|
|
|
1693
1838
|
shared: checkSharedFiles(rows),
|
|
1694
1839
|
wide: checkWideTokens(rows),
|
|
1695
1840
|
unplaced: checkUnplaced(rows, backlog, stems),
|
|
1841
|
+
live: held.mergedUnread
|
|
1842
|
+
? { ...live.reading, pulls: 'unread' }
|
|
1843
|
+
: live.reading,
|
|
1696
1844
|
}
|
|
1697
1845
|
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Fake gh for src/commands/pr.test.ts. Reads FAKE_GH_FX, FAKE_GH_COMMENTS, and FAKE_GH_API_LOG.
|
|
3
|
-
set -o pipefail
|
|
4
|
-
fx="$FAKE_GH_FX"
|
|
5
|
-
f=.
|
|
6
|
-
p=
|
|
7
|
-
for a in "$@"; do
|
|
8
|
-
[ "$p" = --jq ] && f=$a
|
|
9
|
-
p=$a
|
|
10
|
-
done
|
|
11
|
-
case "$*" in
|
|
12
|
-
*graphql* | "pr "* | "repo view"*) exit 1 ;;
|
|
13
|
-
api*pulls/7/files*)
|
|
14
|
-
echo "$*" >>"$fx/files-calls.log"
|
|
15
|
-
[ -f "$fx/files-fail" ] && exit 1
|
|
16
|
-
{
|
|
17
|
-
if [ -f "$fx/files.tsv" ]; then
|
|
18
|
-
cat "$fx/files.tsv"
|
|
19
|
-
else
|
|
20
|
-
git diff --name-status --no-renames main...HEAD | sed -e 's/^A/added/' -e 's/^M/modified/' -e 's/^D/removed/'
|
|
21
|
-
fi
|
|
22
|
-
} | jq -Rn '[inputs | select(length > 0) | split("\t") | {status: .[0], filename: .[1]}]' | jq -r "$f"
|
|
23
|
-
;;
|
|
24
|
-
api*git/trees/*)
|
|
25
|
-
[ -f "$fx/base-tree.json" ] || exit 1
|
|
26
|
-
jq -r "$f" "$fx/base-tree.json"
|
|
27
|
-
;;
|
|
28
|
-
api*contents/*)
|
|
29
|
-
a="$*"
|
|
30
|
-
p=${a##*contents/}
|
|
31
|
-
p=${p%%\?*}
|
|
32
|
-
r=${a##*ref=}
|
|
33
|
-
[ -f "$fx/img/$r-$(basename "$p")" ] && cat "$fx/img/$r-$(basename "$p")" || exit 1
|
|
34
|
-
;;
|
|
35
|
-
api*compare/*)
|
|
36
|
-
if [ -f "$fx/merge-base" ]; then cat "$fx/merge-base"; else git merge-base main HEAD; fi
|
|
37
|
-
;;
|
|
38
|
-
"api -X PUT repos/{owner}/{repo}/pulls/7/reviews/"*)
|
|
39
|
-
id=${4##*/}
|
|
40
|
-
echo "$id" >>"$fx/review-puts.log"
|
|
41
|
-
[ -f "$fx/review-put-fail-$id" ] && exit 1
|
|
42
|
-
printf '%s\n' "${@: -1}" >>"$fx/review-bodies.log"
|
|
43
|
-
echo "{}"
|
|
44
|
-
;;
|
|
45
|
-
api*pulls/7/reviews*) [ -f "$fx/ui-reviews.json" ] && jq -r "$f" "$fx/ui-reviews.json" ;;
|
|
46
|
-
api*issues/7/comments*) jq '[.comments[] | {html_url: .url, body}]' "$FAKE_GH_COMMENTS" | jq -r "$f" ;;
|
|
47
|
-
"api -X PATCH repos/{owner}/{repo}/pulls/7 "*)
|
|
48
|
-
[ -f "$fx/body-fail" ] && exit 1
|
|
49
|
-
printf '%s' "${@: -1}" >"$fx/body-write.log"
|
|
50
|
-
echo "{}"
|
|
51
|
-
;;
|
|
52
|
-
"api repos/{owner}/{repo}/pulls/7")
|
|
53
|
-
if [ -f "$fx/head" ]; then h=$(cat "$fx/head"); else h=$(git rev-parse HEAD); fi
|
|
54
|
-
b=
|
|
55
|
-
[ -f "$fx/pr-body" ] && b=$(cat "$fx/pr-body")
|
|
56
|
-
jq -n --arg h "$h" --arg b "$b" '{number:7,head:{ref:"feat/x",sha:$h},base:{ref:"main"},mergeable_state:"clean",body:$b}'
|
|
57
|
-
;;
|
|
58
|
-
"api repos/{owner}/{repo}") echo "{\"full_name\":\"o/r\"}" ;;
|
|
59
|
-
api*)
|
|
60
|
-
printf '%s' "${@: -1}" >"$FAKE_GH_API_LOG"
|
|
61
|
-
echo "{}"
|
|
62
|
-
;;
|
|
63
|
-
*) exit 1 ;;
|
|
64
|
-
esac
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Busy</title>
|
|
6
|
-
<!--
|
|
7
|
-
A hero canvas that redraws every frame and spends busy=<ms> of main
|
|
8
|
-
thread work on each, so a frame takes about that long to render. A card
|
|
9
|
-
sits about 2900px down, below the window, and the rail link scrolls to it.
|
|
10
|
-
With timed=1 each frame spins on performance.now() instead, as the
|
|
11
|
-
groundwork page did, which never ends once the stepper pauses virtual
|
|
12
|
-
time.
|
|
13
|
-
-->
|
|
14
|
-
<style>
|
|
15
|
-
html {
|
|
16
|
-
scroll-behavior: smooth;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
body {
|
|
20
|
-
margin: 0;
|
|
21
|
-
font-family: sans-serif;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
nav {
|
|
25
|
-
position: fixed;
|
|
26
|
-
top: 10px;
|
|
27
|
-
right: 10px;
|
|
28
|
-
z-index: 2;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
nav a {
|
|
32
|
-
display: block;
|
|
33
|
-
padding: 12px 20px;
|
|
34
|
-
background: #fff;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
canvas {
|
|
38
|
-
display: block;
|
|
39
|
-
width: 100vw;
|
|
40
|
-
height: 100vh;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
section {
|
|
44
|
-
height: 900px;
|
|
45
|
-
border-top: 1px solid #ccc;
|
|
46
|
-
padding: 40px;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.card {
|
|
50
|
-
width: 400px;
|
|
51
|
-
height: 240px;
|
|
52
|
-
background: #ddd;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.card:hover {
|
|
56
|
-
background: #f60;
|
|
57
|
-
}
|
|
58
|
-
</style>
|
|
59
|
-
</head>
|
|
60
|
-
<body>
|
|
61
|
-
<nav><a id="rail" href="#cards">Cards</a></nav>
|
|
62
|
-
<canvas id="hero"></canvas>
|
|
63
|
-
<section>one</section>
|
|
64
|
-
<section>two</section>
|
|
65
|
-
<section id="cards"><div class="card" id="card">card</div></section>
|
|
66
|
-
<section>four</section>
|
|
67
|
-
<script>
|
|
68
|
-
const params = new URLSearchParams(location.search)
|
|
69
|
-
const busy = Number(params.get('busy') || 0)
|
|
70
|
-
const timed = params.get('timed') === '1'
|
|
71
|
-
const hero = document.getElementById('hero')
|
|
72
|
-
hero.width = innerWidth
|
|
73
|
-
hero.height = innerHeight
|
|
74
|
-
const context = hero.getContext('2d')
|
|
75
|
-
// The work is a count calibrated in real time on load, since a loop
|
|
76
|
-
// timed by performance.now() never ends under a paused virtual clock.
|
|
77
|
-
let sink = 0
|
|
78
|
-
const spin = (count) => {
|
|
79
|
-
for (let i = 0; i < count; i += 1) sink += Math.sqrt(i)
|
|
80
|
-
}
|
|
81
|
-
const measure = (ms) => {
|
|
82
|
-
let count = 0
|
|
83
|
-
const start = performance.now()
|
|
84
|
-
while (performance.now() - start < ms) {
|
|
85
|
-
spin(10000)
|
|
86
|
-
count += 10000
|
|
87
|
-
}
|
|
88
|
-
return count / ms
|
|
89
|
-
}
|
|
90
|
-
// Warmed first, so the count matches the optimized loop a frame runs.
|
|
91
|
-
measure(100)
|
|
92
|
-
const work = Math.round(busy * measure(200))
|
|
93
|
-
const draw = (time) => {
|
|
94
|
-
if (timed) {
|
|
95
|
-
const start = performance.now()
|
|
96
|
-
while (performance.now() - start < busy) {}
|
|
97
|
-
} else {
|
|
98
|
-
spin(work)
|
|
99
|
-
}
|
|
100
|
-
context.fillStyle = `hsl(${(time / 10) % 360}, 60%, 50%)`
|
|
101
|
-
context.fillRect(0, 0, hero.width, hero.height)
|
|
102
|
-
requestAnimationFrame(draw)
|
|
103
|
-
}
|
|
104
|
-
requestAnimationFrame(draw)
|
|
105
|
-
</script>
|
|
106
|
-
</body>
|
|
107
|
-
</html>
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Fade</title>
|
|
6
|
-
<!--
|
|
7
|
-
A band that fades from black to white over exactly one second of page
|
|
8
|
-
time once the button is pressed, so its brightness frame by frame reads
|
|
9
|
-
how much page time each frame of a take advanced.
|
|
10
|
-
-->
|
|
11
|
-
<style>
|
|
12
|
-
body {
|
|
13
|
-
margin: 0;
|
|
14
|
-
background: #808080;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
#go {
|
|
18
|
-
position: absolute;
|
|
19
|
-
left: 100px;
|
|
20
|
-
top: 100px;
|
|
21
|
-
width: 200px;
|
|
22
|
-
height: 60px;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
#band {
|
|
26
|
-
position: absolute;
|
|
27
|
-
left: 0;
|
|
28
|
-
top: 300px;
|
|
29
|
-
width: 1920px;
|
|
30
|
-
height: 780px;
|
|
31
|
-
background-color: #000;
|
|
32
|
-
transition: background-color 1000ms linear;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
#band.lit {
|
|
36
|
-
background-color: #fff;
|
|
37
|
-
}
|
|
38
|
-
</style>
|
|
39
|
-
</head>
|
|
40
|
-
<body>
|
|
41
|
-
<button id="go">Go</button>
|
|
42
|
-
<div id="band"></div>
|
|
43
|
-
<script>
|
|
44
|
-
document.getElementById('go').addEventListener('click', () => {
|
|
45
|
-
document.getElementById('band').classList.add('lit')
|
|
46
|
-
})
|
|
47
|
-
</script>
|
|
48
|
-
</body>
|
|
49
|
-
</html>
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Fetch</title>
|
|
6
|
-
<!--
|
|
7
|
-
A button whose press starts a fetch the server answers slowly, the way
|
|
8
|
-
a page switch in an app loads what the next view shows. A take must keep
|
|
9
|
-
stepping frames while the fetch is in flight.
|
|
10
|
-
-->
|
|
11
|
-
<style>
|
|
12
|
-
body {
|
|
13
|
-
margin: 0;
|
|
14
|
-
background: #808080;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
#go {
|
|
18
|
-
position: absolute;
|
|
19
|
-
left: 100px;
|
|
20
|
-
top: 100px;
|
|
21
|
-
width: 200px;
|
|
22
|
-
height: 60px;
|
|
23
|
-
}
|
|
24
|
-
</style>
|
|
25
|
-
</head>
|
|
26
|
-
<body>
|
|
27
|
-
<button id="go">Load</button>
|
|
28
|
-
<p id="status">idle</p>
|
|
29
|
-
<script>
|
|
30
|
-
document.getElementById('go').addEventListener('click', () => {
|
|
31
|
-
document.getElementById('status').textContent = 'loading'
|
|
32
|
-
fetch('/slow').then(() => {
|
|
33
|
-
document.getElementById('status').textContent = 'loaded'
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
</script>
|
|
37
|
-
</body>
|
|
38
|
-
</html>
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Input</title>
|
|
6
|
-
<!--
|
|
7
|
-
One target per input a take performs, each posting what the page saw to
|
|
8
|
-
/report with a sequence number, so a test reads the inputs in the order
|
|
9
|
-
the page received them after the take has closed the browser.
|
|
10
|
-
|
|
11
|
-
- #handle follows a pointer-event drag and reports where it dropped and
|
|
12
|
-
how many of its moves carried the held button
|
|
13
|
-
- #frame reports each dblclick
|
|
14
|
-
- #layer reports pointerdown, pointerup, and click with the Shift flag
|
|
15
|
-
- #field takes focus for key steps, and every keyup reports its value
|
|
16
|
-
-->
|
|
17
|
-
<style>
|
|
18
|
-
body {
|
|
19
|
-
margin: 0;
|
|
20
|
-
background: #f4f4f4;
|
|
21
|
-
font: 24px sans-serif;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
#handle {
|
|
25
|
-
position: absolute;
|
|
26
|
-
left: 200px;
|
|
27
|
-
top: 200px;
|
|
28
|
-
width: 80px;
|
|
29
|
-
height: 80px;
|
|
30
|
-
/* Neutral, so the reel's blue ring reads over it at any alpha. */
|
|
31
|
-
background: #444;
|
|
32
|
-
touch-action: none;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
#lane {
|
|
36
|
-
position: absolute;
|
|
37
|
-
left: 700px;
|
|
38
|
-
top: 160px;
|
|
39
|
-
width: 300px;
|
|
40
|
-
height: 160px;
|
|
41
|
-
border: 2px dashed #555;
|
|
42
|
-
box-sizing: border-box;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
#frame,
|
|
46
|
-
#layer {
|
|
47
|
-
position: absolute;
|
|
48
|
-
top: 500px;
|
|
49
|
-
width: 200px;
|
|
50
|
-
height: 100px;
|
|
51
|
-
background: #ddd;
|
|
52
|
-
user-select: none;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
#frame {
|
|
56
|
-
left: 200px;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
#layer {
|
|
60
|
-
left: 700px;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
#field {
|
|
64
|
-
position: absolute;
|
|
65
|
-
left: 200px;
|
|
66
|
-
top: 750px;
|
|
67
|
-
width: 400px;
|
|
68
|
-
height: 48px;
|
|
69
|
-
font: inherit;
|
|
70
|
-
}
|
|
71
|
-
</style>
|
|
72
|
-
</head>
|
|
73
|
-
<body>
|
|
74
|
-
<div id="lane"></div>
|
|
75
|
-
<div id="handle"></div>
|
|
76
|
-
<div id="frame">Frame</div>
|
|
77
|
-
<div id="layer">Layer</div>
|
|
78
|
-
<input id="field" />
|
|
79
|
-
<script>
|
|
80
|
-
let seq = 0
|
|
81
|
-
const report = (entry) =>
|
|
82
|
-
fetch('/report', {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
body: JSON.stringify({ seq: (seq += 1), ...entry }),
|
|
85
|
-
keepalive: true,
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const handle = document.getElementById('handle')
|
|
89
|
-
const lane = document.getElementById('lane')
|
|
90
|
-
let drag = null
|
|
91
|
-
handle.addEventListener('pointerdown', (event) => {
|
|
92
|
-
handle.setPointerCapture(event.pointerId)
|
|
93
|
-
drag = { held: 0, hover: 0 }
|
|
94
|
-
})
|
|
95
|
-
handle.addEventListener('pointermove', (event) => {
|
|
96
|
-
if (!drag) return
|
|
97
|
-
if (event.buttons === 1) drag.held += 1
|
|
98
|
-
else drag.hover += 1
|
|
99
|
-
handle.style.left = `${event.clientX - 40}px`
|
|
100
|
-
handle.style.top = `${event.clientY - 40}px`
|
|
101
|
-
})
|
|
102
|
-
handle.addEventListener('pointerup', (event) => {
|
|
103
|
-
if (!drag) return
|
|
104
|
-
const box = lane.getBoundingClientRect()
|
|
105
|
-
const onTarget =
|
|
106
|
-
event.clientX >= box.left &&
|
|
107
|
-
event.clientX <= box.right &&
|
|
108
|
-
event.clientY >= box.top &&
|
|
109
|
-
event.clientY <= box.bottom
|
|
110
|
-
report({ type: 'drop', ...drag, onTarget, altKey: event.altKey })
|
|
111
|
-
drag = null
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
document
|
|
115
|
-
.getElementById('frame')
|
|
116
|
-
.addEventListener('dblclick', () => report({ type: 'dblclick' }))
|
|
117
|
-
|
|
118
|
-
const layer = document.getElementById('layer')
|
|
119
|
-
for (const type of ['pointerdown', 'pointerup', 'click']) {
|
|
120
|
-
layer.addEventListener(type, (event) =>
|
|
121
|
-
report({ type, shiftKey: event.shiftKey }),
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const field = document.getElementById('field')
|
|
126
|
-
document.addEventListener('keydown', (event) =>
|
|
127
|
-
report({
|
|
128
|
-
type: 'keydown',
|
|
129
|
-
key: event.key,
|
|
130
|
-
ctrlKey: event.ctrlKey,
|
|
131
|
-
}),
|
|
132
|
-
)
|
|
133
|
-
document.addEventListener('keyup', (event) =>
|
|
134
|
-
report({ type: 'keyup', key: event.key, value: field.value }),
|
|
135
|
-
)
|
|
136
|
-
</script>
|
|
137
|
-
</body>
|
|
138
|
-
</html>
|
package/src/take/zoom/NOTICE.md
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# Vendored zoom notice
|
|
2
|
-
|
|
3
|
-
The files in this folder carry the zoom timing of [OpenScreen](https://github.com/siddharthvaddem/openscreen) at commit `ca8104c279da0d4b4481ec992ff103949e22c772`, under the MIT license below.
|
|
4
|
-
|
|
5
|
-
## What came from where
|
|
6
|
-
|
|
7
|
-
| File | Source in OpenScreen, under `src/components/video-editor/videoPlayback/` |
|
|
8
|
-
| ----------------- | -------------------------------------------------------------------------------------------------------- |
|
|
9
|
-
| `math-utils.ts` | `mathUtils.ts` |
|
|
10
|
-
| `region-utils.ts` | `zoomRegionUtils.ts`, with `TRANSITION_WINDOW_MS` and `ZOOM_IN_TRANSITION_WINDOW_MS` from `constants.ts` |
|
|
11
|
-
| `spring.ts` | `motionSmoothing.ts` and `zoomSpring.ts` |
|
|
12
|
-
|
|
13
|
-
## What changed
|
|
14
|
-
|
|
15
|
-
- The file names are kebab-case, as this repository names every TypeScript file, and the code is reformatted to its style
|
|
16
|
-
- `math-utils.ts` keeps `clamp01`, `cubicBezier`, and `easeOutScreenStudio`, and drops the easings nothing here calls
|
|
17
|
-
- `region-utils.ts` keeps `computeRegionStrength`, the connected-pan easing, and the window and gap constants. It drops the dominant-region search, its cache, the 3D rotation, and the cursor-follow focus, since the camera here aims at clicks and holds its focus by its own rule.
|
|
18
|
-
- The zoom-out window is 700 ms rather than 1015.05 ms, and the connected pan runs 600 ms rather than 1000 ms, both on the operator's eye in groundwork track `193-ui-record`. <!-- canon-keep-retired --> The zoom-in window keeps OpenScreen's 1522.575 ms.
|
|
19
|
-
- `spring.ts` keeps the zoom spring's stiffness 320, damping 40, mass 0.92, its rest thresholds, the delta clamp, and the overshoot clamp from `stepAxis`. OpenScreen steps that spring through the `motion` package, and this solves it in closed form instead, so no dependency comes with it.
|
|
20
|
-
|
|
21
|
-
## License
|
|
22
|
-
|
|
23
|
-
```plaintext
|
|
24
|
-
MIT License
|
|
25
|
-
|
|
26
|
-
Copyright (c) 2025 Siddharth Vaddem
|
|
27
|
-
|
|
28
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
29
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
30
|
-
in the Software without restriction, including without limitation the rights
|
|
31
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
32
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
33
|
-
furnished to do so, subject to the following conditions:
|
|
34
|
-
|
|
35
|
-
The above copyright notice and this permission notice shall be included in all
|
|
36
|
-
copies or substantial portions of the Software.
|
|
37
|
-
|
|
38
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
39
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
40
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
41
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
42
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
43
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
44
|
-
SOFTWARE.
|
|
45
|
-
```
|