rvlib-shipkit 0.38.1 → 1.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 +367 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +209 -15
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// file:line findings out of what a check printed, one function per format the
|
|
2
|
+
// floor meets: tsc, oxlint (both its piped and its graphical output), eslint's
|
|
3
|
+
// stylish output, a failing bun test, and a shipkit wire line naming a file.
|
|
4
|
+
// Every one feeds the one Problem shape (problem-line.ts).
|
|
5
|
+
//
|
|
6
|
+
// a finding the source cannot anchor in a file is dropped rather than pinned
|
|
7
|
+
// to a guess: the pane is for things an edit can fix, and the check's own
|
|
8
|
+
// page still carries every line it printed.
|
|
9
|
+
import { existsSync } from 'fs'
|
|
10
|
+
import { isAbsolute, relative, resolve } from 'path'
|
|
11
|
+
import type { Line } from '../core/wire.ts'
|
|
12
|
+
import type { Problem, Severity } from './problem-line.ts'
|
|
13
|
+
|
|
14
|
+
export type ExtractCtx = {
|
|
15
|
+
/** the repo root every file is made relative to */
|
|
16
|
+
root: string
|
|
17
|
+
/** where the tool ran, for the relative paths it printed (default: root) */
|
|
18
|
+
cwd?: string
|
|
19
|
+
/** the check id each finding is filed under */
|
|
20
|
+
check: string
|
|
21
|
+
/** is this file there? Injected so a fixture needs no tree on disk */
|
|
22
|
+
exists?: (abs: string) => boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stripAnsi = (s: string): string => s.replace(/\x1b\[[0-9;]*m/g, '').replace(/\r/g, '')
|
|
26
|
+
|
|
27
|
+
/** repo-relative, forward slashes. null when the path is not a file this
|
|
28
|
+
* repo has, which is what keeps a url or a `host:port` out of the pane */
|
|
29
|
+
function repoFile(ctx: ExtractCtx, raw: string, mustExist: boolean): string | null {
|
|
30
|
+
const p = raw.trim().replace(/^file:\/\//, '')
|
|
31
|
+
if (p === '') return null
|
|
32
|
+
const abs = isAbsolute(p) ? p : resolve(ctx.cwd ?? ctx.root, p)
|
|
33
|
+
const rel = relative(ctx.root, abs).replace(/\\/g, '/')
|
|
34
|
+
if (rel === '' || rel.startsWith('../') || isAbsolute(rel)) return null
|
|
35
|
+
if (mustExist && !(ctx.exists ?? existsSync)(abs)) return null
|
|
36
|
+
return rel
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TSC = /^(.+?)\((\d+),(\d+)\): (error|warning) (TS\d+): (.*)$/
|
|
40
|
+
|
|
41
|
+
/** `src/a.ts(3,7): error TS2322: msg`, and the indented elaboration lines
|
|
42
|
+
* under it, joined into the one message */
|
|
43
|
+
export function fromTsc(text: string, ctx: ExtractCtx): Problem[] {
|
|
44
|
+
const out: Problem[] = []
|
|
45
|
+
let last: Problem | null = null
|
|
46
|
+
let depth = 0
|
|
47
|
+
const indent = (s: string): number => /^\s*/.exec(s)![0].length
|
|
48
|
+
for (const raw of stripAnsi(text).split('\n')) {
|
|
49
|
+
const m = TSC.exec(raw)
|
|
50
|
+
if (m != null) {
|
|
51
|
+
const file = repoFile(ctx, m[1]!, false)
|
|
52
|
+
last = file == null ? null : { file, line: Number(m[2]), col: Number(m[3]), severity: m[4] as Severity, check: ctx.check, message: `${m[5]} ${m[6]!.trim()}` }
|
|
53
|
+
depth = indent(raw)
|
|
54
|
+
if (last != null) out.push(last)
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
// an elaboration sits deeper than the diagnostic it belongs to
|
|
58
|
+
if (last != null && raw.trim() !== '' && indent(raw) > depth) last.message += ` ${raw.trim()}`
|
|
59
|
+
else last = null
|
|
60
|
+
}
|
|
61
|
+
return out
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const OXLINT_PIPED = /^(.+?):(\d+):(\d+): (error|warning) (\S+?): (.*)$/
|
|
65
|
+
const OXLINT_HEAD = /^\s*(x|!|×|⚠)\s+(.+)$/
|
|
66
|
+
const OXLINT_AT = /^\s*(?:,-|╭─)\[(.+):(\d+):(\d+)\]\s*$/
|
|
67
|
+
|
|
68
|
+
/** oxlint piped (`src/a.ts:1:7: warning eslint(no-unused-vars): msg`, what
|
|
69
|
+
* the gate sees) and graphical (a `x rule: msg` head, then `,-[file:l:c]`) */
|
|
70
|
+
export function fromOxlint(text: string, ctx: ExtractCtx): Problem[] {
|
|
71
|
+
const out: Problem[] = []
|
|
72
|
+
let head: { severity: Severity; message: string } | null = null
|
|
73
|
+
for (const raw of stripAnsi(text).split('\n')) {
|
|
74
|
+
const piped = OXLINT_PIPED.exec(raw)
|
|
75
|
+
if (piped != null && /\(|\//.test(piped[5]!)) {
|
|
76
|
+
const file = repoFile(ctx, piped[1]!, false)
|
|
77
|
+
if (file != null) out.push({ file, line: Number(piped[2]), col: Number(piped[3]), severity: piped[4] as Severity, check: ctx.check, message: `${piped[5]} ${piped[6]!.replace(/\s+help: .*$/, '').trim()}` })
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
const h = OXLINT_HEAD.exec(raw)
|
|
81
|
+
if (h != null) { head = { severity: h[1] === 'x' || h[1] === '×' ? 'error' : 'warning', message: h[2]!.trim() }; continue }
|
|
82
|
+
const at = OXLINT_AT.exec(raw)
|
|
83
|
+
if (at != null && head != null) {
|
|
84
|
+
const file = repoFile(ctx, at[1]!, false)
|
|
85
|
+
if (file != null) out.push({ file, line: Number(at[2]), col: Number(at[3]), severity: head.severity, check: ctx.check, message: head.message })
|
|
86
|
+
head = null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const ESLINT_HEAD = /^(?:\/|[A-Za-z]:\\)[^:()]*\.\w+\s*$/
|
|
93
|
+
const ESLINT_ROW = /^\s+(\d+):(\d+)\s+(error|warning)\s+(.*?)(?:\s{2,}(\S+))?\s*$/
|
|
94
|
+
|
|
95
|
+
/** eslint stylish: a file path alone on its line, then ` 2:7 error msg rule` */
|
|
96
|
+
export function fromEslint(text: string, ctx: ExtractCtx): Problem[] {
|
|
97
|
+
const out: Problem[] = []
|
|
98
|
+
let file: string | null = null
|
|
99
|
+
for (const raw of stripAnsi(text).split('\n')) {
|
|
100
|
+
if (raw.trim() === '') { file = null; continue }
|
|
101
|
+
// stylish always prints the header as an absolute path, alone
|
|
102
|
+
if (!/^\s/.test(raw)) { file = ESLINT_HEAD.test(raw) ? repoFile(ctx, raw, false) : null; continue }
|
|
103
|
+
const m = ESLINT_ROW.exec(raw)
|
|
104
|
+
if (m == null || file == null) continue
|
|
105
|
+
const rule = m[5] == null ? '' : `${m[5]} `
|
|
106
|
+
out.push({ file, line: Number(m[1]), col: Number(m[2]), severity: m[3] as Severity, check: ctx.check, message: `${rule}${m[4]!.trim()}` })
|
|
107
|
+
}
|
|
108
|
+
return out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const BUN_SUITE = /^(\S.*\.(?:test|spec)\.[cm]?[jt]sx?):$/
|
|
112
|
+
const BUN_ERROR = /^(?:error|\w*Error): (.*)$/
|
|
113
|
+
const BUN_FRAME = /^\s+at .*?\(?((?:\/|[A-Za-z]:\\|\.{0,2}\/?[\w@.-]+\/)[^()]*?):(\d+):(\d+)\)?\s*$/
|
|
114
|
+
const BUN_FAIL = /^\(fail\) (.+?)(?: \[[\d.]+m?s\])?$/
|
|
115
|
+
|
|
116
|
+
/** a failing bun test: the `error:` line, the first stack frame inside this
|
|
117
|
+
* repo and outside node_modules, and the `(fail) name` line that closes it.
|
|
118
|
+
* With no frame in the repo it anchors at line 1 of the suite file */
|
|
119
|
+
export function fromBunTest(text: string, ctx: ExtractCtx): Problem[] {
|
|
120
|
+
const out: Problem[] = []
|
|
121
|
+
let suite: string | null = null
|
|
122
|
+
let error: string | null = null
|
|
123
|
+
let at: { file: string; line: number; col: number } | null = null
|
|
124
|
+
for (const raw of stripAnsi(text).split('\n')) {
|
|
125
|
+
const s = BUN_SUITE.exec(raw)
|
|
126
|
+
if (s != null) { suite = repoFile(ctx, s[1]!, false); error = null; at = null; continue }
|
|
127
|
+
const e = BUN_ERROR.exec(raw)
|
|
128
|
+
if (e != null) { error = e[1]!.trim(); at = null; continue }
|
|
129
|
+
const f = BUN_FRAME.exec(raw)
|
|
130
|
+
if (f != null && error != null && at == null && !f[1]!.includes('node_modules')) {
|
|
131
|
+
const file = repoFile(ctx, f[1]!, false)
|
|
132
|
+
if (file != null) at = { file, line: Number(f[2]), col: Number(f[3]) }
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
const fail = BUN_FAIL.exec(raw)
|
|
136
|
+
if (fail != null) {
|
|
137
|
+
const where = at ?? (suite == null ? null : { file: suite, line: 1, col: 1 })
|
|
138
|
+
if (where != null) out.push({ ...where, severity: 'error', check: ctx.check, message: `${fail[1]!.trim()}${error == null ? '' : `: ${error}`}` })
|
|
139
|
+
error = null
|
|
140
|
+
at = null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const WIRE_REF = /((?:[\w@.~-]+\/)*[\w@.~-]+\.[A-Za-z0-9]+):(\d+)(?::(\d+))?/g
|
|
147
|
+
|
|
148
|
+
/** a FAIL or WARN wire line naming `path:line` or `path:line:col`. The path
|
|
149
|
+
* must be a file this repo has: a url and a `host:port` look the same */
|
|
150
|
+
export function fromWire(lines: Line[], ctx: ExtractCtx): Problem[] {
|
|
151
|
+
const out: Problem[] = []
|
|
152
|
+
for (const l of lines) {
|
|
153
|
+
if (l.kind !== 'fail' && l.kind !== 'warn') continue
|
|
154
|
+
const severity: Severity = l.kind === 'fail' ? 'error' : 'warning'
|
|
155
|
+
const message = l.text.replace(/^\[[^\]]+\]\s*/, '').trim()
|
|
156
|
+
for (const m of l.text.matchAll(WIRE_REF)) {
|
|
157
|
+
const before = l.text.slice(0, m.index)
|
|
158
|
+
if (/[a-z]+:\/\/\S*$/i.test(before)) continue
|
|
159
|
+
const file = repoFile(ctx, m[1]!, true)
|
|
160
|
+
if (file == null) continue
|
|
161
|
+
out.push({ file, line: Number(m[2]), col: m[3] == null ? 1 : Number(m[3]), severity, check: ctx.check, message })
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** every format, over what one check printed: its wire lines and, for a
|
|
168
|
+
* subprocess, its whole raw output. One finding is reported once */
|
|
169
|
+
export function extractProblems(src: { lines: Line[]; output?: string }, ctx: ExtractCtx): Problem[] {
|
|
170
|
+
const text = src.output ?? ''
|
|
171
|
+
const all = [
|
|
172
|
+
...fromWire(src.lines, ctx),
|
|
173
|
+
...(text === '' ? [] : [...fromTsc(text, ctx), ...fromOxlint(text, ctx), ...fromEslint(text, ctx), ...fromBunTest(text, ctx)]),
|
|
174
|
+
]
|
|
175
|
+
const seen = new Set<string>()
|
|
176
|
+
return all.filter((p) => {
|
|
177
|
+
const k = `${p.file}:${p.line}:${p.col}:${p.severity}:${p.message}`
|
|
178
|
+
if (seen.has(k)) return false
|
|
179
|
+
seen.add(k)
|
|
180
|
+
return true
|
|
181
|
+
})
|
|
182
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// the latest verdict of every check, as the problems pane shows it. A run that
|
|
2
|
+
// asked about three checks replaces those three and leaves the rest standing:
|
|
3
|
+
// vscode clears the whole pane at every frame, so a narrowed run that printed
|
|
4
|
+
// only its own findings would erase every other check's.
|
|
5
|
+
import { frameLines, type Problem } from './problem-line.ts'
|
|
6
|
+
|
|
7
|
+
export class ProblemBoard {
|
|
8
|
+
private readonly byCheck = new Map<string, Problem[]>()
|
|
9
|
+
|
|
10
|
+
/** a check reached a verdict. What it carries has already passed the pane
|
|
11
|
+
* rule (problem-line.ts feedsProblemsPane), so an empty list clears it */
|
|
12
|
+
set(check: string, problems: Problem[]): void {
|
|
13
|
+
if (problems.length === 0) this.byCheck.delete(check)
|
|
14
|
+
else this.byCheck.set(check, problems)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** every finding standing, sorted by file then line, so a frame reads the
|
|
18
|
+
* same whatever order the checks finished in */
|
|
19
|
+
all(): Problem[] {
|
|
20
|
+
return [...this.byCheck.values()].flat().sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.col - b.col || a.check.localeCompare(b.check))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** one check's verdict, as the runner reports it. A skip is no verdict
|
|
24
|
+
* and what the check last said stands, except a check turned off, which
|
|
25
|
+
* has nothing to say anymore. Returns whether the board moved */
|
|
26
|
+
take(e: { id: string; status: string; note?: string; problems?: Problem[] }): boolean {
|
|
27
|
+
if (e.status === 'skip' && e.note !== 'off') return false
|
|
28
|
+
const next = e.status === 'skip' ? [] : e.problems ?? []
|
|
29
|
+
const before = JSON.stringify(this.byCheck.get(e.id) ?? [])
|
|
30
|
+
this.set(e.id, next)
|
|
31
|
+
return JSON.stringify(next) !== before
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
frame(): string[] {
|
|
35
|
+
return frameLines(this.all())
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// the ONE shape a problem takes on the hub terminal, and the vscode problem
|
|
2
|
+
// matcher that reads it back. The printer and every tasks.json writer import
|
|
3
|
+
// this file, so the line the hub prints and the regex the editor matches can
|
|
4
|
+
// never drift apart.
|
|
5
|
+
//
|
|
6
|
+
// a run prints one FRAME: the begin line, one problem line per finding, the
|
|
7
|
+
// end line. The matcher is a background one, so vscode clears the pane at the
|
|
8
|
+
// begin line and refills it from what sits before the end line.
|
|
9
|
+
|
|
10
|
+
export type Severity = 'error' | 'warning'
|
|
11
|
+
|
|
12
|
+
/** one finding, anchored in a file of this repo */
|
|
13
|
+
export type Problem = {
|
|
14
|
+
/** repo-relative, forward slashes */
|
|
15
|
+
file: string
|
|
16
|
+
line: number
|
|
17
|
+
/** 1 when the source names no column */
|
|
18
|
+
col: number
|
|
19
|
+
severity: Severity
|
|
20
|
+
/** the check that produced it, printed where vscode shows the code */
|
|
21
|
+
check: string
|
|
22
|
+
/** one line, no newline */
|
|
23
|
+
message: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const FRAME_BEGIN = 'shipkit problems: begin'
|
|
27
|
+
const FRAME_END_HEAD = 'shipkit problems: end'
|
|
28
|
+
|
|
29
|
+
export function frameEnd(problems: Problem[]): string {
|
|
30
|
+
const errors = problems.filter((p) => p.severity === 'error').length
|
|
31
|
+
return `${FRAME_END_HEAD}, ${errors} error(s), ${problems.length - errors} warning(s)`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const BEGINS_PATTERN = `^${FRAME_BEGIN}$`
|
|
35
|
+
export const ENDS_PATTERN = `^${FRAME_END_HEAD}\\b`
|
|
36
|
+
|
|
37
|
+
/** file, line, column, severity, check id, message. The file is lazy and the
|
|
38
|
+
* check id cannot hold a space, so a path with spaces and an id with a colon
|
|
39
|
+
* (`imports:check`) both split right */
|
|
40
|
+
export const PROBLEM_PATTERN = '^(.+?):(\\d+):(\\d+): (error|warning) (\\S+?): (.*)$'
|
|
41
|
+
|
|
42
|
+
export function formatProblem(p: Problem): string {
|
|
43
|
+
const message = p.message.replace(/\s*\n\s*/g, ' ').trim()
|
|
44
|
+
return `${p.file}:${p.line}:${p.col}: ${p.severity} ${p.check}: ${message}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** the frame, ready to print line by line */
|
|
48
|
+
export function frameLines(problems: Problem[]): string[] {
|
|
49
|
+
return [FRAME_BEGIN, ...problems.map(formatProblem), frameEnd(problems)]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** what a tasks.json hub task carries, byte for byte the same everywhere */
|
|
53
|
+
export function hubProblemMatcher(): Record<string, unknown> {
|
|
54
|
+
return {
|
|
55
|
+
owner: 'shipkit',
|
|
56
|
+
source: 'shipkit',
|
|
57
|
+
fileLocation: ['relative', '${workspaceFolder}'],
|
|
58
|
+
pattern: { regexp: PROBLEM_PATTERN, file: 1, line: 2, column: 3, severity: 4, code: 5, message: 6 },
|
|
59
|
+
background: { activeBegin: false, beginsPattern: BEGINS_PATTERN, endsPattern: ENDS_PATTERN },
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** how a check is remembered, read from what it declares */
|
|
64
|
+
export type Population = 'content' | 'world' | 'reads' | 'neither'
|
|
65
|
+
|
|
66
|
+
export function populationOf(d: { inputs?: string[]; ttl?: string; never?: string; reads?: boolean }): Population {
|
|
67
|
+
if (d.ttl != null && d.ttl !== '') return 'world'
|
|
68
|
+
if (d.inputs != null) return 'content'
|
|
69
|
+
if (d.reads === true && (d.never == null || d.never === '')) return 'reads'
|
|
70
|
+
return 'neither'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** THE rule for the pane: the latest verdict of every check feeds it, measured
|
|
74
|
+
* or replayed, except a world check. A world answer (a cred, a domain, a
|
|
75
|
+
* service) is not a problem in the code, and it needs hands rather than an
|
|
76
|
+
* edit, so it stays on the needs surface */
|
|
77
|
+
export const feedsProblemsPane = (p: Population): boolean => p !== 'world'
|
|
@@ -0,0 +1,90 @@
|
|
|
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
CHANGED
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
// `prose: { include, exempt }` in repo.config.ts. A repo that declares
|
|
7
7
|
// nothing is not checked, and the rule says so instead of failing a repo
|
|
8
8
|
// that never opted in.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
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'>
|
|
13
18
|
|
|
14
19
|
export type ProseConfig = {
|
|
15
20
|
/** globs (repo-relative) that ARE house prose. Absent = the repo is not checked. */
|
|
@@ -20,6 +25,11 @@ export type ProseConfig = {
|
|
|
20
25
|
allow?: ProseClass[]
|
|
21
26
|
/** the 20-word procedural limit instead of the 25-word descriptive one */
|
|
22
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
|
|
23
33
|
/** soft-tier ratchet: the count this repo is allowed to carry. The gate
|
|
24
34
|
* fails ABOVE it, so the number can only go down. Absent is the default,
|
|
25
35
|
* and it means soft hits warn in `shipkit prose` and never reach a gate
|
|
@@ -28,7 +38,13 @@ export type ProseConfig = {
|
|
|
28
38
|
baseline?: number
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
export type FileReport = {
|
|
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
|
+
}
|
|
32
48
|
|
|
33
49
|
export type ProseReport = {
|
|
34
50
|
files: number
|
|
@@ -58,51 +74,76 @@ export function explicitPath(root: string, p: string): string {
|
|
|
58
74
|
return rel
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
|
|
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[] {
|
|
62
99
|
const include = cfg.include ?? []
|
|
63
100
|
if (include.length === 0) return []
|
|
64
101
|
const exempt = [...DEFAULT_EXEMPT, ...(cfg.exempt ?? [])].map((g) => new Bun.Glob(g))
|
|
65
102
|
const seen = new Set<string>()
|
|
66
103
|
for (const pattern of include)
|
|
67
|
-
for (const
|
|
68
|
-
|
|
104
|
+
for (const rel of d.list(pattern)) {
|
|
105
|
+
if (!hiddenAllowed(pattern, rel)) continue
|
|
69
106
|
if (exempt.some((g) => g.match(rel))) continue
|
|
70
107
|
seen.add(rel)
|
|
71
108
|
}
|
|
72
109
|
return [...seen].sort()
|
|
73
110
|
}
|
|
74
111
|
|
|
75
|
-
export function runProseCheck(o: {
|
|
112
|
+
export function runProseCheck(o: { c: ProseDisk; config: ProseConfig; paths?: string[]; fix?: boolean; keepText?: boolean }): ProseReport {
|
|
76
113
|
const cfg = o.config
|
|
77
|
-
const
|
|
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)
|
|
78
116
|
const explicit = o.paths != null && o.paths.length > 0
|
|
79
|
-
const opts = { allow: cfg.allow, strict: cfg.strict }
|
|
117
|
+
const opts = { allow: cfg.allow, strict: cfg.strict, words: cfg.words }
|
|
80
118
|
const byFile: FileReport[] = []
|
|
81
119
|
const hits: ProseHit[] = []
|
|
82
120
|
const fixed: string[] = []
|
|
83
121
|
for (const rel of files) {
|
|
84
|
-
|
|
85
|
-
let text: string
|
|
122
|
+
let text: string | null
|
|
86
123
|
try {
|
|
87
|
-
text =
|
|
124
|
+
text = d.read(rel)
|
|
88
125
|
} catch (e) {
|
|
89
|
-
|
|
90
|
-
// path the glob produced can vanish between the scan and the read,
|
|
91
|
-
// and skipping that one is right.
|
|
126
|
+
text = null
|
|
92
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`)
|
|
93
134
|
continue
|
|
94
135
|
}
|
|
95
136
|
if (isGenerated(text)) continue
|
|
96
137
|
if (o.fix === true) {
|
|
97
138
|
const out = fixProse(text, opts)
|
|
98
139
|
if (out.applied > 0) {
|
|
99
|
-
|
|
140
|
+
d.write(rel, out.text)
|
|
100
141
|
fixed.push(rel)
|
|
101
142
|
text = out.text
|
|
102
143
|
}
|
|
103
144
|
}
|
|
104
145
|
const fh = checkProse(text, opts)
|
|
105
|
-
if (fh.length > 0) byFile.push({ file: rel, hits: fh })
|
|
146
|
+
if (fh.length > 0) byFile.push({ file: rel, hits: fh, ...(o.keepText === true ? { text } : {}) })
|
|
106
147
|
hits.push(...fh)
|
|
107
148
|
}
|
|
108
149
|
const byTier: Record<ProseTier, number> = { fix: 0, hard: 0, soft: 0 }
|