rvlib-shipkit 1.8.0 → 2.0.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 (127) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/package.json +1 -1
  3. package/src/assets/gen/comfyts-gen.ts +31 -14
  4. package/src/assets/gen/generate-roll.ts +20 -7
  5. package/src/assets/kit-config.ts +9 -0
  6. package/src/assets/kits/identity-kit.ts +7 -1
  7. package/src/assets/run-assets.ts +3 -3
  8. package/src/assets/server/studio-server.ts +3 -3
  9. package/src/check.ts +5 -2
  10. package/src/checks/SK001-package-json-shape.check.ts +47 -12
  11. package/src/checks/SK013-one-way-in-policy.check.ts +8 -7
  12. package/src/checks/SK017-vscode-hub-task.check.ts +109 -106
  13. package/src/checks/SK019-tests-run-in-units.check.ts +2 -1
  14. package/src/checks/SK027-agent-wiki-links.check.ts +5 -2
  15. package/src/checks/SK029-explicit-check-files.check.ts +25 -8
  16. package/src/checks/SK034-commit-gates-armed.check.ts +90 -0
  17. package/src/cli/cmd_Ci.ts +6 -5
  18. package/src/cli/cmd_Commands.ts +21 -5
  19. package/src/cli/cmd_Hooks.ts +6 -2
  20. package/src/cli/cmd_Init.ts +1 -1
  21. package/src/cli/cmd_Serve.ts +1 -2
  22. package/src/cli/cmd_Start.ts +1 -1
  23. package/src/cli.ts +2 -4
  24. package/src/collections/providers.ts +1 -1
  25. package/src/core/budget.ts +6 -8
  26. package/src/core/bun-builtins.ts +36 -0
  27. package/src/core/bun-tool-cli.ts +23 -0
  28. package/src/core/canonical-scripts.ts +6 -4
  29. package/src/core/check-ctx.ts +104 -10
  30. package/src/core/check-file-names.ts +2 -3
  31. package/src/core/check-pass.ts +17 -1
  32. package/src/core/check-runner.ts +32 -10
  33. package/src/core/config-rewrite.ts +27 -6
  34. package/src/core/discover-checks.ts +64 -15
  35. package/src/core/fix-scope.ts +5 -0
  36. package/src/core/floor-resolve.ts +18 -8
  37. package/src/core/git-root.ts +22 -12
  38. package/src/core/job-run.ts +2 -2
  39. package/src/core/layout.ts +4 -1
  40. package/src/core/legacy-shapes.ts +40 -0
  41. package/src/core/one-lib.ts +5 -3
  42. package/src/core/one-way-in-policy.ts +106 -13
  43. package/src/core/pre-commit-discovery.ts +106 -0
  44. package/src/core/retired-checks.ts +2 -0
  45. package/src/core/run-ci.ts +23 -16
  46. package/src/core/run-fix.ts +9 -0
  47. package/src/core/run-fixers.ts +91 -35
  48. package/src/core/runs-matrix.ts +7 -0
  49. package/src/core/slots.ts +36 -6
  50. package/src/core/vendored-read.ts +39 -0
  51. package/src/core/vendored.ts +82 -0
  52. package/src/core/waivers.ts +125 -0
  53. package/src/core/when-block.ts +25 -185
  54. package/src/core/when.ts +30 -13
  55. package/src/core/wire.ts +17 -1
  56. package/src/git-bootstrap.ts +1 -1
  57. package/src/home/attach.ts +3 -5
  58. package/src/home/gui-app/ShipkitGui.swift +96 -0
  59. package/src/home/insights.ts +6 -1
  60. package/src/home/pm-config.ts +2 -2
  61. package/src/home/resources.ts +3 -0
  62. package/src/home/run-daemon.ts +2 -2
  63. package/src/home/run-unit-action.ts +2 -2
  64. package/src/hooks/commit-message.ts +2 -14
  65. package/src/hooks/hook-gates.ts +38 -0
  66. package/src/hooks/leak-guard.ts +8 -3
  67. package/src/hooks/one-way-in-guard.ts +8 -2
  68. package/src/hooks/root-identity.ts +9 -6
  69. package/src/hooks/run-hooks.ts +72 -41
  70. package/src/hooks/staged.ts +49 -0
  71. package/src/hooks/tree-shrink.ts +41 -0
  72. package/src/init-repo.ts +0 -42
  73. package/src/ledger/check-table.ts +1 -1
  74. package/src/ledger/gate-checks.ts +45 -9
  75. package/src/ledger/ledger.ts +6 -1
  76. package/src/ledger/repo-view.ts +14 -4
  77. package/src/ledger/toolchain-identity.ts +4 -2
  78. package/src/mv/mv-md.ts +6 -1
  79. package/src/plan/planner.ts +21 -4
  80. package/src/plan/repo-source.ts +7 -3
  81. package/src/problems/extract.ts +27 -5
  82. package/src/problems/problem-line.ts +2 -3
  83. package/src/release/run-release.ts +30 -16
  84. package/src/release/unit-gate.ts +12 -5
  85. package/src/repo-config.ts +51 -27
  86. package/src/scaffold/files/bunfig.toml +11 -0
  87. package/src/scaffold/files/vscode-settings.json +1 -2
  88. package/src/scaffold/jsonc-spans.ts +16 -0
  89. package/src/scaffold/project-scaffold.ts +43 -77
  90. package/src/serve/hub-app/check-view.tsx +4 -4
  91. package/src/serve/hub-app/check-words.ts +1 -0
  92. package/src/serve/hub-app/checks-svc.ts +1 -2
  93. package/src/serve/hub-app/home.html +0 -1
  94. package/src/serve/hub-app/hub-types.ts +0 -2
  95. package/src/serve/hub-app/index.html +0 -1
  96. package/src/serve/hub-app/project-list.css +1 -2
  97. package/src/serve/hub-app/tree-build.ts +3 -2
  98. package/src/serve/hub-app/tree-svc.ts +2 -2
  99. package/src/serve/hub-port.ts +3 -3
  100. package/src/serve/hub-runtime.ts +3 -5
  101. package/src/serve/project-gutter.ts +5 -2
  102. package/src/serve/run-lanes.ts +7 -3
  103. package/src/serve/serve-hub.ts +1 -16
  104. package/src/serve/tracked-watch.ts +1 -1
  105. package/src/smoke/run-smoke.ts +13 -6
  106. package/src/start-repo.ts +15 -3
  107. package/src/suite/judge-suites.ts +81 -32
  108. package/src/suite/select.ts +2 -0
  109. package/src/suite/suite-cli.ts +27 -16
  110. package/src/suite/suite-cpu-preload.ts +7 -11
  111. package/src/suite/test-units.ts +7 -4
  112. package/src/ts/config.ts +12 -5
  113. package/src/ts/imports.ts +3 -1
  114. package/src/ts/typecheck-cache.ts +11 -0
  115. package/src/ts/typecheck-cli.ts +2 -1
  116. package/src/tui/build-steps-panel.ts +1 -1
  117. package/src/wiki/scan-md.ts +16 -9
  118. package/src/checks/SK009-commit-ai-tells.check.ts +0 -131
  119. package/src/checks/SK014-prose-ste.check.ts +0 -78
  120. package/src/cli/cmd_Prose.ts +0 -74
  121. package/src/prose/prose-view.ts +0 -90
  122. package/src/prose/run-prose.ts +0 -187
  123. package/src/prose/ste.ts +0 -552
  124. package/src/scaffold/files/vscode-tasks.json +0 -24
  125. package/src/serve/hub-app/prose-hits-st.ts +0 -125
  126. package/src/serve/hub-app/prose-hits-view.tsx +0 -176
  127. package/src/serve/hub-app/prose-hits.css +0 -21
@@ -1,125 +0,0 @@
1
- // The SK014 page's own state: the hits the server grouped, the filters over
2
- // them, and the word list form. Component scoped (it lives while the page is
3
- // open), and every write goes through the ONE config writer the run when
4
- // switch uses (checks-svc.ts writeGate), so a word list edit is refused for
5
- // the same reasons a switch is.
6
- import { makeAutoObservable, observableRef, runInAction } from 'mobx'
7
- import type { ProseGroup, ProseView } from '../../prose/prose-view.ts'
8
- import type { RepoSt } from './repo-st.ts'
9
- import { ownTrouble, reach } from './reach.ts'
10
-
11
- export type TierFilter = 'all' | 'blocking' | 'soft'
12
- export type ListKind = 'allow' | 'ban'
13
-
14
- /** how many groups the page draws before a "show every word" button */
15
- export const GROUP_CAP = 40
16
- /** how many hits one open group draws before its own "show all" */
17
- export const HIT_CAP = 60
18
-
19
- export class ProseHitsSt {
20
- view: ProseView | null = null
21
- error = ''
22
- loading = false
23
- word = ''
24
- file = ''
25
- tier: TierFilter = 'all'
26
- allGroups = false
27
- /** group keys the reader opened */
28
- open = new Set<string>()
29
- /** group keys whose every hit is drawn */
30
- full = new Set<string>()
31
- form: { kind: ListKind; word: string; to: string; reason: string } = { kind: 'allow', word: '', to: '', reason: '' }
32
-
33
- constructor(private st: RepoSt) {
34
- // .ref: the view is swapped whole on every load, never edited in place
35
- makeAutoObservable<this, 'st'>(this, { st: false, view: observableRef })
36
- }
37
-
38
- async load(): Promise<void> {
39
- runInAction(() => { this.loading = true })
40
- try {
41
- const r = await reach(this.st.url('/api/prose.json'))
42
- if (!r.ok) throw new Error(await r.text())
43
- const v = (await r.json()) as ProseView
44
- runInAction(() => { this.view = v; this.error = '' })
45
- } catch (e) {
46
- runInAction(() => { this.error = ownTrouble(e, this.st.home.machine, (m) => `the hits could not be read: ${m}`) })
47
- }
48
- runInAction(() => { this.loading = false })
49
- }
50
-
51
- setWord(v: string): void { this.word = v }
52
- setFile(v: string): void { this.file = v }
53
- setTier(v: TierFilter): void { this.tier = v }
54
- showAllGroups(): void { this.allGroups = true }
55
- toggle(key: string): void {
56
- if (this.open.has(key)) this.open.delete(key)
57
- else this.open.add(key)
58
- }
59
- showAllHits(key: string): void { this.full.add(key) }
60
- setForm<K extends keyof ProseHitsSt['form']>(k: K, v: ProseHitsSt['form'][K]): void {
61
- this.form = { ...this.form, [k]: v }
62
- }
63
-
64
- /** the groups under the filters, each recounted over the hits it keeps,
65
- * so a file filter reorders the words by their noise IN that file */
66
- get groups(): ProseGroup[] {
67
- const v = this.view
68
- if (v == null) return []
69
- const w = this.word.trim().toLowerCase()
70
- const f = this.file.trim().toLowerCase()
71
- const out: ProseGroup[] = []
72
- for (const g of v.groups) {
73
- if (this.tier === 'blocking' && g.tier === 'soft') continue
74
- if (this.tier === 'soft' && g.tier !== 'soft') continue
75
- if (w !== '' && !(g.word ?? g.cls).includes(w) && !g.cls.includes(w)) continue
76
- const hits = f === '' ? g.hits : g.hits.filter((h) => h.file.toLowerCase().includes(f))
77
- if (hits.length === 0) continue
78
- out.push(hits === g.hits ? g : { ...g, hits, count: hits.length })
79
- }
80
- return out.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))
81
- }
82
-
83
- get shownCount(): number {
84
- return this.groups.reduce((n, g) => n + g.count, 0)
85
- }
86
-
87
- /** every file that carries a hit, for the file filter's suggestions */
88
- get files(): string[] {
89
- const s = new Set<string>()
90
- for (const g of this.view?.groups ?? []) for (const h of g.hits) s.add(h.file)
91
- return [...s].sort()
92
- }
93
-
94
- get busy(): boolean { return this.st.checks.gateBusy }
95
-
96
- /** after any write: the config moved, so the hits are re-read and the
97
- * rule reruns, and its row recolours on the tree like any other run */
98
- private async after(ok: boolean): Promise<void> {
99
- if (!ok) return
100
- await this.load()
101
- void this.st.checks.run(['SK014'])
102
- }
103
-
104
- async allow(word: string, reason = ''): Promise<void> {
105
- const ok = await this.st.checks.writeGate({ words: { allow: [{ word, reason }] } }, `'${word}' is allowed in this repo — repo.config.ts is edited and waiting for your commit`)
106
- await this.after(ok)
107
- }
108
-
109
- async submit(): Promise<void> {
110
- const f = this.form
111
- if (f.word.trim() === '') return
112
- const entry = f.kind === 'allow' ? { allow: [{ word: f.word, reason: f.reason }] } : { ban: [{ word: f.word, to: f.to, reason: f.reason }] }
113
- const said = f.kind === 'allow'
114
- ? `'${f.word.trim()}' is allowed in this repo — repo.config.ts is edited and waiting for your commit`
115
- : `'${f.word.trim()}' is refused in this repo — repo.config.ts is edited and waiting for your commit`
116
- const ok = await this.st.checks.writeGate({ words: entry }, said)
117
- if (ok) runInAction(() => { this.form = { kind: this.form.kind, word: '', to: '', reason: '' } })
118
- await this.after(ok)
119
- }
120
-
121
- async remove(word: string): Promise<void> {
122
- const ok = await this.st.checks.writeGate({ words: { remove: [word] } }, `'${word}' is back to the house dictionary — repo.config.ts is edited and waiting for your commit`)
123
- await this.after(ok)
124
- }
125
- }
@@ -1,176 +0,0 @@
1
- // THE SK014 PAGE's own sections: what the prose check found, grouped by word
2
- // with the noisiest on top, each hit a file:line link into the editor and the
3
- // sentence around it with the word marked; then the repo's own word lists,
4
- // editable in place. The house dictionary stays the default, a list entry
5
- // overrides it for this repo only.
6
- import { observer } from 'mobx-react-lite'
7
- import { useEffect, useMemo } from 'react'
8
- import type { ProseGroup, ProseViewHit } from '../../prose/prose-view.ts'
9
- import type { ProseWords } from '../../prose/ste.ts'
10
- import { editorHref } from './check-words.ts'
11
- import { Help } from './help.tsx'
12
- import type { RepoSt } from './repo-st.ts'
13
- import { GROUP_CAP, HIT_CAP, ProseHitsSt, type ListKind, type TierFilter } from './prose-hits-st.ts'
14
-
15
- /** blocking tiers read red, the soft tier reads as a warning */
16
- const TIER_WORD: Record<string, { word: string; tone: string }> = {
17
- fix: { word: 'blocks, --fix rewrites it', tone: 'red' },
18
- hard: { word: 'blocks', tone: 'red' },
19
- soft: { word: 'soft', tone: 'warn' },
20
- }
21
-
22
- const TIERS: { v: TierFilter; label: string }[] = [
23
- { v: 'all', label: 'every hit' },
24
- { v: 'blocking', label: 'what blocks' },
25
- { v: 'soft', label: 'soft only' },
26
- ]
27
-
28
- /** the sentence around a hit, the hit itself marked */
29
- const Excerpt = ({ h }: { h: ProseViewHit }) => (
30
- <span className="pexc">
31
- {h.excerpt.slice(0, h.mark[0])}
32
- <mark>{h.excerpt.slice(h.mark[0], h.mark[1])}</mark>
33
- {h.excerpt.slice(h.mark[1])}
34
- </span>
35
- )
36
-
37
- const HitLine = ({ root, h, shape }: { root: string; h: ProseViewHit; shape: boolean }) => {
38
- const href = editorHref(root, h.file)
39
- const at = `${h.file}:${h.line}`
40
- return (
41
- <div className="phit">
42
- {href == null ? <code className="cpath">{at}</code> : <a href={`${href}:${h.line}`} title="open it in the editor"><code className="cpath">{at}</code></a>}
43
- <Excerpt h={h} />
44
- {/* a shape's reason names its own numbers (31 words, 8 sentences) */}
45
- {shape ? <span className="dim pwhy">{h.why}</span> : null}
46
- </div>
47
- )
48
- }
49
-
50
- const Group = observer(({ app, s, g }: { app: RepoSt; s: ProseHitsSt; g: ProseGroup }) => {
51
- const open = s.open.has(g.key)
52
- const t = TIER_WORD[g.tier] ?? { word: g.tier, tone: '' }
53
- const shown = s.full.has(g.key) ? g.hits : g.hits.slice(0, HIT_CAP)
54
- return (
55
- <div className="pgroup">
56
- <div className="phead">
57
- <button className="ptoggle" onClick={() => s.toggle(g.key)} title={open ? 'hide the hits' : 'show the hits'}>{open ? '▾' : '▸'}</button>
58
- <span className="pcount dnum">{g.count}</span>
59
- <b className="pword">{g.word ?? g.cls}</b>
60
- <span className="stchip">{g.cls}</span>
61
- <span className={`tl ${t.tone}`}>{t.word}</span>
62
- <span className="dim pwhy">{g.word == null ? null : g.hits[0]?.why}</span>
63
- <span className="sp" />
64
- {g.word == null
65
- ? null
66
- : <button disabled={s.busy} title="accept this word in this repo: every hit on it goes away" onClick={() => void s.allow(g.word!)}>allow it here</button>}
67
- </div>
68
- {open
69
- ? <div className="phits">
70
- {shown.map((h, n) => <HitLine key={`${h.file}:${h.line}:${n}`} root={app.root} h={h} shape={g.word == null} />)}
71
- {shown.length < g.hits.length
72
- ? <button className="pmore" onClick={() => s.showAllHits(g.key)}>show the other {g.hits.length - shown.length}</button>
73
- : null}
74
- </div>
75
- : null}
76
- </div>
77
- )
78
- })
79
-
80
- const Hits = observer(({ app, s }: { app: RepoSt; s: ProseHitsSt }) => {
81
- const v = s.view
82
- if (v == null) return <div className="dim">{s.error !== '' ? s.error : 'reading the hits…'}</div>
83
- if (!v.scoped) return <div className="dim">this repo declares no prose scope, so the check reads no file: add `prose: {'{'} include: [...] {'}'}` to repo.config.ts</div>
84
- const groups = s.groups
85
- const drawn = s.allGroups ? groups : groups.slice(0, GROUP_CAP)
86
- const blocking = v.byTier.fix + v.byTier.hard
87
- return (
88
- <>
89
- <div className="wline">
90
- {v.total} hit(s) in {v.files} file(s): <span className={blocking > 0 ? 'tl red' : 'dim'}>{blocking} block</span>, <span className="tl warn">{v.byTier.soft} soft</span>
91
- {s.loading ? <span className="dim"> (reading again…)</span> : null}
92
- </div>
93
- <div className="pfilters">
94
- <input placeholder="filter by word or rule" value={s.word} onChange={(e) => s.setWord(e.target.value)} />
95
- <input placeholder="filter by file" list="prose-files" value={s.file} onChange={(e) => s.setFile(e.target.value)} />
96
- <datalist id="prose-files">{s.files.map((f) => <option key={f} value={f} />)}</datalist>
97
- <select className="wsel" value={s.tier} onChange={(e) => s.setTier(e.target.value as TierFilter)}>
98
- {TIERS.map((t) => <option key={t.v} value={t.v}>{t.label}</option>)}
99
- </select>
100
- {s.word !== '' || s.file !== '' || s.tier !== 'all' ? <span className="dim">{s.shownCount} shown</span> : null}
101
- </div>
102
- {groups.length === 0 ? <div className="dim">no hit matches the filters</div> : null}
103
- {drawn.map((g) => <Group key={g.key} app={app} s={s} g={g} />)}
104
- {drawn.length < groups.length
105
- ? <button className="pmore" onClick={() => s.showAllGroups()}>show the other {groups.length - drawn.length} words and rules</button>
106
- : null}
107
- </>
108
- )
109
- })
110
-
111
- const ListRows = ({ s, words }: { s: ProseHitsSt; words: ProseWords }) => {
112
- const rows = [
113
- ...(words.allow ?? []).map((a) => ({ kind: 'allow' as ListKind, word: a.word, to: undefined as string | undefined, reason: a.reason })),
114
- ...(words.ban ?? []).map((b) => ({ kind: 'ban' as ListKind, word: b.word, to: b.to, reason: b.reason })),
115
- ]
116
- if (rows.length === 0) return <div className="dim">no entry: the house dictionary decides every word here</div>
117
- return (
118
- <div className="plist">
119
- {rows.map((r) => (
120
- <div key={`${r.kind}:${r.word}`} className="prow">
121
- <span className={`stchip ${r.kind === 'allow' ? 'ok' : 'miss'}`}>{r.kind}</span>
122
- <b>{r.word}</b>
123
- {r.to == null || r.to === '' ? null : <span>→ {r.to}</span>}
124
- {r.reason == null || r.reason === '' ? null : <span className="dim">{r.reason}</span>}
125
- <span className="sp" />
126
- <button disabled={s.busy} onClick={() => void s.remove(r.word)}>drop it</button>
127
- </div>
128
- ))}
129
- </div>
130
- )
131
- }
132
-
133
- const Lists = observer(({ app, s }: { app: RepoSt; s: ProseHitsSt }) => {
134
- const f = s.form
135
- const g = app.checks.gate
136
- return (
137
- <>
138
- <ListRows s={s} words={s.view?.words ?? {}} />
139
- <div className="pform">
140
- <select className="wsel" value={f.kind} onChange={(e) => s.setForm('kind', e.target.value as ListKind)}>
141
- <option value="allow">allow</option>
142
- <option value="ban">ban</option>
143
- </select>
144
- <input placeholder={f.kind === 'allow' ? 'word or phrase this repo accepts' : 'word or phrase this repo refuses'} value={f.word} onChange={(e) => s.setForm('word', e.target.value)} />
145
- {f.kind === 'ban' ? <input placeholder="write this instead" value={f.to} onChange={(e) => s.setForm('to', e.target.value)} /> : null}
146
- <input placeholder="why (optional, it rides in the config)" value={f.reason} onChange={(e) => s.setForm('reason', e.target.value)} />
147
- <button disabled={s.busy || f.word.trim() === ''} onClick={() => void s.submit()}>{f.kind === 'allow' ? 'allow it' : 'ban it'}</button>
148
- </div>
149
- {g?.wordsState === 'stale' ? <div className="warnline">the words block in {g.path} was hand edited: reconcile it, then the lists work again</div> : null}
150
- {g != null && g.dirty ? <div className="note">{g.path} is edited and not committed, review it in your git client</div> : null}
151
- {app.checks.gateNote === '' ? null : <div className="note">{app.checks.gateNote}</div>}
152
- </>
153
- )
154
- })
155
-
156
- /** the two sections over one state, the part a test renders headless */
157
- export const ProseSections = observer(({ app, s }: { app: RepoSt; s: ProseHitsSt }) => (
158
- <>
159
- <section className="csec">
160
- <div className="cstitle">hits<Help text="every hit of the last read, grouped by the word or the rule that fired, the noisiest first. A file:line opens the editor on that line" /></div>
161
- <Hits app={app} s={s} />
162
- </section>
163
- <section className="csec">
164
- <div className="cstitle">this repo's words<Help text="allow drops every hit on a word or phrase, ban refuses an extra one and names what to write instead. The house dictionary stays the default. An edit writes repo.config.ts, reruns the check and leaves the file for you to commit" /></div>
165
- <Lists app={app} s={s} />
166
- </section>
167
- </>
168
- ))
169
-
170
- /** mounted by the check page for SK014 only: the state lives while the page
171
- * is open, and reads the hits once on the way in */
172
- export const ProseHits = observer(({ app }: { app: RepoSt }) => {
173
- const s = useMemo(() => new ProseHitsSt(app), [app])
174
- useEffect(() => { void s.load() }, [s])
175
- return <ProseSections app={app} s={s} />
176
- })
@@ -1,21 +0,0 @@
1
- /* the SK014 page: word groups, their hits, the repo's word lists (prose-hits-view.tsx) */
2
- .pfilters{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:center;margin-bottom:var(--s2)}
3
- .pfilters input,.pform input{background:var(--bg);border:1px solid var(--line);border-radius:4px;color:var(--ink);
4
- padding:var(--s1) var(--s2);font:inherit;font-size:12.5px}
5
- .pfilters input:focus,.pform input:focus{border-color:var(--accent);outline:none}
6
- .pgroup{border-top:1px solid var(--line)}
7
- .phead{display:flex;gap:var(--s2);align-items:baseline;padding:var(--s1) 0;min-width:0}
8
- .phead .sp,.prow .sp{flex:1}
9
- .phead .pwhy{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
10
- .ptoggle{background:none;border:0;color:var(--dim);cursor:pointer;padding:0;font:inherit}
11
- .pcount{min-width:3.5em;text-align:right}
12
- .pword{font-weight:500}
13
- .phits{padding:0 0 var(--s2) var(--s4)}
14
- .phit{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:baseline;padding:var(--s0) 0;font-size:12.5px}
15
- .phit a{text-decoration:none}
16
- .pexc{color:var(--dim);word-break:break-word}
17
- .pexc mark{background:var(--hi2);color:var(--ink);border-bottom:1px solid var(--red);padding:0 var(--s0)}
18
- .pmore{margin:var(--s1) 0}
19
- .plist{display:flex;flex-direction:column;gap:var(--s1);margin-bottom:var(--s2)}
20
- .prow{display:flex;gap:var(--s2);align-items:baseline}
21
- .pform{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:center}