rvlib-shipkit 2.1.1 → 2.3.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.
Files changed (142) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +2 -1
  3. package/src/agent-time/mask.ts +43 -0
  4. package/src/agent-time/render.ts +187 -0
  5. package/src/agent-time/report.ts +335 -0
  6. package/src/agent-time/shape.ts +271 -0
  7. package/src/agent-time/signals.ts +84 -0
  8. package/src/agent-time/store.ts +379 -0
  9. package/src/agent-time/timeline.ts +218 -0
  10. package/src/agent-time/turns.ts +135 -0
  11. package/src/assets/kits/identity-kit.ts +7 -3
  12. package/src/assets/run-assets.ts +18 -4
  13. package/src/checks/SK004-gitignore-policy.check.ts +11 -6
  14. package/src/checks/SK013-one-way-in-policy.check.ts +13 -8
  15. package/src/cli/cmd_Calls.ts +53 -0
  16. package/src/cli/cmd_Commands.ts +163 -43
  17. package/src/cli/cmd_Review.ts +948 -0
  18. package/src/cli/cmd_Shot.ts +63 -0
  19. package/src/cli/cmd_Start.ts +8 -4
  20. package/src/cli/cmd_Time.ts +112 -0
  21. package/src/cli/cmd_Trailer.ts +361 -0
  22. package/src/cli/kit/builtins.ts +40 -0
  23. package/src/cli/kit/calls.ts +177 -0
  24. package/src/cli/kit/families.ts +67 -0
  25. package/src/cli/kit/index.ts +8 -0
  26. package/src/cli/kit/pages-check.ts +36 -0
  27. package/src/cli/kit/project-cli.ts +73 -0
  28. package/src/cli/kit/verb.ts +167 -0
  29. package/src/cli.ts +15 -2
  30. package/src/collections/collection-types.ts +6 -1
  31. package/src/collections/providers.ts +3 -0
  32. package/src/collections/review.ts +70 -0
  33. package/src/core/layout.ts +12 -2
  34. package/src/core/migrate-layout.ts +31 -8
  35. package/src/core/one-way-in-policy.ts +12 -4
  36. package/src/reflect/prompt-corpus.ts +15 -114
  37. package/src/reflect/transcript.ts +254 -0
  38. package/src/review/anim.ts +228 -0
  39. package/src/review/auto.ts +141 -0
  40. package/src/review/format.ts +150 -0
  41. package/src/review/http.ts +23 -0
  42. package/src/review/marks.ts +43 -0
  43. package/src/review/media.ts +45 -0
  44. package/src/review/model.ts +854 -0
  45. package/src/review/project.ts +110 -0
  46. package/src/review/role.ts +48 -0
  47. package/src/review/routes.ts +228 -0
  48. package/src/review/store.ts +1002 -0
  49. package/src/review/threads.ts +237 -0
  50. package/src/review/trailer-desk.ts +385 -0
  51. package/src/review/trailer-model.ts +183 -0
  52. package/src/review/trailer-routes.ts +136 -0
  53. package/src/review/unit.ts +165 -0
  54. package/src/review/watchers.ts +54 -0
  55. package/src/scaffold/cli-skeleton.ts +149 -0
  56. package/src/scaffold/files/agent-index.md +2 -2
  57. package/src/scaffold/files/tsconfig.json +3 -0
  58. package/src/scaffold/files/verify-skill.md +1 -1
  59. package/src/scaffold/project-scaffold.ts +90 -12
  60. package/src/serve/embed.ts +35 -0
  61. package/src/serve/hub-app/activity-view.tsx +1 -1
  62. package/src/serve/hub-app/activity.css +0 -4
  63. package/src/serve/hub-app/dock-view.tsx +5 -4
  64. package/src/serve/hub-app/hash-route.ts +28 -1
  65. package/src/serve/hub-app/header-bar.tsx +3 -1
  66. package/src/serve/hub-app/home-st.ts +10 -2
  67. package/src/serve/hub-app/home-strip.tsx +3 -2
  68. package/src/serve/hub-app/home.html +1 -0
  69. package/src/serve/hub-app/hub-app.tsx +80 -11
  70. package/src/serve/hub-app/hub.css +45 -13
  71. package/src/serve/hub-app/icons.tsx +9 -0
  72. package/src/serve/hub-app/index.html +1 -0
  73. package/src/serve/hub-app/insights-view.tsx +4 -3
  74. package/src/serve/hub-app/keys.ts +14 -10
  75. package/src/serve/hub-app/layout-st.ts +63 -2
  76. package/src/serve/hub-app/planned-view.tsx +3 -2
  77. package/src/serve/hub-app/planned.css +0 -1
  78. package/src/serve/hub-app/project-list.css +22 -4
  79. package/src/serve/hub-app/project-list.tsx +41 -8
  80. package/src/serve/hub-app/projects-svc.ts +4 -0
  81. package/src/serve/hub-app/repo-sections.ts +15 -3
  82. package/src/serve/hub-app/repo-st.ts +45 -3
  83. package/src/serve/hub-app/review/clip-st.ts +208 -0
  84. package/src/serve/hub-app/review/kind-view.tsx +171 -0
  85. package/src/serve/hub-app/review/kit.tsx +113 -0
  86. package/src/serve/hub-app/review/review-answer.tsx +239 -0
  87. package/src/serve/hub-app/review/review-board.tsx +46 -0
  88. package/src/serve/hub-app/review/review-list.tsx +156 -0
  89. package/src/serve/hub-app/review/review-page.tsx +101 -0
  90. package/src/serve/hub-app/review/review-st.ts +779 -0
  91. package/src/serve/hub-app/review/review-views.tsx +503 -0
  92. package/src/serve/hub-app/review/trailer-page.tsx +416 -0
  93. package/src/serve/hub-app/review/trailer-st.ts +602 -0
  94. package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
  95. package/src/serve/hub-app/review.css +329 -0
  96. package/src/serve/hub-app/theme-st.ts +56 -0
  97. package/src/serve/hub-app/tree-build.ts +4 -0
  98. package/src/serve/hub-app/tree-row.tsx +1 -1
  99. package/src/serve/hub-app/tree-svc.ts +4 -0
  100. package/src/serve/hub-runtime.ts +5 -3
  101. package/src/serve/same-origin.ts +5 -1
  102. package/src/serve/serve-hub.ts +8 -0
  103. package/src/shipkit-files.ts +12 -4
  104. package/src/shot/client.ts +67 -0
  105. package/src/shot/clip-encoder.ts +146 -0
  106. package/src/shot/dom-clip.ts +124 -0
  107. package/src/shot/serve-shot.ts +54 -0
  108. package/src/shot/shooter.ts +343 -0
  109. package/src/shot/shot-ask.ts +189 -0
  110. package/src/shot/shot.shipkit.ts +14 -0
  111. package/src/smoke/run-smoke.ts +2 -2
  112. package/src/start-repo.ts +29 -14
  113. package/src/suite/judge-suites.ts +56 -10
  114. package/src/suite/select.ts +2 -5
  115. package/src/suite/suite-cli.ts +3 -2
  116. package/src/trailer/adapter.ts +78 -0
  117. package/src/trailer/build.ts +603 -0
  118. package/src/trailer/ffmpeg.ts +72 -0
  119. package/src/trailer/footage.ts +173 -0
  120. package/src/trailer/grammar.ts +1290 -0
  121. package/src/trailer/graph.ts +508 -0
  122. package/src/trailer/holes.ts +72 -0
  123. package/src/trailer/load-unit.ts +31 -0
  124. package/src/trailer/look.ts +124 -0
  125. package/src/trailer/passage.ts +129 -0
  126. package/src/trailer/plan.ts +192 -0
  127. package/src/trailer/prune.ts +193 -0
  128. package/src/trailer/score/compose.ts +240 -0
  129. package/src/trailer/score/dsp.ts +272 -0
  130. package/src/trailer/score/palette.ts +188 -0
  131. package/src/trailer/score/pattern.ts +91 -0
  132. package/src/trailer/score/score.ts +214 -0
  133. package/src/trailer/score/sound.ts +112 -0
  134. package/src/trailer/score/synth.ts +361 -0
  135. package/src/trailer/states.ts +76 -0
  136. package/src/trailer/stitch.ts +233 -0
  137. package/src/trailer/titles.ts +379 -0
  138. package/src/trailer/url.ts +108 -0
  139. package/src/trailer/vertical-draw.ts +286 -0
  140. package/src/trailer/vertical.ts +148 -0
  141. package/src/trailer/vocab.ts +59 -0
  142. package/src/trailer/words.ts +254 -0
@@ -335,6 +335,18 @@ export function aloneRunsNeeded(suspects: Timed[], parallelCpu: Record<string, n
335
335
  * every (fail) line under no header of its own: it closes the last file */
336
336
  export const BUN_RECAP = /^\s*\d+ tests? failed:\s*$/
337
337
 
338
+ /** the banner bun prints under a file header for an error thrown outside
339
+ * any test (a top level throw, a throw inside a describe callback), then a
340
+ * rule, the code frame, the error, the stack and a closing rule */
341
+ export const BETWEEN_TESTS = '# Unhandled error between tests'
342
+ export const BUN_RULE = /^-{20,}$/
343
+ /** the line that carries an error's message: bun's own `error:` and a named
344
+ * error class (`ReferenceError: x is not defined`) */
345
+ const ERROR_LINE = /^\s*(?:error|[A-Z]\w*(?:Error|Exception)): /
346
+ const BETWEEN_WORDS = 'an unhandled error between tests'
347
+ /** the line that opens a file in bun's report: its path and a colon */
348
+ const HEADER = /^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/
349
+
338
350
  /** the suites with a failing test, read off bun's report: a file that failed
339
351
  * prints its path as a header line, then its `(fail)` lines under it. A
340
352
  * passing file prints nothing at all in a piped run, which is why this reads
@@ -343,39 +355,73 @@ export function failedSuites(output: string): string[] {
343
355
  const out = new Set<string>()
344
356
  let cur: string | null = null
345
357
  for (const line of output.split('\n')) {
346
- const head = line.match(/^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/)
358
+ const head = line.match(HEADER)
347
359
  if (head != null) { cur = normSuitePath(head[1]!); continue }
348
360
  if (BUN_RECAP.test(line)) { cur = null; continue }
349
361
  // a file that cannot load prints no `(fail)` line, only this banner
350
- if (cur != null && (/^\(fail\) /.test(line) || line.startsWith('# Unhandled error between tests'))) out.add(cur)
362
+ if (cur != null && (/^\(fail\) /.test(line) || line.startsWith(BETWEEN_TESTS))) out.add(cur)
351
363
  }
352
364
  return [...out]
353
365
  }
354
366
 
367
+ /** the error line inside the banner block that starts at `from`, before its closing rule */
368
+ function blockError(lines: string[], from: number): string | null {
369
+ let rules = 0
370
+ for (let j = from; j < lines.length && rules < 2; j++) {
371
+ const line = lines[j]!.trim()
372
+ if (BUN_RULE.test(line)) { rules++; continue }
373
+ if (HEADER.test(line)) return null
374
+ if (ERROR_LINE.test(line)) return line
375
+ }
376
+ return null
377
+ }
378
+
355
379
  /** the FIRST failure of each failing suite, read off the same report as
356
- * failedSuites: its `(fail)` line, timing dropped, with the first `error:`
357
- * line bun printed before it in that file. What the record keeps so a page
358
- * can say why a file is red without a rerun */
380
+ * failedSuites: its `(fail)` line, timing dropped, with the first error line
381
+ * bun printed before it in that file, or the error inside the banner of one
382
+ * that threw outside any test. What the record keeps so a page can say why a
383
+ * file is red without a rerun */
359
384
  export function failLines(output: string): Map<string, string> {
360
385
  const out = new Map<string, string>()
386
+ const lines = output.split('\n')
361
387
  let cur: string | null = null
362
388
  let err: string | null = null
363
- for (const raw of output.split('\n')) {
364
- const line = raw.trimEnd()
365
- const head = line.match(/^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/)
389
+ for (let i = 0; i < lines.length; i++) {
390
+ const line = lines[i]!.trimEnd()
391
+ const head = line.match(HEADER)
366
392
  if (head != null) { cur = normSuitePath(head[1]!); err = null; continue }
367
393
  if (BUN_RECAP.test(line)) { cur = null; continue }
368
394
  if (cur == null || out.has(cur)) continue
369
- if (err == null && /^\s*error: /.test(line)) err = line.trim()
395
+ if (err == null && ERROR_LINE.test(line)) err = line.trim()
370
396
  const fail = line.match(/^\(fail\) (.*?)(?: \[[\d.]+m?s\])?$/)
371
397
  if (fail != null) out.set(cur, clip(err == null ? fail[1]! : `${fail[1]!}: ${err}`))
372
- else if (line.startsWith('# Unhandled error between tests')) out.set(cur, clip(err == null ? 'an unhandled error between tests' : `an unhandled error between tests: ${err}`))
398
+ else if (line.startsWith(BETWEEN_TESTS)) {
399
+ const thrown = blockError(lines, i + 1) ?? err
400
+ out.set(cur, clip(thrown == null ? BETWEEN_WORDS : `${BETWEEN_WORDS}: ${thrown}`))
401
+ }
373
402
  }
374
403
  return out
375
404
  }
376
405
 
377
406
  const clip = (s: string): string => (s.length > 300 ? `${s.slice(0, 299)}…` : s)
378
407
 
408
+ /** the FAIL headline of a run bun ended red with no kill, read off bun's own
409
+ * summary (`N fail`, `N error`) and the report in `out`. bun can count an
410
+ * error thrown outside any test as `0 fail`, so the errors lead, each with
411
+ * the file that threw */
412
+ export function redWords(o: { failedTests: number; errors: number; ran: number; slowest: string; out: string }): string {
413
+ const thrown = [...failLines(o.out)].filter(([, w]) => w.startsWith(BETWEEN_WORDS))
414
+ const parts: string[] = []
415
+ if (o.errors > 0 || thrown.length > 0) {
416
+ const named = thrown.slice(0, 3).map(([file, w]) => `${file} threw ${w.slice(BETWEEN_WORDS.length + 2) || 'with no message'}`)
417
+ const more = thrown.length > 3 ? `; ${thrown.length - 3} more` : ''
418
+ parts.push(`${Math.max(o.errors, thrown.length)} error(s) outside any test ${named.length > 0 ? `(${named.join('; ')}${more})` : '(bun named no file)'}`)
419
+ }
420
+ if (o.failedTests > 0) parts.push(`${o.failedTests} test(s) failed`)
421
+ if (parts.length === 0) parts.push('bun test exited red with no failed test and no error counted')
422
+ return `${parts.join(', ')}, Ran ${o.ran} tests${o.slowest}`
423
+ }
424
+
379
425
  /** the clause the headline carries for the gate line: the command phase one
380
426
  * ran, and the flags the runner added to the script's own. Per file ignores
381
427
  * stay out, the slow and serial counts already say what they leave out */
@@ -31,7 +31,7 @@ import { blobOid, listingHash } from '../core/read-set.ts'
31
31
  import { shipkitRoot } from '../ledger/db.ts'
32
32
  import { extractSpecs, resolveSpec } from '../ts/source-graph.ts'
33
33
  import { type RvtConfig } from '../ts/config.ts'
34
- import { inDotDir } from './judge-suites.ts'
34
+ import { BETWEEN_TESTS, BUN_RULE, inDotDir } from './judge-suites.ts'
35
35
 
36
36
  export const SELECT_VERSION = 5
37
37
 
@@ -403,9 +403,6 @@ export function selectSuites(o: {
403
403
  return { all: false, run, why: reasons, changed: o.changed.size }
404
404
  }
405
405
 
406
- const BETWEEN_TESTS = '# Unhandled error between tests'
407
- const RULE = /^-{20,}$/
408
-
409
406
  /** the repo modules named in the frames of every error bun printed between
410
407
  * tests (a module whose top level threw), repo-relative to `top` */
411
408
  export function loadErrorModules(out: string, top: string): string[] {
@@ -419,7 +416,7 @@ export function loadErrorModules(out: string, top: string): string[] {
419
416
  let rules = 0
420
417
  for (let j = i + 1; j < lines.length && rules < 2; j++) {
421
418
  const line = lines[j]!
422
- if (RULE.test(line.trim())) { rules++; continue }
419
+ if (BUN_RULE.test(line.trim())) { rules++; continue }
423
420
  const m = /^\s+at .*?\(?((?:\/|[A-Za-z]:\\)[^():]+):\d+(?::\d+)?\)?\s*$/.exec(line)
424
421
  if (m == null) continue
425
422
  const base = roots.find((r) => m[1]!.startsWith(`${r}/`))
@@ -20,7 +20,7 @@ import { SUITE_READS_ENV, SUITE_ROOT_ENV, SUITE_SELECT_ENV } from './reads-env.t
20
20
  import { changedKeys, dropStaticEdges, extras, isBunSuite, envFilesOf, foldReads, loadErrorModules, loadState, preloadsOf, saveState, selectStatePath, selectSuites, snapshot, suiteFiles, taintedSuites, walkGraph, type GraphEntry, type Selection, type SelectState } from './select.ts'
21
21
  import { gitRoot } from '../core/git-root.ts'
22
22
  import { loadConfig } from '../ts/config.ts'
23
- import { aloneRunsNeeded, bunfigTest, type BunfigTest, readPhaseCpu, SUITE_CPU_ENV, type AloneRun, type Confirmed, confirmOver, crashedWorkers, interruptedSuites, overWords, ranAs, retryAlone, smallerRun, stallWords, DEFAULT_BUDGET_MS, FULL_CEILING_MS, failedSuites, failLines, judgeSuites, normSuitePath, parseSuiteFlags, planPhases, QUICK_CEILING_MS, poolWorkers } from './judge-suites.ts'
23
+ import { aloneRunsNeeded, bunfigTest, type BunfigTest, readPhaseCpu, SUITE_CPU_ENV, type AloneRun, type Confirmed, confirmOver, crashedWorkers, interruptedSuites, overWords, ranAs, retryAlone, smallerRun, stallWords, DEFAULT_BUDGET_MS, FULL_CEILING_MS, failedSuites, failLines, judgeSuites, normSuitePath, parseSuiteFlags, planPhases, QUICK_CEILING_MS, poolWorkers, redWords } from './judge-suites.ts'
24
24
 
25
25
  type Phase = { code: number; signal: string | null; stalled: boolean; out: string; timings: Record<string, number>; cpuMs: number | null }
26
26
 
@@ -196,6 +196,7 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
196
196
  const phases = [p1, ...(p2 != null ? [p2] : [])]
197
197
  const ran = phases.reduce((n, p) => n + lastCount(p.out, /Ran (\d+) tests?/g), 0)
198
198
  const failedTests = phases.reduce((n, p) => n + lastCount(p.out, /^\s*(\d+) fail$/gm), 0)
199
+ const errors = phases.reduce((n, p) => n + lastCount(p.out, /^\s*(\d+) errors?$/gm), 0)
199
200
  const timings = Object.assign({}, ...phases.map((p) => p.timings)) as Record<string, number>
200
201
  const crashed = crashedWorkers(p1.out)
201
202
  const red = new Set([...phases.flatMap((p) => failedSuites(p.out)), ...crashed.map((c) => c.file)])
@@ -290,7 +291,7 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
290
291
  ...killed.map((k) => `FAIL: ${k}`),
291
292
  ...(vacuous ? ['FAIL: ran 0 tests: a suite with no test in it proves nothing, write a real one'] : []),
292
293
  ...(!vacuous && (failedTests > 0 || (phases.some((p) => p.code !== 0 && p.signal == null) && killed.length === 0))
293
- ? [`FAIL: ${failedTests} test(s) failed, Ran ${ran} tests${slowest}`] : []),
294
+ ? [`FAIL: ${redWords({ failedTests, errors, ran, slowest, out: phases.map((p) => p.out).join('\n') })}`] : []),
294
295
  ...(over.length > 0
295
296
  ? [`FAIL: ${over.length} suite(s) over the ${budget}ms budget: ${over.map((c) => overWords(c, load, budget)).join(', ')} (run alone, load ${load.load1.toFixed(1)} on ${load.cores} cores before the run). Make each fast, or declare it in ${declaredIn}.slow with what it catches and why it cannot be fast`]
296
297
  : []),
@@ -0,0 +1,78 @@
1
+ // what a repo adds to the trailer core, all optional (wiki build/trailer.md, The trailer unit). Filming is never here:
2
+ // a film line calls the project CLI's film rows (vocab.ts). A `trailer({...})` unit in any `<name>.shipkit.ts`
3
+ // resolves the sounds that are not files, names the paths a take's world is, checks the content the language cannot
4
+ // know, refines one film line's take, and turns on the guards that read the repo's own words. Discovered like every
5
+ // unit file (load-unit.ts)
6
+ import type { TakeLength } from './grammar.ts'
7
+
8
+ export interface TrailerSounds {
9
+ /** a music id to an audio file of at least `seconds`, cached by the repo */
10
+ music?(id: string, o: { take?: string; seconds: number }): Promise<string>
11
+ /** an sfx id to an audio file */
12
+ sfx?(id: string): Promise<string>
13
+ /** a voice line, found by its text, to an audio file */
14
+ voice?(text: string): Promise<string>
15
+ }
16
+
17
+ /** a film line of the project CLI as the unit sees it: its words as written, the text of each `@<file>` word */
18
+ export interface TrailerFilmLine {
19
+ line: number
20
+ /** the shot or footage block it films */
21
+ shot: string
22
+ argv: string[]
23
+ output: 'still' | 'clip'
24
+ files: Record<string, string>
25
+ }
26
+
27
+ /** what `check` reads: the cut's film lines and the sounds it names by id */
28
+ export interface TrailerCheckInput {
29
+ file: string
30
+ dir: string
31
+ size: [number, number]
32
+ fps: number
33
+ films: TrailerFilmLine[]
34
+ /** each music cue that names an id; `opening`: the header's, the one the cut starts on */
35
+ music: { line: number; id: string; take?: string; from: number; opening: boolean }[]
36
+ sfx: { line: number; id: string }[]
37
+ voices: { line: number; text: string }[]
38
+ }
39
+
40
+ /** an error at its line; `warn`: said, never blocking */
41
+ export interface TrailerFinding {
42
+ line: number
43
+ message: string
44
+ warn?: true
45
+ }
46
+
47
+ /** one film line's take over its row's defaults: its version, how long it films, a fixed world, what its key reads */
48
+ export interface TakeAnswer {
49
+ version?: number | string
50
+ length?: TakeLength
51
+ /** the take's world for good (`rev:<commit>`): never stale */
52
+ world?: string
53
+ /** what the key reads instead of the words */
54
+ recipe?: unknown
55
+ }
56
+
57
+ export interface TrailerDef {
58
+ sound?: TrailerSounds
59
+ /** the paths a take's world is (`['src', 'public']`); default the whole tree, trailer/ and .shipkit/ left out */
60
+ stamp?: string[]
61
+ /** in process, before filming, in a few ms: the content the language cannot know */
62
+ check?(c: TrailerCheckInput): TrailerFinding[] | Promise<TrailerFinding[]>
63
+ /** one film line's take, when its words change what its row says */
64
+ take?(f: TrailerFilmLine & { version: number | string | null; length: TakeLength }): TakeAnswer | undefined
65
+ /** the `states` a film prints: parts a cut may land on (`ignore`), parts a shot never opens or ends on (`underWay`) */
66
+ states?: { ignore?: string[]; underWay?: string[] }
67
+ /** a clip take that drops its picture for a few frames is filmed again once, then refused (holes.ts) */
68
+ holes?: boolean
69
+ }
70
+
71
+ export type TrailerUnit = TrailerDef & { kind: 'trailer' }
72
+
73
+ export function trailer(d: TrailerDef): TrailerUnit {
74
+ for (const [k, f] of Object.entries(d.sound ?? {})) if (typeof f !== 'function') throw new Error(`trailer: sound.${k} is a function returning an audio file`)
75
+ for (const k of ['check', 'take'] as const) if (d[k] !== undefined && typeof d[k] !== 'function') throw new Error(`trailer: ${k} is a function`)
76
+ if (d.stamp !== undefined && (!Array.isArray(d.stamp) || d.stamp.some((p) => typeof p !== 'string' || p === '' || p.startsWith('/')))) throw new Error('trailer: stamp is a list of repo relative paths')
77
+ return { ...d, kind: 'trailer' }
78
+ }