@erclx/canon 5.72.1 → 5.74.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 +40 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/references/adopted.md +5 -3
- package/claude/skills/app-drive/REQUIREMENT.md +2 -2
- package/claude/skills/app-drive/SKILL.md +22 -5
- package/claude/skills/app-drive/references/config.md +2 -1
- package/claude/skills/app-drive/references/drive-record.md +13 -0
- package/claude/skills/app-drive/references/isolate.md +1 -1
- package/claude/skills/build-in-slices/references/adopted.md +4 -2
- package/claude/skills/code-craft/references/adopted.md +9 -7
- package/claude/skills/codebase-layout/references/adopted.md +8 -6
- package/claude/skills/create-skill/references/skill-practice.md +5 -0
- package/claude/skills/deprecation-migration/references/adopted.md +5 -3
- package/claude/skills/design-pick/references/board.md +2 -2
- package/claude/skills/plan-feature/SKILL.md +3 -2
- package/claude/skills/plan-feature/references/plan.md +12 -2
- package/claude/skills/plan-groundwork/SKILL.md +2 -0
- package/claude/skills/plan-intake/references/intake.md +3 -2
- package/claude/skills/pr-evidence/REQUIREMENT.md +1 -0
- package/claude/skills/pr-evidence/SKILL.md +2 -0
- package/claude/skills/pr-evidence/references/evidence.md +11 -4
- package/claude/skills/{ui-film/references/pr-clip.md → pr-evidence/references/reel.md} +21 -21
- package/claude/skills/proposal-critique/REQUIREMENT.md +34 -0
- package/claude/skills/proposal-critique/SKILL.md +126 -0
- package/claude/skills/review-craft/references/adopted.md +4 -4
- package/claude/skills/role-orchestrator/SKILL.md +3 -2
- package/claude/skills/role-orchestrator/references/orchestrator-filing.md +8 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +4 -1
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +26 -2
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +22 -3
- package/claude/skills/role-planner/SKILL.md +3 -1
- package/claude/skills/role-reviewer/SKILL.md +15 -4
- package/claude/skills/search-craft/references/adopted.md +9 -7
- package/claude/skills/systematic-debugging/REQUIREMENT.md +1 -0
- package/claude/skills/systematic-debugging/SKILL.md +21 -8
- package/claude/skills/systematic-debugging/references/adopted.md +15 -4
- package/claude/skills/teach-craft/references/adopted.md +8 -6
- package/claude/skills/test-craft/references/adopted.md +14 -12
- package/claude/skills/test-first/SKILL.md +1 -1
- package/claude/skills/test-first/references/adopted.md +9 -5
- package/claude/skills/ui-film/REQUIREMENT.md +5 -6
- package/claude/skills/ui-film/SKILL.md +21 -13
- package/claude/skills/ui-film/references/compose-craft.md +3 -2
- package/claude/skills/ui-film/references/handoff.md +12 -8
- package/claude/skills/ui-film/references/take-craft.md +10 -0
- package/claude/skills/write-human/references/adopted.md +8 -3
- package/docs/agents/records.md +1 -1
- package/docs/target/index.md +1 -0
- package/docs/target/outside-tools.md +39 -0
- package/docs/workflow/operating-model.md +8 -8
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map.md +2 -1
- package/governance/ledger/canon/plan.md +20 -0
- package/governance/ledger/skills/build-in-slices/SKILL.md +55 -0
- package/governance/ledger/skills/proposal-critique/SKILL.md +66 -0
- package/governance/ledger/skills/search-craft/SKILL.md +54 -0
- package/governance/ledger/skills/systematic-debugging/SKILL.md +64 -0
- package/governance/ledger/skills/test-first/SKILL.md +28 -0
- package/governance/ledger/skills/write-human/SKILL.md +62 -0
- package/governance/ledger/skills/write-human/density.md +36 -0
- package/governance/ledger/skills/write-human/machine-tells.md +30 -0
- package/governance/rules/canon/plan.md +1 -0
- package/package.json +1 -1
- package/scripts/ci/docs-only.sh +24 -0
- package/src/claude/cases/misc.ts +14 -0
- package/src/claude/skills-audit.ts +89 -8
- package/src/commands/intake.ts +4 -1
- package/src/commands/pr.ts +110 -13
- package/src/gate/measures.ts +1 -1
- package/src/gate/sequencer.ts +7 -1
- package/src/gate/stages.ts +36 -24
- package/src/intake/drive-check.ts +75 -4
- package/src/markdown/scope.ts +2 -0
- package/src/pr/checks.ts +75 -3
- package/src/pr/evidence.ts +62 -1
- package/src/pr/lift.ts +23 -2
- package/src/pr/merge.ts +13 -3
- package/src/records/backup.ts +1 -0
- package/src/targets/pulls.ts +42 -0
package/src/pr/checks.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ChecksState,
|
|
3
|
+
type RawCheck,
|
|
4
|
+
rollupRequired,
|
|
5
|
+
} from '@/targets/pulls'
|
|
2
6
|
|
|
3
7
|
/** One row of `repos/{owner}/{repo}/commits/<sha>/check-runs`. */
|
|
4
8
|
export interface RawCheckRun {
|
|
@@ -9,6 +13,12 @@ export interface RawCheckRun {
|
|
|
9
13
|
readonly id?: number
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
/** One row of `repos/{owner}/{repo}/commits/<sha>/status`, newest per context already. */
|
|
17
|
+
export interface RawStatus {
|
|
18
|
+
readonly context?: string
|
|
19
|
+
readonly state?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
/** What that endpoint returns, count and rows apart. */
|
|
13
23
|
export interface CheckRunListing {
|
|
14
24
|
readonly total_count?: number
|
|
@@ -27,6 +37,41 @@ export interface ChecksReading {
|
|
|
27
37
|
readonly reported: number
|
|
28
38
|
/** Runs `matched` carried for a check name this reading already kept a newer run for. */
|
|
29
39
|
readonly collapsed: number
|
|
40
|
+
/**
|
|
41
|
+
* Failing checks outside the required set, present only when a required set
|
|
42
|
+
* was read, so a reading over every check keeps its old shape.
|
|
43
|
+
*/
|
|
44
|
+
readonly advisory?: readonly string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reads the check names a base branch's rules require, from
|
|
49
|
+
* `repos/{owner}/{repo}/rules/branches/<base>`.
|
|
50
|
+
*
|
|
51
|
+
* Undefined when the answer is not a rule list, which a repository without
|
|
52
|
+
* rulesets or a refused read returns, so the caller falls back to every check.
|
|
53
|
+
* Classic branch protection is not read, and falls back the same way.
|
|
54
|
+
*/
|
|
55
|
+
export function requiredChecks(
|
|
56
|
+
rules: unknown,
|
|
57
|
+
): ReadonlySet<string> | undefined {
|
|
58
|
+
if (!Array.isArray(rules)) return undefined
|
|
59
|
+
|
|
60
|
+
const names = new Set<string>()
|
|
61
|
+
for (const rule of rules as readonly RequiredRule[]) {
|
|
62
|
+
if (rule?.type !== 'required_status_checks') continue
|
|
63
|
+
for (const check of rule.parameters?.required_status_checks ?? []) {
|
|
64
|
+
if (typeof check.context === 'string') names.add(check.context)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return names
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface RequiredRule {
|
|
71
|
+
readonly type?: string
|
|
72
|
+
readonly parameters?: {
|
|
73
|
+
readonly required_status_checks?: readonly { readonly context?: unknown }[]
|
|
74
|
+
}
|
|
30
75
|
}
|
|
31
76
|
|
|
32
77
|
export interface ConflictedReading extends ChecksReading {
|
|
@@ -68,6 +113,7 @@ export function withMergeState(
|
|
|
68
113
|
*/
|
|
69
114
|
function adapt(run: RawCheckRun): RawCheck {
|
|
70
115
|
return {
|
|
116
|
+
...(run.name !== undefined && { name: run.name }),
|
|
71
117
|
...(run.status !== undefined && { status: run.status.toUpperCase() }),
|
|
72
118
|
...(run.conclusion !== undefined &&
|
|
73
119
|
run.conclusion !== null && {
|
|
@@ -125,10 +171,22 @@ function newestPerCheck(runs: readonly RawCheckRun[]): {
|
|
|
125
171
|
* rolls up from `newestPerCheck`'s kept rows rather than from `matched`
|
|
126
172
|
* directly, so a stale failing run left behind by a rerun no longer outvotes
|
|
127
173
|
* the passing run beside it.
|
|
174
|
+
*
|
|
175
|
+
* A required set narrows only that last rollup. The foreign and partial
|
|
176
|
+
* listing guards still read every run, since either says the set in hand is
|
|
177
|
+
* incomplete whichever checks gate.
|
|
178
|
+
*
|
|
179
|
+
* A ruleset can require a commit status, which the check-run listing never
|
|
180
|
+
* carries, so the statuses join the rollup by `context` once a non-empty
|
|
181
|
+
* required set was read. An unread or empty set keeps them out, since the
|
|
182
|
+
* every-check fallback is the reading these verbs gave before rules were read
|
|
183
|
+
* and it saw no status.
|
|
128
184
|
*/
|
|
129
185
|
export function collapseChecks(
|
|
130
186
|
tip: string,
|
|
131
187
|
listing: CheckRunListing,
|
|
188
|
+
required?: ReadonlySet<string>,
|
|
189
|
+
statuses: readonly RawStatus[] = [],
|
|
132
190
|
): ChecksReading {
|
|
133
191
|
const runs = listing.check_runs ?? []
|
|
134
192
|
const matched = runs.filter((run) => run.head_sha === tip)
|
|
@@ -139,6 +197,7 @@ export function collapseChecks(
|
|
|
139
197
|
foreign: runs.length - matched.length,
|
|
140
198
|
reported: listing.total_count ?? runs.length,
|
|
141
199
|
collapsed,
|
|
200
|
+
...(required !== undefined && { advisory: [] }),
|
|
142
201
|
}
|
|
143
202
|
|
|
144
203
|
if (reading.foreign > 0) return { ...reading, state: 'pending' }
|
|
@@ -149,7 +208,20 @@ export function collapseChecks(
|
|
|
149
208
|
// sitting outside of, which is the false green keyed on a sha alone.
|
|
150
209
|
if (reading.reported > runs.length) return { ...reading, state: 'pending' }
|
|
151
210
|
|
|
152
|
-
|
|
211
|
+
const statusRows: RawCheck[] =
|
|
212
|
+
required === undefined || required.size === 0
|
|
213
|
+
? []
|
|
214
|
+
: statuses.map((status) => ({
|
|
215
|
+
...(status.context !== undefined && { name: status.context }),
|
|
216
|
+
state: (status.state ?? '').toUpperCase(),
|
|
217
|
+
}))
|
|
218
|
+
const rows = [...kept.map(adapt), ...statusRows]
|
|
219
|
+
if (rows.length === 0) return { ...reading, state: 'pending' }
|
|
153
220
|
|
|
154
|
-
|
|
221
|
+
const { state, advisory } = rollupRequired(rows, required)
|
|
222
|
+
return {
|
|
223
|
+
...reading,
|
|
224
|
+
state: state ?? 'pending',
|
|
225
|
+
...(required !== undefined && { advisory }),
|
|
226
|
+
}
|
|
155
227
|
}
|
package/src/pr/evidence.ts
CHANGED
|
@@ -25,6 +25,12 @@ const REEL_PREFIX = '**Reel** filmed at'
|
|
|
25
25
|
const REEL_LINE =
|
|
26
26
|
/^\*\*Reel\*\* filmed at `([0-9a-f]{7,40})`:<br><img src="([^"]+)"/
|
|
27
27
|
|
|
28
|
+
/** How the line naming why no local address was posted opens. */
|
|
29
|
+
const LOCAL_SKIP_PREFIX = '**No local preview:**'
|
|
30
|
+
|
|
31
|
+
/** How the line naming why no reel was filmed opens. */
|
|
32
|
+
const REEL_SKIP_PREFIX = '**No reel:**'
|
|
33
|
+
|
|
28
34
|
/** The width a reel is filmed at, which it asks for in the comment. */
|
|
29
35
|
const REEL_WIDTH = 960
|
|
30
36
|
|
|
@@ -198,6 +204,15 @@ function escapeCell(text: string): string {
|
|
|
198
204
|
.replaceAll('|', '|')
|
|
199
205
|
}
|
|
200
206
|
|
|
207
|
+
function unescapeCell(text: string): string {
|
|
208
|
+
return text
|
|
209
|
+
.replaceAll('|', '|')
|
|
210
|
+
.replaceAll('"', '"')
|
|
211
|
+
.replaceAll('>', '>')
|
|
212
|
+
.replaceAll('<', '<')
|
|
213
|
+
.replaceAll('&', '&')
|
|
214
|
+
}
|
|
215
|
+
|
|
201
216
|
function evidenceImage(url: string, alt: string, width?: number): string {
|
|
202
217
|
const shown = Math.min(width ?? IMAGE_WIDTH, IMAGE_WIDTH)
|
|
203
218
|
return `<img src="${url}" width="${shown}" alt="${alt}">`
|
|
@@ -449,6 +464,18 @@ function reelLine(reel: EvidenceReel): string {
|
|
|
449
464
|
return `${REEL_PREFIX} \`${head}\`:<br><img src="${escapeCell(reel.link)}" width="${REEL_WIDTH}" alt="reel filmed at ${head}">`
|
|
450
465
|
}
|
|
451
466
|
|
|
467
|
+
/** Why a render carries no local address or no reel, each a reason code and any one-line message the caller joined to it. */
|
|
468
|
+
export interface EvidenceSkips {
|
|
469
|
+
readonly local?: string
|
|
470
|
+
readonly reel?: string
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** A skip line, or nothing for an empty reason. A reason is folded onto one line, since a line break would end the address region early. */
|
|
474
|
+
function skipLines(prefix: string, reason: string | undefined): string[] {
|
|
475
|
+
const text = reason?.replace(/\s+/g, ' ').trim() ?? ''
|
|
476
|
+
return text === '' ? [] : [`${prefix} ${escapeCell(text)}`, '']
|
|
477
|
+
}
|
|
478
|
+
|
|
452
479
|
export function evidenceMarker(head: string): string {
|
|
453
480
|
return `${MARKER_PREFIX} head=${head} -->`
|
|
454
481
|
}
|
|
@@ -480,6 +507,10 @@ export function evidenceMarker(head: string): string {
|
|
|
480
507
|
*
|
|
481
508
|
* A reel sits on its own line under the addresses, labeled with the head it
|
|
482
509
|
* was filmed at, so a reel carried past a later push never reads as current.
|
|
510
|
+
*
|
|
511
|
+
* A skip names why the local address or the reel is missing, on its own line
|
|
512
|
+
* where that address or reel would sit. Each is dropped whenever its address
|
|
513
|
+
* or reel renders, carried or passed, so a skip never outlives its cause.
|
|
483
514
|
*/
|
|
484
515
|
export function renderEvidenceBody(
|
|
485
516
|
states: readonly EvidenceState[],
|
|
@@ -490,6 +521,7 @@ export function renderEvidenceBody(
|
|
|
490
521
|
checklist?: string,
|
|
491
522
|
local?: string,
|
|
492
523
|
reel?: EvidenceReel,
|
|
524
|
+
skips: EvidenceSkips = {},
|
|
493
525
|
): string {
|
|
494
526
|
const segments = [
|
|
495
527
|
...(preview === undefined ? [] : [`${PREVIEW_PREFIX} ${preview}`]),
|
|
@@ -499,7 +531,10 @@ export function renderEvidenceBody(
|
|
|
499
531
|
EVIDENCE_HEADING,
|
|
500
532
|
'',
|
|
501
533
|
...(segments.length === 0 ? [] : [segments.join(ADDRESS_SEPARATOR), '']),
|
|
502
|
-
...(
|
|
534
|
+
...(local === undefined ? skipLines(LOCAL_SKIP_PREFIX, skips.local) : []),
|
|
535
|
+
...(reel === undefined
|
|
536
|
+
? skipLines(REEL_SKIP_PREFIX, skips.reel)
|
|
537
|
+
: [reelLine(reel), '']),
|
|
503
538
|
]
|
|
504
539
|
const closing =
|
|
505
540
|
checklist === undefined
|
|
@@ -756,6 +791,32 @@ export function findEvidenceReel(
|
|
|
756
791
|
return undefined
|
|
757
792
|
}
|
|
758
793
|
|
|
794
|
+
/**
|
|
795
|
+
* The skips the marked comment already carries, read from the address region
|
|
796
|
+
* only, so a line quoted in a checklist is never taken for one. A re-render
|
|
797
|
+
* passing no skip carries these forward the way it carries an address.
|
|
798
|
+
*/
|
|
799
|
+
export function findEvidenceSkips(
|
|
800
|
+
comments: readonly EvidenceComment[],
|
|
801
|
+
): EvidenceSkips {
|
|
802
|
+
const marked = findMarked(comments)
|
|
803
|
+
if (marked === undefined) return {}
|
|
804
|
+
let local: string | undefined
|
|
805
|
+
let reel: string | undefined
|
|
806
|
+
for (const raw of marked.body.split('\n')) {
|
|
807
|
+
const line = raw.trim()
|
|
808
|
+
if (REGION_ENDS.some((end) => line.startsWith(end))) break
|
|
809
|
+
const ownSkip = readSegment(line, LOCAL_SKIP_PREFIX)
|
|
810
|
+
const reelSkip = readSegment(line, REEL_SKIP_PREFIX)
|
|
811
|
+
if (ownSkip !== undefined) local ??= unescapeCell(ownSkip)
|
|
812
|
+
if (reelSkip !== undefined) reel ??= unescapeCell(reelSkip)
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
...(local !== undefined && { local }),
|
|
816
|
+
...(reel !== undefined && { reel }),
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
759
820
|
/**
|
|
760
821
|
* The reel a render embeds: a passed link, filmed at this head, replaces
|
|
761
822
|
* whatever the comment carries, and with none passed the posted reel carries
|
package/src/pr/lift.ts
CHANGED
|
@@ -83,8 +83,19 @@ export interface EvidenceGate {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export interface ChecksGate {
|
|
86
|
+
/** The state over the checks the base requires, or every check when none were read. */
|
|
86
87
|
readonly state: ChecksState
|
|
87
88
|
readonly isConflicted: boolean
|
|
89
|
+
/** Failing checks outside the required set, which are reported and never gate. */
|
|
90
|
+
readonly advisory?: readonly string[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The advisory field a record carries, absent when no advisory check failed. */
|
|
94
|
+
export function advisoryOf(reading: ChecksGate): {
|
|
95
|
+
advisory?: readonly string[]
|
|
96
|
+
} {
|
|
97
|
+
const advisory = reading.advisory ?? []
|
|
98
|
+
return advisory.length > 0 ? { advisory } : {}
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
/**
|
|
@@ -129,6 +140,8 @@ export interface LiftRecord {
|
|
|
129
140
|
readonly carriedFrom?: string
|
|
130
141
|
readonly paintedPaths?: readonly string[]
|
|
131
142
|
readonly drivenPaths?: readonly string[]
|
|
143
|
+
/** Failing checks the base does not require, named so a reader sees them before merging. */
|
|
144
|
+
readonly advisory?: readonly string[]
|
|
132
145
|
readonly isLifted: boolean
|
|
133
146
|
/** Whether the caller can treat the pull request as ready afterwards. */
|
|
134
147
|
readonly isOk: boolean
|
|
@@ -247,7 +260,11 @@ function standingGate(standing: Standing): GateResult {
|
|
|
247
260
|
}
|
|
248
261
|
|
|
249
262
|
type CiOutcome =
|
|
250
|
-
| {
|
|
263
|
+
| {
|
|
264
|
+
readonly kind: 'passing'
|
|
265
|
+
readonly waited: number
|
|
266
|
+
readonly reading: ChecksGate
|
|
267
|
+
}
|
|
251
268
|
| {
|
|
252
269
|
readonly kind: 'refused'
|
|
253
270
|
readonly reason: LiftRefusal
|
|
@@ -274,7 +291,9 @@ async function waitForChecks(
|
|
|
274
291
|
if (reading === undefined) {
|
|
275
292
|
return { kind: 'refused', reason: 'gh-failed', waited }
|
|
276
293
|
}
|
|
277
|
-
if (reading.state === 'passing')
|
|
294
|
+
if (reading.state === 'passing') {
|
|
295
|
+
return { kind: 'passing', waited, reading }
|
|
296
|
+
}
|
|
278
297
|
if (reading.state === 'failing') {
|
|
279
298
|
return { kind: 'refused', reason: 'ci-failing', waited }
|
|
280
299
|
}
|
|
@@ -422,6 +441,7 @@ export async function lift(
|
|
|
422
441
|
gates: all,
|
|
423
442
|
waitedSeconds: 0,
|
|
424
443
|
...carried(all),
|
|
444
|
+
...advisoryOf(reading),
|
|
425
445
|
}
|
|
426
446
|
}
|
|
427
447
|
|
|
@@ -485,6 +505,7 @@ export async function lift(
|
|
|
485
505
|
gates: cleared,
|
|
486
506
|
waitedSeconds: ci.waited,
|
|
487
507
|
...carried(cleared),
|
|
508
|
+
...advisoryOf(ci.reading),
|
|
488
509
|
}
|
|
489
510
|
}
|
|
490
511
|
|
package/src/pr/merge.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { advisoryOf, type ChecksGate } from '@/pr/lift'
|
|
2
2
|
import { readSection } from '@/pr/paths'
|
|
3
3
|
import { UI_PASS_MARKER } from '@/pr/tick'
|
|
4
4
|
|
|
@@ -67,6 +67,8 @@ export interface MergeRecord {
|
|
|
67
67
|
readonly description?: string
|
|
68
68
|
/** The squash commit, present only on a merge. */
|
|
69
69
|
readonly sha?: string
|
|
70
|
+
/** Failing checks the base does not require, which the merge went past. */
|
|
71
|
+
readonly advisory?: readonly string[]
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export type Composed =
|
|
@@ -230,8 +232,15 @@ export async function merge(
|
|
|
230
232
|
)
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
if (opts.isDryRun)
|
|
234
|
-
return {
|
|
235
|
+
if (opts.isDryRun) {
|
|
236
|
+
return {
|
|
237
|
+
reason: 'dry-run',
|
|
238
|
+
isOk: true,
|
|
239
|
+
number,
|
|
240
|
+
...message,
|
|
241
|
+
...advisoryOf(checks),
|
|
242
|
+
}
|
|
243
|
+
}
|
|
235
244
|
|
|
236
245
|
const outcome = await api.putMerge({ ...message, sha: pull.head })
|
|
237
246
|
if (outcome.kind === 'merged') {
|
|
@@ -241,6 +250,7 @@ export async function merge(
|
|
|
241
250
|
number,
|
|
242
251
|
...message,
|
|
243
252
|
sha: outcome.sha,
|
|
253
|
+
...advisoryOf(checks),
|
|
244
254
|
}
|
|
245
255
|
}
|
|
246
256
|
return refused(
|
package/src/records/backup.ts
CHANGED
|
@@ -107,6 +107,7 @@ export const EXCLUDED_ENTRIES = [
|
|
|
107
107
|
/** Media under a video workspace, remade from its plan and composition, so never backed up. */
|
|
108
108
|
export const MEDIA_PATHSPECS = [
|
|
109
109
|
':(exclude,glob)video/*/takes/**',
|
|
110
|
+
':(exclude,glob)video/*/scratch/**',
|
|
110
111
|
':(exclude,glob)video/**/renders/**',
|
|
111
112
|
':(exclude,glob)video/**/snapshots/**',
|
|
112
113
|
':(exclude,glob)video/**/node_modules/**',
|
package/src/targets/pulls.ts
CHANGED
|
@@ -94,6 +94,8 @@ interface RawPull {
|
|
|
94
94
|
* rather than writing a second collapse beside `rollup`.
|
|
95
95
|
*/
|
|
96
96
|
export interface RawCheck {
|
|
97
|
+
/** The check-run name, which a ruleset's required context matches. */
|
|
98
|
+
readonly name?: string
|
|
97
99
|
readonly status?: string
|
|
98
100
|
readonly conclusion?: string
|
|
99
101
|
readonly state?: string
|
|
@@ -137,6 +139,46 @@ export function rollup(checks: readonly RawCheck[]): ChecksState | null {
|
|
|
137
139
|
return running ? 'pending' : 'passing'
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
/** A head's state over the checks the base requires, with failing others named apart. */
|
|
143
|
+
export interface RequiredRollup {
|
|
144
|
+
readonly state: ChecksState | null
|
|
145
|
+
/** Names of checks outside the required set that failed, which never gate. */
|
|
146
|
+
readonly advisory: readonly string[]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Collapses a head's checks over the set the base branch's rules require.
|
|
151
|
+
*
|
|
152
|
+
* An unread or empty set gates by every check, which is `rollup` itself and
|
|
153
|
+
* the safe default for a repository whose rules list nothing. A required name
|
|
154
|
+
* with no run on the head reads pending rather than passing, since a run not
|
|
155
|
+
* yet created has not cleared anything.
|
|
156
|
+
*/
|
|
157
|
+
export function rollupRequired(
|
|
158
|
+
checks: readonly RawCheck[],
|
|
159
|
+
required: ReadonlySet<string> | undefined,
|
|
160
|
+
): RequiredRollup {
|
|
161
|
+
if (required === undefined || required.size === 0) {
|
|
162
|
+
return { state: rollup(checks), advisory: [] }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const isRequired = (check: RawCheck): boolean =>
|
|
166
|
+
check.name !== undefined && required.has(check.name)
|
|
167
|
+
const gating = checks.filter(isRequired)
|
|
168
|
+
const advisory = checks
|
|
169
|
+
.filter((check) => !isRequired(check) && rollup([check]) === 'failing')
|
|
170
|
+
.map((check) => check.name ?? '')
|
|
171
|
+
|
|
172
|
+
const state = rollup(gating)
|
|
173
|
+
const isMissing = [...required].some(
|
|
174
|
+
(name) => !gating.some((check) => check.name === name),
|
|
175
|
+
)
|
|
176
|
+
return {
|
|
177
|
+
state: state !== 'failing' && isMissing ? 'pending' : state,
|
|
178
|
+
advisory,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
140
182
|
/**
|
|
141
183
|
* Reads the heading of the newest pass carrying one, matching on the first line
|
|
142
184
|
* alone the way `poll.ts` does.
|