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.
- package/CHANGELOG.md +66 -0
- package/package.json +1 -1
- package/src/assets/gen/comfyts-gen.ts +31 -14
- package/src/assets/gen/generate-roll.ts +20 -7
- package/src/assets/kit-config.ts +9 -0
- package/src/assets/kits/identity-kit.ts +7 -1
- package/src/assets/run-assets.ts +3 -3
- package/src/assets/server/studio-server.ts +3 -3
- package/src/check.ts +5 -2
- package/src/checks/SK001-package-json-shape.check.ts +47 -12
- package/src/checks/SK013-one-way-in-policy.check.ts +8 -7
- package/src/checks/SK017-vscode-hub-task.check.ts +109 -106
- package/src/checks/SK019-tests-run-in-units.check.ts +2 -1
- package/src/checks/SK027-agent-wiki-links.check.ts +5 -2
- package/src/checks/SK029-explicit-check-files.check.ts +25 -8
- package/src/checks/SK034-commit-gates-armed.check.ts +90 -0
- package/src/cli/cmd_Ci.ts +6 -5
- package/src/cli/cmd_Commands.ts +21 -5
- package/src/cli/cmd_Hooks.ts +6 -2
- package/src/cli/cmd_Init.ts +1 -1
- package/src/cli/cmd_Serve.ts +1 -2
- package/src/cli/cmd_Start.ts +1 -1
- package/src/cli.ts +2 -4
- package/src/collections/providers.ts +1 -1
- package/src/core/budget.ts +6 -8
- package/src/core/bun-builtins.ts +36 -0
- package/src/core/bun-tool-cli.ts +23 -0
- package/src/core/canonical-scripts.ts +6 -4
- package/src/core/check-ctx.ts +104 -10
- package/src/core/check-file-names.ts +2 -3
- package/src/core/check-pass.ts +17 -1
- package/src/core/check-runner.ts +32 -10
- package/src/core/config-rewrite.ts +27 -6
- package/src/core/discover-checks.ts +64 -15
- package/src/core/fix-scope.ts +5 -0
- package/src/core/floor-resolve.ts +18 -8
- package/src/core/git-root.ts +22 -12
- package/src/core/job-run.ts +2 -2
- package/src/core/layout.ts +4 -1
- package/src/core/legacy-shapes.ts +40 -0
- package/src/core/one-lib.ts +5 -3
- package/src/core/one-way-in-policy.ts +106 -13
- package/src/core/pre-commit-discovery.ts +106 -0
- package/src/core/retired-checks.ts +2 -0
- package/src/core/run-ci.ts +23 -16
- package/src/core/run-fix.ts +9 -0
- package/src/core/run-fixers.ts +91 -35
- package/src/core/runs-matrix.ts +7 -0
- package/src/core/slots.ts +36 -6
- package/src/core/vendored-read.ts +39 -0
- package/src/core/vendored.ts +82 -0
- package/src/core/waivers.ts +125 -0
- package/src/core/when-block.ts +25 -185
- package/src/core/when.ts +30 -13
- package/src/core/wire.ts +17 -1
- package/src/git-bootstrap.ts +1 -1
- package/src/home/attach.ts +3 -5
- package/src/home/gui-app/ShipkitGui.swift +96 -0
- package/src/home/insights.ts +6 -1
- package/src/home/pm-config.ts +2 -2
- package/src/home/resources.ts +3 -0
- package/src/home/run-daemon.ts +2 -2
- package/src/home/run-unit-action.ts +2 -2
- package/src/hooks/commit-message.ts +2 -14
- package/src/hooks/hook-gates.ts +38 -0
- package/src/hooks/leak-guard.ts +8 -3
- package/src/hooks/one-way-in-guard.ts +8 -2
- package/src/hooks/root-identity.ts +9 -6
- package/src/hooks/run-hooks.ts +72 -41
- package/src/hooks/staged.ts +49 -0
- package/src/hooks/tree-shrink.ts +41 -0
- package/src/init-repo.ts +0 -42
- package/src/ledger/check-table.ts +1 -1
- package/src/ledger/gate-checks.ts +45 -9
- package/src/ledger/ledger.ts +6 -1
- package/src/ledger/repo-view.ts +14 -4
- package/src/ledger/toolchain-identity.ts +4 -2
- package/src/mv/mv-md.ts +6 -1
- package/src/plan/planner.ts +21 -4
- package/src/plan/repo-source.ts +7 -3
- package/src/problems/extract.ts +27 -5
- package/src/problems/problem-line.ts +2 -3
- package/src/release/run-release.ts +30 -16
- package/src/release/unit-gate.ts +12 -5
- package/src/repo-config.ts +51 -27
- package/src/scaffold/files/bunfig.toml +11 -0
- package/src/scaffold/files/vscode-settings.json +1 -2
- package/src/scaffold/jsonc-spans.ts +16 -0
- package/src/scaffold/project-scaffold.ts +43 -77
- package/src/serve/hub-app/check-view.tsx +4 -4
- package/src/serve/hub-app/check-words.ts +1 -0
- package/src/serve/hub-app/checks-svc.ts +1 -2
- package/src/serve/hub-app/home.html +0 -1
- package/src/serve/hub-app/hub-types.ts +0 -2
- package/src/serve/hub-app/index.html +0 -1
- package/src/serve/hub-app/project-list.css +1 -2
- package/src/serve/hub-app/tree-build.ts +3 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-port.ts +3 -3
- package/src/serve/hub-runtime.ts +3 -5
- package/src/serve/project-gutter.ts +5 -2
- package/src/serve/run-lanes.ts +7 -3
- package/src/serve/serve-hub.ts +1 -16
- package/src/serve/tracked-watch.ts +1 -1
- package/src/smoke/run-smoke.ts +13 -6
- package/src/start-repo.ts +15 -3
- package/src/suite/judge-suites.ts +81 -32
- package/src/suite/select.ts +2 -0
- package/src/suite/suite-cli.ts +27 -16
- package/src/suite/suite-cpu-preload.ts +7 -11
- package/src/suite/test-units.ts +7 -4
- package/src/ts/config.ts +12 -5
- package/src/ts/imports.ts +3 -1
- package/src/ts/typecheck-cache.ts +11 -0
- package/src/ts/typecheck-cli.ts +2 -1
- package/src/tui/build-steps-panel.ts +1 -1
- package/src/wiki/scan-md.ts +16 -9
- package/src/checks/SK009-commit-ai-tells.check.ts +0 -131
- package/src/checks/SK014-prose-ste.check.ts +0 -78
- package/src/cli/cmd_Prose.ts +0 -74
- package/src/prose/prose-view.ts +0 -90
- package/src/prose/run-prose.ts +0 -187
- package/src/prose/ste.ts +0 -552
- package/src/scaffold/files/vscode-tasks.json +0 -24
- package/src/serve/hub-app/prose-hits-st.ts +0 -125
- package/src/serve/hub-app/prose-hits-view.tsx +0 -176
- package/src/serve/hub-app/prose-hits.css +0 -21
package/src/cli/cmd_Prose.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// shipkit prose — the STE checker as a command, so a writer can run it on one
|
|
2
|
-
// file while drafting instead of waiting for the gate. Rule SK014 runs the
|
|
3
|
-
// same module over the repo's declared scope.
|
|
4
|
-
import { Command, Option } from 'clipanion'
|
|
5
|
-
import { loadRepoConfig } from '../repo-config.ts'
|
|
6
|
-
import { classSummary, proseLines, runProseCheck } from '../prose/run-prose.ts'
|
|
7
|
-
import { CheckCtx } from '../core/check-ctx.ts'
|
|
8
|
-
|
|
9
|
-
export class ProseCommand extends Command {
|
|
10
|
-
static override paths = [['prose']]
|
|
11
|
-
|
|
12
|
-
static override usage = Command.Usage({
|
|
13
|
-
category: 'Health',
|
|
14
|
-
description: 'STE prose check (identity/writing-voice.md): mechanical fixes, hard violations, soft warnings',
|
|
15
|
-
examples: [
|
|
16
|
-
['every file this repo declares as prose', 'sk prose'],
|
|
17
|
-
['one file while drafting', 'sk prose wiki/build/shipkit.md'],
|
|
18
|
-
['apply every mechanical rewrite', 'sk prose --fix'],
|
|
19
|
-
['the procedural 20-word limit', 'sk prose --strict'],
|
|
20
|
-
['machine-readable', 'sk prose --json'],
|
|
21
|
-
],
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
// `targets`, not `paths`: clipanion's Command owns the static `paths`
|
|
25
|
-
targets = Option.Rest()
|
|
26
|
-
fix = Option.Boolean('--fix', false, { description: 'apply the mechanical rewrites (contractions, latin, one-to-one substitutions, empty words)' })
|
|
27
|
-
json = Option.Boolean('--json', false, { description: 'machine-readable { files, byClass, hits }' })
|
|
28
|
-
strict = Option.Boolean('--strict', false, { description: 'the 20-word procedural sentence limit instead of the 25-word descriptive one' })
|
|
29
|
-
cap = Option.String('--cap', '25', { description: 'how many hit lines to print before the per-class totals' })
|
|
30
|
-
|
|
31
|
-
async execute(): Promise<number> {
|
|
32
|
-
const root = process.cwd()
|
|
33
|
-
const config = await loadRepoConfig(root)
|
|
34
|
-
const cfg = { ...(config?.prose ?? {}) }
|
|
35
|
-
if (this.strict) cfg.strict = true
|
|
36
|
-
const explicit = this.targets.length > 0
|
|
37
|
-
if ((cfg.include ?? []).length === 0 && !explicit) {
|
|
38
|
-
this.context.stdout.write(
|
|
39
|
-
`skip: this repo declares no prose scope — add \`prose: { include: ['**/*.md'], exempt: [...] }\` to repo.config.ts (identity/writing-voice.md)\n`,
|
|
40
|
-
)
|
|
41
|
-
return 0
|
|
42
|
-
}
|
|
43
|
-
const r = runProseCheck({ c: CheckCtx.open({ root, config }), config: cfg, paths: explicit ? this.targets : undefined, fix: this.fix })
|
|
44
|
-
if (this.json) {
|
|
45
|
-
// every hit carries its file: a flat list without one cannot be
|
|
46
|
-
// acted on, and the grouping is already in byFile
|
|
47
|
-
const hits = r.byFile.flatMap((f) => f.hits.map((h) => ({ file: f.file, ...h })))
|
|
48
|
-
this.context.stdout.write(`${JSON.stringify({ files: r.files, byTier: r.byTier, byClass: r.byClass, fixed: r.fixed, hits }, null, 2)}\n`)
|
|
49
|
-
return r.byTier.fix + r.byTier.hard > 0 ? 1 : 0
|
|
50
|
-
}
|
|
51
|
-
const w = (l: string) => this.context.stdout.write(`${l}\n`)
|
|
52
|
-
// a declared scope that resolves to nothing reads as `0 file(s)
|
|
53
|
-
// STE-clean`, the same green a clean repo gets, over a check that
|
|
54
|
-
// inspected no file at all (rule SK014 warns on the same fact)
|
|
55
|
-
if (!explicit && r.files === 0) {
|
|
56
|
-
w(`WARN prose scope matches no file: ${(cfg.include ?? []).join(', ')} — declared and inspecting nothing`)
|
|
57
|
-
w(' fix: fix the globs in repo.config.ts prose.include (or its exempt list), or drop the key to opt out')
|
|
58
|
-
return 0
|
|
59
|
-
}
|
|
60
|
-
if (r.fixed.length > 0) w(`ok: rewrote ${r.fixed.length} file(s)`)
|
|
61
|
-
for (const l of proseLines(r, { cap: Number(this.cap) || 25 })) w(l)
|
|
62
|
-
const blocking = r.byTier.fix + r.byTier.hard
|
|
63
|
-
const baseline = cfg.baseline
|
|
64
|
-
const overBaseline = baseline != null && r.byTier.soft > baseline
|
|
65
|
-
if (blocking === 0 && !overBaseline) {
|
|
66
|
-
w(`ok: ${r.files} file(s) STE-clean${r.byTier.soft > 0 ? `, ${r.byTier.soft} soft warning(s)${baseline != null ? ` (baseline ${baseline})` : ''}` : ''}`)
|
|
67
|
-
return 0
|
|
68
|
-
}
|
|
69
|
-
if (blocking > 0)
|
|
70
|
-
w(`FAIL: ${blocking} STE violation(s) in ${r.byFile.length} file(s) — ${classSummary(r)}${r.fixable > 0 ? `\n fix: shipkit prose --fix clears ${r.fixable} of them` : ''}`)
|
|
71
|
-
if (overBaseline) w(`FAIL: ${r.byTier.soft} soft hits, above this repo's baseline of ${baseline} — lower the count, or move the baseline down in repo.config.ts`)
|
|
72
|
-
return 1
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/prose/prose-view.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// The SK014 page's data: every hit of the prose check, grouped by the word
|
|
2
|
-
// (or the rule, for the classes that judge a shape), noisiest first, each hit
|
|
3
|
-
// with its file, its line and the sentence around it. It runs the SAME
|
|
4
|
-
// module the rule runs, over the same config, so the page and the gate can
|
|
5
|
-
// never count differently.
|
|
6
|
-
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
7
|
-
import type { RepoConfig } from '../repo-config.ts'
|
|
8
|
-
import { runProseCheck } from './run-prose.ts'
|
|
9
|
-
import type { ProseClass, ProseHit, ProseTier, ProseWords } from './ste.ts'
|
|
10
|
-
|
|
11
|
-
export type ProseViewHit = {
|
|
12
|
-
file: string
|
|
13
|
-
line: number
|
|
14
|
-
/** the source line around the hit, clipped to a readable width */
|
|
15
|
-
excerpt: string
|
|
16
|
-
/** where the hit sits inside `excerpt`, [from, to) */
|
|
17
|
-
mark: [number, number]
|
|
18
|
-
why: string
|
|
19
|
-
fix?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type ProseGroup = {
|
|
23
|
-
/** `<class>:<word>` for a word hit, `<class>` for a shape */
|
|
24
|
-
key: string
|
|
25
|
-
cls: ProseClass
|
|
26
|
-
tier: ProseTier
|
|
27
|
-
/** the dictionary word or phrase an allow entry names; null on a shape */
|
|
28
|
-
word: string | null
|
|
29
|
-
count: number
|
|
30
|
-
hits: ProseViewHit[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type ProseView = {
|
|
34
|
-
/** false when the repo declares no prose scope: the rule skips it */
|
|
35
|
-
scoped: boolean
|
|
36
|
-
files: number
|
|
37
|
-
total: number
|
|
38
|
-
byTier: Record<ProseTier, number>
|
|
39
|
-
groups: ProseGroup[]
|
|
40
|
-
/** the repo's own lists as the config loads them */
|
|
41
|
-
words: ProseWords
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** how much of a long line the page shows on each side of the hit */
|
|
45
|
-
const SIDE = 70
|
|
46
|
-
|
|
47
|
-
/** the source line a hit sits on, clipped around it, with the hit's span
|
|
48
|
-
* inside the clip. A hit that runs past its line (a long sentence, a
|
|
49
|
-
* paragraph) marks to the end of the line */
|
|
50
|
-
export function excerptOf(text: string, h: Pick<ProseHit, 'start' | 'end'>): { excerpt: string; mark: [number, number] } {
|
|
51
|
-
const from = text.lastIndexOf('\n', h.start - 1) + 1
|
|
52
|
-
const nl = text.indexOf('\n', h.start)
|
|
53
|
-
const to = nl < 0 ? text.length : nl
|
|
54
|
-
const line = text.slice(from, to)
|
|
55
|
-
let a = h.start - from
|
|
56
|
-
let b = Math.min(h.end, to) - from
|
|
57
|
-
let lo = Math.max(0, a - SIDE)
|
|
58
|
-
const hi = Math.min(line.length, Math.max(b, a) + SIDE)
|
|
59
|
-
// a leading indent is noise in a one line quote
|
|
60
|
-
while (lo < a && /\s/.test(line[lo]!)) lo++
|
|
61
|
-
const head = lo > 0 ? '…' : ''
|
|
62
|
-
const tail = hi < line.length ? '…' : ''
|
|
63
|
-
const excerpt = head + line.slice(lo, hi).trimEnd() + tail
|
|
64
|
-
a = a - lo + head.length
|
|
65
|
-
b = Math.min(b - lo + head.length, excerpt.length - tail.length)
|
|
66
|
-
return { excerpt, mark: [a, Math.max(a, b)] }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function proseView(c: Pick<CheckCtx, 'root' | 'read' | 'list' | 'write'>, config: RepoConfig): ProseView {
|
|
70
|
-
const cfg = config.prose
|
|
71
|
-
const empty: ProseView = { scoped: false, files: 0, total: 0, byTier: { fix: 0, hard: 0, soft: 0 }, groups: [], words: cfg?.words ?? {} }
|
|
72
|
-
if (cfg == null || (cfg.include ?? []).length === 0) return empty
|
|
73
|
-
const r = runProseCheck({ c, config: cfg, keepText: true })
|
|
74
|
-
const groups = new Map<string, ProseGroup>()
|
|
75
|
-
for (const f of r.byFile)
|
|
76
|
-
for (const h of f.hits) {
|
|
77
|
-
const key = h.word == null ? h.cls : `${h.cls}:${h.word}`
|
|
78
|
-
let g = groups.get(key)
|
|
79
|
-
if (g == null) {
|
|
80
|
-
g = { key, cls: h.cls, tier: h.tier, word: h.word ?? null, count: 0, hits: [] }
|
|
81
|
-
groups.set(key, g)
|
|
82
|
-
}
|
|
83
|
-
g.count++
|
|
84
|
-
const ex = excerptOf(f.text ?? '', h)
|
|
85
|
-
g.hits.push({ file: f.file, line: h.line, ...ex, why: h.why, ...(h.fix == null ? {} : { fix: h.fix }) })
|
|
86
|
-
}
|
|
87
|
-
// the noisiest first: one allow entry on the top word clears the most
|
|
88
|
-
const sorted = [...groups.values()].sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))
|
|
89
|
-
return { scoped: true, files: r.files, total: r.hits.length, byTier: r.byTier, groups: sorted, words: cfg.words ?? {} }
|
|
90
|
-
}
|
package/src/prose/run-prose.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
// `shipkit prose` — the file-walking half of the STE checker. The judgement
|
|
2
|
-
// lives in ste.ts; this module resolves WHICH files a repo puts in scope,
|
|
3
|
-
// prints the wire lines, and applies the mechanical fixes.
|
|
4
|
-
//
|
|
5
|
-
// Scope is repo knowledge, never a hardcoded path list: a repo declares
|
|
6
|
-
// `prose: { include, exempt }` in repo.config.ts. A repo that declares
|
|
7
|
-
// nothing is not checked, and the rule says so instead of failing a repo
|
|
8
|
-
// that never opted in.
|
|
9
|
-
//
|
|
10
|
-
// It reads and writes ONLY through the check context it is handed, so rule
|
|
11
|
-
// SK014 is cached on the scope's listings and the files in it.
|
|
12
|
-
import { isAbsolute, relative, resolve } from 'path'
|
|
13
|
-
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
14
|
-
import { checkProse, fixProse, tierOf, type ProseClass, type ProseHit, type ProseTier, type ProseWords } from './ste.ts'
|
|
15
|
-
|
|
16
|
-
/** what the walker reads and writes through: a check context */
|
|
17
|
-
export type ProseDisk = Pick<CheckCtx, 'root' | 'read' | 'list' | 'write'>
|
|
18
|
-
|
|
19
|
-
export type ProseConfig = {
|
|
20
|
-
/** globs (repo-relative) that ARE house prose. Absent = the repo is not checked. */
|
|
21
|
-
include?: string[]
|
|
22
|
-
/** globs never checked: journals, archives, upstream snapshots, generated files */
|
|
23
|
-
exempt?: string[]
|
|
24
|
-
/** classes this repo does not check, with the reason living in repo.config.ts */
|
|
25
|
-
allow?: ProseClass[]
|
|
26
|
-
/** the 20-word procedural limit instead of the 25-word descriptive one */
|
|
27
|
-
strict?: boolean
|
|
28
|
-
/** this repo's own word lists over the house dictionary: `allow` drops
|
|
29
|
-
* every hit on a word or phrase, `ban` refuses an extra one (a hard hit,
|
|
30
|
-
* with its replacement). They bind this repo only, and the hub's SK014
|
|
31
|
-
* page writes them through a managed block inside this object */
|
|
32
|
-
words?: ProseWords
|
|
33
|
-
/** soft-tier ratchet: the count this repo is allowed to carry. The gate
|
|
34
|
-
* fails ABOVE it, so the number can only go down. Absent is the default,
|
|
35
|
-
* and it means soft hits warn in `shipkit prose` and never reach a gate
|
|
36
|
-
* or an attention surface. A four-digit count nobody can act on is worse
|
|
37
|
-
* than no number at all. */
|
|
38
|
-
baseline?: number
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type FileReport = {
|
|
42
|
-
file: string
|
|
43
|
-
hits: ProseHit[]
|
|
44
|
-
/** the file as it was judged, kept only when the caller asked, so a page
|
|
45
|
-
* can quote the line around each hit without reading the file twice */
|
|
46
|
-
text?: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type ProseReport = {
|
|
50
|
-
files: number
|
|
51
|
-
hits: ProseHit[]
|
|
52
|
-
byFile: FileReport[]
|
|
53
|
-
byTier: Record<ProseTier, number>
|
|
54
|
-
byClass: Record<string, number>
|
|
55
|
-
fixable: number
|
|
56
|
-
/** files rewritten by a --fix run */
|
|
57
|
-
fixed: string[]
|
|
58
|
-
scoped: boolean // did this repo declare any include globs?
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const DEFAULT_EXEMPT = ['**/node_modules/**', '**/CHANGELOG.md', '**/dist/**', '**/.git/**']
|
|
62
|
-
|
|
63
|
-
/** a generated file is not house prose, and rewriting it loses the change on
|
|
64
|
-
* the next render */
|
|
65
|
-
const isGenerated = (text: string): boolean => text.slice(0, 400).includes('<!-- generated')
|
|
66
|
-
|
|
67
|
-
/** an explicitly named path, resolved against the repo and REFUSED when it
|
|
68
|
-
* lands outside: `--fix` writes, and a checker that rewrites files outside
|
|
69
|
-
* the repo it was pointed at is a much worse tool than one that says no. */
|
|
70
|
-
export function explicitPath(root: string, p: string): string {
|
|
71
|
-
const abs = isAbsolute(p) ? p : resolve(root, p)
|
|
72
|
-
const rel = relative(root, abs)
|
|
73
|
-
if (rel === '' || rel.startsWith('..')) throw new Error(`FAIL ${p} is outside ${root} — shipkit prose only reads and writes inside the repo`)
|
|
74
|
-
return rel
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** does `path` match `pattern` under the dot rule: a hidden segment (one
|
|
78
|
-
* starting with a dot) only where the pattern names a hidden segment itself,
|
|
79
|
-
* never inside a `**` or under a wildcard segment */
|
|
80
|
-
export function hiddenAllowed(pattern: string, path: string): boolean {
|
|
81
|
-
const pat = pattern.split('/')
|
|
82
|
-
const segs = path.split('/')
|
|
83
|
-
const hidden = (s: string): boolean => s.startsWith('.') && s !== '.' && s !== '..'
|
|
84
|
-
const star = pat.indexOf('**')
|
|
85
|
-
const head = star === -1 ? pat : pat.slice(0, star)
|
|
86
|
-
const tail = star === -1 ? [] : pat.slice(pat.lastIndexOf('**') + 1)
|
|
87
|
-
for (let i = 0; i < segs.length; i++) {
|
|
88
|
-
if (!hidden(segs[i]!)) continue
|
|
89
|
-
const fromEnd = segs.length - 1 - i
|
|
90
|
-
const p = i < head.length ? head[i] : fromEnd < tail.length ? tail[tail.length - 1 - fromEnd] : undefined
|
|
91
|
-
if (p == null || !p.startsWith('.')) return false
|
|
92
|
-
}
|
|
93
|
-
return true
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** the files in scope: what git sees (tracked, or untracked and not ignored)
|
|
97
|
-
* matching an include glob, the dot rule applied, minus every exempt glob */
|
|
98
|
-
export function proseFiles(d: Pick<ProseDisk, 'list'>, cfg: ProseConfig): string[] {
|
|
99
|
-
const include = cfg.include ?? []
|
|
100
|
-
if (include.length === 0) return []
|
|
101
|
-
const exempt = [...DEFAULT_EXEMPT, ...(cfg.exempt ?? [])].map((g) => new Bun.Glob(g))
|
|
102
|
-
const seen = new Set<string>()
|
|
103
|
-
for (const pattern of include)
|
|
104
|
-
for (const rel of d.list(pattern)) {
|
|
105
|
-
if (!hiddenAllowed(pattern, rel)) continue
|
|
106
|
-
if (exempt.some((g) => g.match(rel))) continue
|
|
107
|
-
seen.add(rel)
|
|
108
|
-
}
|
|
109
|
-
return [...seen].sort()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function runProseCheck(o: { c: ProseDisk; config: ProseConfig; paths?: string[]; fix?: boolean; keepText?: boolean }): ProseReport {
|
|
113
|
-
const cfg = o.config
|
|
114
|
-
const d = o.c
|
|
115
|
-
const files = o.paths != null && o.paths.length > 0 ? o.paths.map((p) => explicitPath(d.root, p)) : proseFiles(d, cfg)
|
|
116
|
-
const explicit = o.paths != null && o.paths.length > 0
|
|
117
|
-
const opts = { allow: cfg.allow, strict: cfg.strict, words: cfg.words }
|
|
118
|
-
const byFile: FileReport[] = []
|
|
119
|
-
const hits: ProseHit[] = []
|
|
120
|
-
const fixed: string[] = []
|
|
121
|
-
for (const rel of files) {
|
|
122
|
-
let text: string | null
|
|
123
|
-
try {
|
|
124
|
-
text = d.read(rel)
|
|
125
|
-
} catch (e) {
|
|
126
|
-
text = null
|
|
127
|
-
if (explicit) throw new Error(`FAIL cannot read ${rel} — ${String(e)}`)
|
|
128
|
-
}
|
|
129
|
-
if (text == null) {
|
|
130
|
-
// a path a HUMAN typed that cannot be read is a loud failure. A
|
|
131
|
-
// path the listing produced can vanish between the scan and the
|
|
132
|
-
// read, and skipping that one is right.
|
|
133
|
-
if (explicit) throw new Error(`FAIL cannot read ${rel} — no such file`)
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
if (isGenerated(text)) continue
|
|
137
|
-
if (o.fix === true) {
|
|
138
|
-
const out = fixProse(text, opts)
|
|
139
|
-
if (out.applied > 0) {
|
|
140
|
-
d.write(rel, out.text)
|
|
141
|
-
fixed.push(rel)
|
|
142
|
-
text = out.text
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const fh = checkProse(text, opts)
|
|
146
|
-
if (fh.length > 0) byFile.push({ file: rel, hits: fh, ...(o.keepText === true ? { text } : {}) })
|
|
147
|
-
hits.push(...fh)
|
|
148
|
-
}
|
|
149
|
-
const byTier: Record<ProseTier, number> = { fix: 0, hard: 0, soft: 0 }
|
|
150
|
-
const byClass: Record<string, number> = {}
|
|
151
|
-
for (const h of hits) {
|
|
152
|
-
byTier[h.tier]++
|
|
153
|
-
byClass[h.cls] = (byClass[h.cls] ?? 0) + 1
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
files: files.length,
|
|
157
|
-
hits,
|
|
158
|
-
byFile,
|
|
159
|
-
byTier,
|
|
160
|
-
byClass,
|
|
161
|
-
fixable: hits.filter((h) => h.fix != null).length,
|
|
162
|
-
fixed,
|
|
163
|
-
scoped: (cfg.include ?? []).length > 0,
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** one line per hit, capped: a first STE pass on a real repo finds hundreds,
|
|
168
|
-
* and a wall of them hides the mechanical ones that `--fix` clears in a
|
|
169
|
-
* second. The per-class totals below the cap keep the report complete. */
|
|
170
|
-
export function proseLines(r: ProseReport, o: { cap?: number; tiers?: ProseTier[] } = {}): string[] {
|
|
171
|
-
const cap = o.cap ?? 25
|
|
172
|
-
const tiers = new Set(o.tiers ?? (['fix', 'hard', 'soft'] as ProseTier[]))
|
|
173
|
-
const shown = r.hits.filter((h) => tiers.has(h.tier))
|
|
174
|
-
const out: string[] = []
|
|
175
|
-
const fileOf = new Map<ProseHit, string>()
|
|
176
|
-
for (const f of r.byFile) for (const h of f.hits) fileOf.set(h, f.file)
|
|
177
|
-
for (const h of shown.slice(0, cap))
|
|
178
|
-
out.push(`${fileOf.get(h) ?? '?'}:${h.line} [${h.cls}] ${JSON.stringify(h.text.trim())} — ${h.why}${h.fix != null ? ` (--fix writes ${JSON.stringify(h.fix.trim())})` : ''}`)
|
|
179
|
-
if (shown.length > cap) out.push(`… and ${shown.length - cap} more`)
|
|
180
|
-
return out
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export const classSummary = (r: ProseReport): string =>
|
|
184
|
-
Object.entries(r.byClass)
|
|
185
|
-
.sort((a, b) => b[1] - a[1])
|
|
186
|
-
.map(([c, n]) => `${c} ${n}${tierOf(c as ProseClass) === 'soft' ? '' : '!'}`)
|
|
187
|
-
.join(', ')
|