@quickchatai/quickreview 1.2.0 → 1.3.2
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/README.md +116 -53
- package/bin/qcr.js +1 -1
- package/config.example.toml +25 -7
- package/deploy/ansible/README.md +20 -12
- package/deploy/ansible/inventory.example.ini +1 -1
- package/deploy/collect-secrets.sh +1 -1
- package/deploy/qcr-update.sh +1 -1
- package/dist/extensions/cli/home.js +14 -0
- package/dist/extensions/cli/home.js.map +1 -0
- package/dist/extensions/cli/selfimprove.js +18 -0
- package/dist/extensions/cli/selfimprove.js.map +1 -0
- package/dist/extensions/home.js +350 -0
- package/dist/extensions/home.js.map +1 -0
- package/dist/extensions/login-route.js +1 -1
- package/dist/extensions/login-route.js.map +1 -1
- package/dist/extensions/onboarding.js +8 -7
- package/dist/extensions/onboarding.js.map +1 -1
- package/dist/extensions/selfimprove.js +49 -0
- package/dist/extensions/selfimprove.js.map +1 -0
- package/dist/extensions/setup-scan.js +23 -592
- package/dist/extensions/setup-scan.js.map +1 -1
- package/dist/extensions/setup.js +2 -47
- package/dist/extensions/setup.js.map +1 -1
- package/dist/extensions/vendor/openviking-pi/index.js +4 -0
- package/dist/extensions/vendor/openviking-pi/index.js.map +1 -1
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +3 -1
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -1
- package/dist/extensions/vendor/openviking-pi/sync.js +81 -0
- package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -1
- package/dist/src/archive/restore.js +18 -15
- package/dist/src/archive/restore.js.map +1 -1
- package/dist/src/botworker.js +13 -2
- package/dist/src/botworker.js.map +1 -1
- package/dist/src/channelfs.js +3 -26
- package/dist/src/channelfs.js.map +1 -1
- package/dist/src/cli.js +14 -50
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.js +16 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/daemon.js +33 -4
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/debuground.js +1 -0
- package/dist/src/debuground.js.map +1 -1
- package/dist/src/debugroundpi.js +1 -0
- package/dist/src/debugroundpi.js.map +1 -1
- package/dist/src/engine/caserunner-sdk.js +15 -251
- package/dist/src/engine/caserunner-sdk.js.map +1 -1
- package/dist/src/home/adapters.js +349 -0
- package/dist/src/home/adapters.js.map +1 -0
- package/dist/src/home/advanced.js +318 -0
- package/dist/src/home/advanced.js.map +1 -0
- package/dist/src/home/advancedtools.js +57 -0
- package/dist/src/home/advancedtools.js.map +1 -0
- package/dist/src/home/autoreview.js +325 -0
- package/dist/src/home/autoreview.js.map +1 -0
- package/dist/src/home/daemonstatus.js +32 -0
- package/dist/src/home/daemonstatus.js.map +1 -0
- package/dist/src/home/devicelogin.js +61 -0
- package/dist/src/home/devicelogin.js.map +1 -0
- package/dist/src/home/launch.js +202 -0
- package/dist/src/home/launch.js.map +1 -0
- package/dist/src/home/loginowner.js +205 -0
- package/dist/src/home/loginowner.js.map +1 -0
- package/dist/src/home/memory.js +122 -0
- package/dist/src/home/memory.js.map +1 -0
- package/dist/src/home/model.js +811 -0
- package/dist/src/home/model.js.map +1 -0
- package/dist/src/home/panel.js +520 -0
- package/dist/src/home/panel.js.map +1 -0
- package/dist/src/home/ports.js +19 -0
- package/dist/src/home/ports.js.map +1 -0
- package/dist/src/home/review.js +163 -0
- package/dist/src/home/review.js.map +1 -0
- package/dist/src/home/reviews.js +478 -0
- package/dist/src/home/reviews.js.map +1 -0
- package/dist/src/home/setup.js +507 -0
- package/dist/src/home/setup.js.map +1 -0
- package/dist/src/home/setupadapters.js +214 -0
- package/dist/src/home/setupadapters.js.map +1 -0
- package/dist/src/home/setupcard.js +199 -0
- package/dist/src/home/setupcard.js.map +1 -0
- package/dist/src/home/setuptools.js +45 -0
- package/dist/src/home/setuptools.js.map +1 -0
- package/dist/src/home/spend.js +160 -0
- package/dist/src/home/spend.js.map +1 -0
- package/dist/src/home/switches.js +209 -0
- package/dist/src/home/switches.js.map +1 -0
- package/dist/src/home/switchon.js +439 -0
- package/dist/src/home/switchon.js.map +1 -0
- package/dist/src/home/toolnames.js +41 -0
- package/dist/src/home/toolnames.js.map +1 -0
- package/dist/src/home/tune.js +341 -0
- package/dist/src/home/tune.js.map +1 -0
- package/dist/src/home/words.js +7 -0
- package/dist/src/home/words.js.map +1 -0
- package/dist/src/homes.js +14 -0
- package/dist/src/homes.js.map +1 -1
- package/dist/src/install.js +1 -1
- package/dist/src/install.js.map +1 -1
- package/dist/src/ipc/workerspec.js +8 -0
- package/dist/src/ipc/workerspec.js.map +1 -1
- package/dist/src/ledgercheck.js +39 -10
- package/dist/src/ledgercheck.js.map +1 -1
- package/dist/src/live/publish.js +1 -1
- package/dist/src/live/publish.js.map +1 -1
- package/dist/src/livecli.js +1 -1
- package/dist/src/livecli.js.map +1 -1
- package/dist/src/loginrefresh/handover.js +121 -0
- package/dist/src/loginrefresh/handover.js.map +1 -0
- package/dist/src/loginrefresh/lock.js +54 -3
- package/dist/src/loginrefresh/lock.js.map +1 -1
- package/dist/src/loginrefresh/refresher.js +33 -5
- package/dist/src/loginrefresh/refresher.js.map +1 -1
- package/dist/src/loginrefresh/supervisor.js +18 -1
- package/dist/src/loginrefresh/supervisor.js.map +1 -1
- package/dist/src/npmverbs.js +42 -12
- package/dist/src/npmverbs.js.map +1 -1
- package/dist/src/onboard/choices.js +6 -3
- package/dist/src/onboard/choices.js.map +1 -1
- package/dist/src/onboard/engine.js +33 -292
- package/dist/src/onboard/engine.js.map +1 -1
- package/dist/src/onboard/steps/bringup.js +3 -2
- package/dist/src/onboard/steps/bringup.js.map +1 -1
- package/dist/src/onboard/steps/configfill.js +18 -7
- package/dist/src/onboard/steps/configfill.js.map +1 -1
- package/dist/src/onboard/steps/loginshare.js +213 -0
- package/dist/src/onboard/steps/loginshare.js.map +1 -0
- package/dist/src/onboard/steps/memory.js +14 -2
- package/dist/src/onboard/steps/memory.js.map +1 -1
- package/dist/src/onboard/steps/repoconfig.js +72 -0
- package/dist/src/onboard/steps/repoconfig.js.map +1 -0
- package/dist/src/onboard/toolnames.js +2 -2
- package/dist/src/onboard/toolnames.js.map +1 -1
- package/dist/src/piagentdir.js +13 -0
- package/dist/src/piagentdir.js.map +1 -0
- package/dist/src/pilogin.js +31 -16
- package/dist/src/pilogin.js.map +1 -1
- package/dist/src/qcrbin.js +16 -11
- package/dist/src/qcrbin.js.map +1 -1
- package/dist/src/reviewargs.js +122 -0
- package/dist/src/reviewargs.js.map +1 -0
- package/dist/src/reviewcli.js +7 -161
- package/dist/src/reviewcli.js.map +1 -1
- package/dist/src/reviewdetach.js +76 -11
- package/dist/src/reviewdetach.js.map +1 -1
- package/dist/src/scancli.js +39 -691
- package/dist/src/scancli.js.map +1 -1
- package/dist/src/selfimprove.js +66 -0
- package/dist/src/selfimprove.js.map +1 -0
- package/dist/src/session-command-supervisor.js +38 -9
- package/dist/src/session-command-supervisor.js.map +1 -1
- package/dist/src/setup/adapters.js +5 -223
- package/dist/src/setup/adapters.js.map +1 -1
- package/dist/src/setup/caserunner.js +15 -249
- package/dist/src/setup/caserunner.js.map +1 -1
- package/dist/src/setup/engine.js +32 -341
- package/dist/src/setup/engine.js.map +1 -1
- package/dist/src/setup/facts.js +3 -592
- package/dist/src/setup/facts.js.map +1 -1
- package/dist/src/setup/findings.js +3 -947
- package/dist/src/setup/findings.js.map +1 -1
- package/dist/src/setup/presentation.js +3 -8
- package/dist/src/setup/presentation.js.map +1 -1
- package/dist/src/setup/scan.js +3 -724
- package/dist/src/setup/scan.js.map +1 -1
- package/dist/src/setup/scanbuild.js +9 -92
- package/dist/src/setup/scanbuild.js.map +1 -1
- package/dist/src/setup/scanlease.js +3 -250
- package/dist/src/setup/scanlease.js.map +1 -1
- package/dist/src/setup/scanreport.js +3 -288
- package/dist/src/setup/scanreport.js.map +1 -1
- package/dist/src/setup/scanrun.js +3 -1732
- package/dist/src/setup/scanrun.js.map +1 -1
- package/dist/src/setup/scantree.js +3 -256
- package/dist/src/setup/scantree.js.map +1 -1
- package/dist/src/setup/stage2request.js +3 -258
- package/dist/src/setup/stage2request.js.map +1 -1
- package/dist/src/setup/unitcut.js +3 -436
- package/dist/src/setup/unitcut.js.map +1 -1
- package/dist/src/state.js +75 -9
- package/dist/src/state.js.map +1 -1
- package/dist/src/takeover.js +1 -0
- package/dist/src/takeover.js.map +1 -1
- package/dist/src/takeoverpi.js +2 -1
- package/dist/src/takeoverpi.js.map +1 -1
- package/dist/src/tui/alerts.js +4 -1
- package/dist/src/tui/alerts.js.map +1 -1
- package/dist/src/tui/board.js +2 -2
- package/dist/src/tui/board.js.map +1 -1
- package/dist/src/tui/companion.js +2 -1
- package/dist/src/tui/companion.js.map +1 -1
- package/dist/src/tui/deploytab.js +2 -2
- package/dist/src/tui/deploytab.js.map +1 -1
- package/dist/src/tui/index.js +9 -6
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/memory.js +3 -16
- package/dist/src/tui/memory.js.map +1 -1
- package/dist/src/tui/memoryurl.js +22 -0
- package/dist/src/tui/memoryurl.js.map +1 -0
- package/dist/src/tui/remote.js +1 -1
- package/dist/src/tui/remote.js.map +1 -1
- package/dist/src/tui/route.js +2 -2
- package/dist/src/tui/route.js.map +1 -1
- package/dist/src/tui/setupstate.js +8 -5
- package/dist/src/tui/setupstate.js.map +1 -1
- package/dist/src/tui/sshtriage.js +1 -1
- package/dist/src/tui/sshtriage.js.map +1 -1
- package/dist/src/verdicts.js +12 -0
- package/dist/src/verdicts.js.map +1 -1
- package/dist/src/wire.js +9 -4
- package/dist/src/wire.js.map +1 -1
- package/dist/src/workerbreaker.js +32 -5
- package/dist/src/workerbreaker.js.map +1 -1
- package/dist/src/workerworld.js +4 -0
- package/dist/src/workerworld.js.map +1 -1
- package/docker/README.md +1 -1
- package/docker/entrypoint.sh +40 -6
- package/docker-compose.yml +16 -1
- package/extensions/cli/home.ts +14 -0
- package/extensions/cli/selfimprove.ts +18 -0
- package/extensions/home.ts +484 -0
- package/extensions/login-route.ts +1 -1
- package/extensions/onboarding.ts +8 -7
- package/extensions/selfimprove.ts +65 -0
- package/extensions/setup-scan.ts +79 -766
- package/extensions/setup.ts +14 -70
- package/extensions/vendor/openviking-pi/NOTICE +5 -0
- package/extensions/vendor/openviking-pi/README.md +39 -5
- package/extensions/vendor/openviking-pi/index.ts +4 -0
- package/extensions/vendor/openviking-pi/patches/0014-one-turn-sync-in-flight.patch +239 -0
- package/extensions/vendor/openviking-pi/qcr-hooks.ts +3 -1
- package/extensions/vendor/openviking-pi/sync.ts +80 -0
- package/extensions/vendor/openviking-pi/upstream.json +2 -1
- package/package.json +6 -4
- package/prompts/onboarding.md +8 -7
- package/schemas/review-result.v1.json +60 -0
- package/scripts/deploy.sh +8 -6
- package/scripts/qcr-host.sh +28 -5
- package/scripts/vm-ghcr-login.sh +7 -4
- package/src/archive/restore.ts +18 -21
- package/src/botworker.ts +13 -2
- package/src/channelfs.ts +3 -26
- package/src/cli.ts +13 -54
- package/src/config.ts +38 -0
- package/src/daemon.ts +34 -4
- package/src/debuground.ts +3 -0
- package/src/debugroundpi.ts +1 -0
- package/src/engine/caserunner-sdk.ts +29 -314
- package/src/home/adapters.ts +418 -0
- package/src/home/advanced.ts +421 -0
- package/src/home/advancedtools.ts +96 -0
- package/src/home/autoreview.ts +385 -0
- package/src/home/daemonstatus.ts +55 -0
- package/src/home/devicelogin.ts +107 -0
- package/src/home/launch.ts +257 -0
- package/src/home/loginowner.ts +248 -0
- package/src/home/memory.ts +176 -0
- package/src/home/model.ts +965 -0
- package/src/home/panel.ts +666 -0
- package/src/home/ports.ts +338 -0
- package/src/home/review.ts +206 -0
- package/src/home/reviews.ts +565 -0
- package/src/home/setup.ts +689 -0
- package/src/home/setupadapters.ts +217 -0
- package/src/home/setupcard.ts +288 -0
- package/src/home/setuptools.ts +57 -0
- package/src/home/spend.ts +194 -0
- package/src/home/switches.ts +293 -0
- package/src/home/switchon.ts +554 -0
- package/src/home/toolnames.ts +46 -0
- package/src/home/tune.ts +405 -0
- package/src/home/words.ts +8 -0
- package/src/homes.ts +17 -0
- package/src/install.ts +1 -1
- package/src/ipc/workerspec.ts +15 -0
- package/src/ledgercheck.ts +45 -9
- package/src/live/publish.ts +4 -1
- package/src/livecli.ts +6 -1
- package/src/loginrefresh/handover.ts +138 -0
- package/src/loginrefresh/lock.ts +72 -3
- package/src/loginrefresh/refresher.ts +34 -5
- package/src/loginrefresh/supervisor.ts +25 -2
- package/src/npmverbs.ts +51 -11
- package/src/onboard/choices.ts +6 -3
- package/src/onboard/engine.ts +64 -434
- package/src/onboard/steps/bringup.ts +3 -2
- package/src/onboard/steps/configfill.ts +23 -7
- package/src/onboard/steps/loginshare.ts +258 -0
- package/src/onboard/steps/memory.ts +18 -2
- package/src/onboard/steps/repoconfig.ts +85 -0
- package/src/onboard/toolnames.ts +2 -2
- package/src/piagentdir.ts +12 -0
- package/src/pilogin.ts +32 -16
- package/src/qcrbin.ts +16 -11
- package/src/reviewargs.ts +119 -0
- package/src/reviewcli.ts +6 -167
- package/src/reviewdetach.ts +107 -13
- package/src/scancli.ts +75 -826
- package/src/selfimprove.ts +90 -0
- package/src/session-command-supervisor.ts +41 -8
- package/src/setup/adapters.ts +13 -221
- package/src/setup/caserunner.ts +34 -273
- package/src/setup/engine.ts +62 -496
- package/src/setup/facts.ts +3 -627
- package/src/setup/findings.ts +3 -1264
- package/src/setup/presentation.ts +3 -18
- package/src/setup/scan.ts +3 -1033
- package/src/setup/scanbuild.ts +20 -114
- package/src/setup/scanlease.ts +3 -297
- package/src/setup/scanreport.ts +3 -373
- package/src/setup/scanrun.ts +3 -2268
- package/src/setup/scantree.ts +3 -259
- package/src/setup/stage2request.ts +3 -347
- package/src/setup/unitcut.ts +3 -523
- package/src/state.ts +126 -11
- package/src/takeover.ts +5 -0
- package/src/takeoverpi.ts +3 -2
- package/src/tui/alerts.ts +9 -2
- package/src/tui/board.ts +2 -2
- package/src/tui/companion.ts +2 -1
- package/src/tui/deploytab.ts +2 -2
- package/src/tui/index.ts +9 -6
- package/src/tui/memory.ts +3 -15
- package/src/tui/memoryurl.ts +22 -0
- package/src/tui/remote.ts +1 -1
- package/src/tui/route.ts +2 -2
- package/src/tui/setupstate.ts +20 -16
- package/src/tui/sshtriage.ts +1 -1
- package/src/verdicts.ts +13 -0
- package/src/wire.ts +9 -4
- package/src/workerbreaker.ts +41 -5
- package/src/workerworld.ts +9 -1
- package/dist/src/live/projection.js +0 -450
- package/dist/src/live/projection.js.map +0 -1
- package/dist/src/live/read.js +0 -60
- package/dist/src/live/read.js.map +0 -1
- package/dist/src/onboard/launch.js +0 -75
- package/dist/src/onboard/launch.js.map +0 -1
- package/dist/src/setup/ledger.js +0 -207
- package/dist/src/setup/ledger.js.map +0 -1
- package/dist/src/setup/stage2.js +0 -617
- package/dist/src/setup/stage2.js.map +0 -1
- package/dist/src/setup/textsniff.js +0 -89
- package/dist/src/setup/textsniff.js.map +0 -1
- package/src/live/projection.ts +0 -491
- package/src/live/read.ts +0 -86
- package/src/onboard/launch.ts +0 -103
- package/src/setup/ledger.ts +0 -324
- package/src/setup/stage2.ts +0 -926
- package/src/setup/textsniff.ts +0 -95
package/dist/src/setup/scan.js
CHANGED
|
@@ -1,725 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
// stateful half — the run, the ledger, the findings store — is scanrun.ts,
|
|
5
|
-
// ledger.ts and findings.ts, every effect through a port.
|
|
6
|
-
//
|
|
7
|
-
// Stage 1 is `pass-scan` over every unit of the saved unit cut, one case per
|
|
8
|
-
// unit (an oversized unit splits, its name kept; a single file over one case
|
|
9
|
-
// is read in overlapping line ranges, one case each — `cutRanges`), at the
|
|
10
|
-
// `[scan]` tier and effort. It never reads tests or migrations (they enter
|
|
11
|
-
// only in stage 2, paired with their code), nor what no pass reads (lockfiles, generated
|
|
12
|
-
// output, binaries, prose — `isSweepSkipped`), nor the operator's `[scan]
|
|
13
|
-
// exclude` globs. A file the coverage ledger already holds at the same
|
|
14
|
-
// content hash for `pass-scan` is not read again: that is how an interrupted
|
|
15
|
-
// scan resumes, and why a finished one costs nothing to "re-run". Coverage is
|
|
16
|
-
// read at the run's depth (`[scan] depth`): a deep run reads again a file only
|
|
17
|
-
// a standard reading covers, and a deep reading covers both (ledger.ts).
|
|
18
|
-
//
|
|
19
|
-
// A delta re-scan is stage 1 over the files whose content changed since the
|
|
20
|
-
// last scan (read by the ledger at another content, or new or different
|
|
21
|
-
// against the tree the last scan read) and every file no complete reading
|
|
22
|
-
// covers at its content — never read (a capped or stopped run's tail;
|
|
23
|
-
// unreadable then, readable now) or cut short between ranges. Those are
|
|
24
|
-
// owed, not skipped: a delta that left them out would leave a file no
|
|
25
|
-
// lens ever read out of every delta for as long as it stays unchanged.
|
|
26
|
-
import { planKey } from "@quickchatai/pi-quickreview/src/plankey.js";
|
|
27
|
-
import { createHash } from "node:crypto";
|
|
28
|
-
import { posix } from "node:path";
|
|
29
|
-
import { globMatcher } from "@quickchatai/pi-quickreview/src/glob.js";
|
|
30
|
-
import { isSweepSkipped, isTestFile } from "@quickchatai/pi-quickreview/src/heuristics.js";
|
|
31
|
-
import { SCAN_DEEP_MAX_SUGGESTIONS, SCAN_FALSE_POSITIVE_CLASSES, SCAN_PASS, VALIDATE_PASS, } from "@quickchatai/pi-quickreview/src/include.js";
|
|
32
|
-
import { SCAN_DEEP_LENSES } from "@quickchatai/pi-quickreview/src/passes.js";
|
|
33
|
-
import { REACHABILITY_VALUES, reachabilityCappedSeverity, } from "@quickchatai/pi-quickreview/src/severity.js";
|
|
34
|
-
import { isMigrationFile } from "./facts.js";
|
|
35
|
-
import { assign, splitUnit } from "./unitcut.js";
|
|
36
|
-
/** The stage-1 lens (agents/scan/pass-scan.md). */
|
|
37
|
-
export { SCAN_PASS };
|
|
38
|
-
/** Lenses a stage-1 suggestion may NOT name (passes.ts), and the deep lenses
|
|
39
|
-
* stage 1 may point stage 2 at. */
|
|
40
|
-
export { DIFF_ONLY_LENSES, SCAN_DEEP_LENSES } from "@quickchatai/pi-quickreview/src/passes.js";
|
|
41
|
-
export { isMigrationFile };
|
|
42
|
-
/** Why stage 1 does not read `path`, or null when it does. */
|
|
43
|
-
export function stage1Exclusion(path, excluded) {
|
|
44
|
-
if (isSweepSkipped(path))
|
|
45
|
-
return "unreadable-kind";
|
|
46
|
-
if (isTestFile(path))
|
|
47
|
-
return "test";
|
|
48
|
-
if (isMigrationFile(path))
|
|
49
|
-
return "migration";
|
|
50
|
-
if (excluded(path))
|
|
51
|
-
return "excluded-glob";
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
/** Neighbouring ranges overlap by this many lines at most (a tenth of the
|
|
55
|
-
* window below that), so a finding straddling a cut sits whole in one. */
|
|
56
|
-
export const RANGE_OVERLAP_MAX = 40;
|
|
57
|
-
/** A top-level declaration: a line a cut may fall just before. */
|
|
58
|
-
const DECL_RE = /^(export\s+(default\s+)?)?(declare\s+)?(abstract\s+)?(async\s+)?(pub(\([a-z]+\))?\s+)?(def|class|function\*?|interface|type|enum|const|let|var|namespace|module|fn|func|impl|struct|trait|object)\b/;
|
|
59
|
-
/** Lines that belong to the declaration below them (decorators, comments). */
|
|
60
|
-
const LEAD_RE = /^(@\w|#|\/\/|\/\*|\s\*)/;
|
|
61
|
-
/**
|
|
62
|
-
* Cut a file of `total` lines into windows of at most `maxLines`: each
|
|
63
|
-
* window ends just before a top-level declaration found in its second half
|
|
64
|
-
* when the text is at hand (pulled above that declaration's decorators and
|
|
65
|
-
* comments), else at the limit; the next starts a few lines earlier
|
|
66
|
-
* (`RANGE_OVERLAP_MAX`), so neighbours overlap and nothing falls between
|
|
67
|
-
* them. A file that fits is one window. Deterministic.
|
|
68
|
-
*/
|
|
69
|
-
export function cutRanges(total, maxLines, text) {
|
|
70
|
-
const n = Math.max(0, Math.floor(total));
|
|
71
|
-
const w = Math.max(1, Math.floor(maxLines));
|
|
72
|
-
if (n <= w)
|
|
73
|
-
return [{ start: 1, end: Math.max(n, 1), total: n }];
|
|
74
|
-
const overlap = Math.min(RANGE_OVERLAP_MAX, Math.floor(w / 10));
|
|
75
|
-
const lines = text === null ? null : text.split("\n");
|
|
76
|
-
const at = (l) => (lines === null ? "" : (lines[l - 1] ?? ""));
|
|
77
|
-
const out = [];
|
|
78
|
-
let s = 1;
|
|
79
|
-
for (;;) {
|
|
80
|
-
if (n - s + 1 <= w) {
|
|
81
|
-
out.push({ start: s, end: n, total: n });
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
let end = s + w - 1;
|
|
85
|
-
if (lines !== null) {
|
|
86
|
-
const floor = s + Math.ceil(w / 2);
|
|
87
|
-
for (let l = end + 1; l > floor; l--) {
|
|
88
|
-
if (!DECL_RE.test(at(l)))
|
|
89
|
-
continue;
|
|
90
|
-
let first = l;
|
|
91
|
-
while (first - 1 > floor && LEAD_RE.test(at(first - 1)))
|
|
92
|
-
first--;
|
|
93
|
-
end = first - 1;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
out.push({ start: s, end, total: n });
|
|
98
|
-
s = Math.max(end + 1 - overlap, s + 1);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/** Does the union of `ranges` cover every line of `start..end`? */
|
|
102
|
-
export function rangesCover(ranges, start, end) {
|
|
103
|
-
let reach = start - 1;
|
|
104
|
-
for (const r of [...ranges].sort((a, b) => a.start - b.start)) {
|
|
105
|
-
if (r.start > reach + 1)
|
|
106
|
-
break;
|
|
107
|
-
reach = Math.max(reach, r.end);
|
|
108
|
-
if (reach >= end)
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
return reach >= end;
|
|
112
|
-
}
|
|
113
|
-
/** Has `f`'s content changed since the last scan? Newest read by the ledger
|
|
114
|
-
* at another content, or absent from / different in the last scan's tree.
|
|
115
|
-
* Not what a delta reads by: an unchanged file with no complete reading at
|
|
116
|
-
* this content is owed too (planStage1). */
|
|
117
|
-
export function changedSince(f, basis) {
|
|
118
|
-
const read = basis.read.get(f.path);
|
|
119
|
-
if (read !== undefined && read !== f.blob)
|
|
120
|
-
return true;
|
|
121
|
-
const was = basis.baseline.get(f.path);
|
|
122
|
-
return was === undefined || was !== f.blob;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Coverage first. The hard stop ends a run whose actuals outrun its estimate
|
|
126
|
-
* before the plan's tail, so the tail must not be the same cases every run:
|
|
127
|
-
* a file the ledger never read, or whose newest reading was cut short
|
|
128
|
-
* between ranges, is tier 0; a file read before at other content is tier 1
|
|
129
|
-
* (changed); anything else is tier 2. A case takes its best file's
|
|
130
|
-
* tier; a tier keeps the plan's order.
|
|
131
|
-
*/
|
|
132
|
-
export function readTier(f, history) {
|
|
133
|
-
const r = history.get(f.path);
|
|
134
|
-
if (r === undefined || !r.complete)
|
|
135
|
-
return 0;
|
|
136
|
-
return r.blob !== f.blob ? 1 : 2;
|
|
137
|
-
}
|
|
138
|
-
export const coverKey = (path, blob) => `${path}\0${blob}`;
|
|
139
|
-
/** Stage 1 over the saved cut at one tree: every readable, uncovered,
|
|
140
|
-
* non-excluded file (a delta the same set, counting what it owes unchanged
|
|
141
|
-
* as `owed`), one case per
|
|
142
|
-
* unit, split to fit — ordered coverage-first by `history` (readTier).
|
|
143
|
-
* Deterministic. */
|
|
144
|
-
export function planStage1(input) {
|
|
145
|
-
const excludedGlob = globMatcher(input.exclude);
|
|
146
|
-
const excluded = {
|
|
147
|
-
test: 0,
|
|
148
|
-
migration: 0,
|
|
149
|
-
"unreadable-kind": 0,
|
|
150
|
-
"excluded-glob": 0,
|
|
151
|
-
};
|
|
152
|
-
const blobs = new Map(input.files.map((f) => [f.path, f.blob]));
|
|
153
|
-
const lines = new Map();
|
|
154
|
-
let covered = 0;
|
|
155
|
-
let unreadable = 0;
|
|
156
|
-
const skipped = [];
|
|
157
|
-
const unchangedPaths = new Set();
|
|
158
|
-
const readable = [];
|
|
159
|
-
// Files over one case: the ranges still to read, cut up front.
|
|
160
|
-
const ranged = new Map();
|
|
161
|
-
for (const f of [...input.files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) {
|
|
162
|
-
const why = stage1Exclusion(f.path, excludedGlob);
|
|
163
|
-
if (why !== null) {
|
|
164
|
-
excluded[why]++;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const n = input.linesOf(f.path);
|
|
168
|
-
if (n === null) {
|
|
169
|
-
unreadable++;
|
|
170
|
-
skipped.push({ path: f.path, why: input.whyUnreadable?.(f.path) ?? "unreadable" });
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (input.covered.has(coverKey(f.path, f.blob))) {
|
|
174
|
-
covered++;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
// A delta reads what changed since the last scan AND every file no
|
|
178
|
-
// complete reading covers at this content — never read (the rest of an
|
|
179
|
-
// unfinished scan; unreadable then, readable now), or cut short between
|
|
180
|
-
// ranges. Past `covered`, that is every file left: a delta leaves out only
|
|
181
|
-
// what an ordinary plan leaves out, and says how much it owes unchanged.
|
|
182
|
-
// An unchanged file that was never read is owed, never skipped as
|
|
183
|
-
// "unchanged".
|
|
184
|
-
if (input.delta !== undefined && !changedSince(f, input.delta))
|
|
185
|
-
unchangedPaths.add(f.path);
|
|
186
|
-
if (n > input.budget.maxLines) {
|
|
187
|
-
const done = input.partial?.get(coverKey(f.path, f.blob)) ?? [];
|
|
188
|
-
const todo = cutRanges(n, input.budget.maxLines, input.textOf?.(f.path) ?? null).filter((r) => !rangesCover(done, r.start, r.end));
|
|
189
|
-
if (todo.length === 0) {
|
|
190
|
-
covered++;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
ranged.set(f.path, todo);
|
|
194
|
-
}
|
|
195
|
-
lines.set(f.path, n);
|
|
196
|
-
readable.push(f.path);
|
|
197
|
-
}
|
|
198
|
-
const a = assign(input.cut, readable);
|
|
199
|
-
const cases = [];
|
|
200
|
-
for (const u of input.cut.units) {
|
|
201
|
-
const owned = a.byUnit.get(u.name) ?? [];
|
|
202
|
-
if (owned.length === 0)
|
|
203
|
-
continue;
|
|
204
|
-
// A file over one case is alone in its split case; it becomes one case
|
|
205
|
-
// per range. Parts are numbered over the unit's final list.
|
|
206
|
-
const parts = [];
|
|
207
|
-
for (const c of splitUnit(u.name, owned, (p) => lines.get(p) ?? 0, input.budget)) {
|
|
208
|
-
const only = c.files.length === 1 ? ranged.get(c.files[0]) : undefined;
|
|
209
|
-
if (only !== undefined) {
|
|
210
|
-
const p = c.files[0];
|
|
211
|
-
for (const r of only) {
|
|
212
|
-
parts.push({
|
|
213
|
-
files: [{ path: p, blob: blobs.get(p), lines: r.end - r.start + 1, range: r }],
|
|
214
|
-
lines: r.end - r.start + 1,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
parts.push({
|
|
220
|
-
files: c.files.map((p) => ({ path: p, blob: blobs.get(p), lines: lines.get(p) ?? 0 })),
|
|
221
|
-
lines: c.lines,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
parts.forEach((p, i) => {
|
|
225
|
-
cases.push({
|
|
226
|
-
unit: u.name,
|
|
227
|
-
id: parts.length === 1 ? u.name : `${u.name}#${i + 1}`,
|
|
228
|
-
part: i + 1,
|
|
229
|
-
parts: parts.length,
|
|
230
|
-
files: p.files,
|
|
231
|
-
lines: p.lines,
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
const history = input.history;
|
|
236
|
-
if (history !== undefined) {
|
|
237
|
-
const rank = (c, i) => {
|
|
238
|
-
const tiers = c.files.map((f) => readTier(f, history));
|
|
239
|
-
const t = Math.min(...tiers);
|
|
240
|
-
// The stalest tier-1 reading of the case ("" sorts first; tier 0/2 unused).
|
|
241
|
-
let at = "";
|
|
242
|
-
if (t === 1) {
|
|
243
|
-
const stamps = c.files.filter((_, k) => tiers[k] === 1).map((f) => history.get(f.path).at);
|
|
244
|
-
at = stamps.reduce((m, x) => (x < m ? x : m));
|
|
245
|
-
}
|
|
246
|
-
return { c, i, t, at };
|
|
247
|
-
};
|
|
248
|
-
const ranked = cases.map(rank);
|
|
249
|
-
ranked.sort((x, y) => x.t - y.t || (x.at < y.at ? -1 : x.at > y.at ? 1 : 0) || x.i - y.i);
|
|
250
|
-
cases.splice(0, cases.length, ...ranked.map((r) => r.c));
|
|
251
|
-
}
|
|
252
|
-
const owed = new Set(cases.flatMap((c) => c.files.map((f) => f.path)).filter((p) => unchangedPaths.has(p))).size;
|
|
253
|
-
return { cases, excluded, covered, unassigned: a.unassigned, unreadable, skipped, owed };
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* The calibration pick: one case from each of up to `n` units spread across
|
|
257
|
-
* the size range (by the unit's planned lines), so the measured rate is not
|
|
258
|
-
* the rate of the smallest or the largest code alone. Deterministic.
|
|
259
|
-
*/
|
|
260
|
-
export function calibrationCases(plan, n) {
|
|
261
|
-
const byUnit = new Map();
|
|
262
|
-
for (const c of plan)
|
|
263
|
-
byUnit.set(c.unit, [...(byUnit.get(c.unit) ?? []), c]);
|
|
264
|
-
const units = [...byUnit.entries()]
|
|
265
|
-
.map(([unit, cs]) => ({ unit, lines: cs.reduce((s, c) => s + c.lines, 0), first: cs[0] }))
|
|
266
|
-
.sort((a, b) => a.lines - b.lines || (a.unit < b.unit ? -1 : 1));
|
|
267
|
-
if (units.length <= n)
|
|
268
|
-
return units.map((u) => u.first);
|
|
269
|
-
const picked = new Set();
|
|
270
|
-
for (let i = 0; i < n; i++)
|
|
271
|
-
picked.add(Math.floor(((i + 1) * units.length) / (n + 1)));
|
|
272
|
-
return [...picked].sort((a, b) => a - b).map((i) => units[i].first);
|
|
273
|
-
}
|
|
274
|
-
export function caseEstimate(lines, rate) {
|
|
275
|
-
return (Math.max(lines, 1) / 1000) * rate.usd_per_kline;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Run `lanes` copies of `lane` to the end. One that throws aborts the rest
|
|
279
|
-
* and the throw is raised only once EVERY lane has stopped — never while a
|
|
280
|
-
* sibling is still in a paid case, which would otherwise go on spending and
|
|
281
|
-
* writing the run record after the run is settled and its slot released.
|
|
282
|
-
*/
|
|
283
|
-
export async function drainLanes(lanes, lane, abort) {
|
|
284
|
-
let failed = null;
|
|
285
|
-
await Promise.all(Array.from({ length: lanes }, async () => {
|
|
286
|
-
try {
|
|
287
|
-
await lane();
|
|
288
|
-
}
|
|
289
|
-
catch (err) {
|
|
290
|
-
failed ??= { err };
|
|
291
|
-
abort.abort();
|
|
292
|
-
}
|
|
293
|
-
}));
|
|
294
|
-
if (failed !== null)
|
|
295
|
-
throw failed.err;
|
|
296
|
-
}
|
|
297
|
-
/** What a preview's key covers (packages/pi-plugin/src/plankey.ts, shared with `qcr review`). */
|
|
298
|
-
export { planKey };
|
|
299
|
-
/** Where a lens opens a tree file: its absolute path under the root. */
|
|
300
|
-
export function treePath(root, path) {
|
|
301
|
-
return posix.join(root, path);
|
|
302
|
-
}
|
|
303
|
-
/** The line every scan task carries about paths: the lens runs outside the
|
|
304
|
-
* tree (first-party cwd — the case runner), so a relative path it opens is
|
|
305
|
-
* some other checkout's file, never the scanned one. */
|
|
306
|
-
export const TREE_PATHS_NOTE = "Your working directory is NOT the tree: a relative path opens some other checkout's file. Open every file — focus files, callers, tests — by its absolute path under the tree root (each focus file below says where). Paths in your answer are relative to the tree root.";
|
|
307
|
-
/** One focus file as a task names it: its repo-relative path (what an
|
|
308
|
-
* answer anchors to), its size or range, and — given the tree root — the
|
|
309
|
-
* absolute path to open it by. */
|
|
310
|
-
export function focusLine(f, root) {
|
|
311
|
-
const open = root === undefined ? "" : ` — open ${treePath(root, f.path)}`;
|
|
312
|
-
return f.range
|
|
313
|
-
? `- ${f.path} (lines ${f.range.start}-${f.range.end} of ${f.range.total} — read the whole file, report only inside lines ${f.range.start}-${f.range.end})${open}`
|
|
314
|
-
: `- ${f.path} (${f.lines} lines)${open}`;
|
|
315
|
-
}
|
|
316
|
-
/** The note a case with a ranged file carries, or none. */
|
|
317
|
-
export function rangeNote(files) {
|
|
318
|
-
const ranged = files.filter((f) => f.range);
|
|
319
|
-
if (ranged.length === 0)
|
|
320
|
-
return [];
|
|
321
|
-
return [
|
|
322
|
-
"",
|
|
323
|
-
...ranged.map((f) => `Focus range: ${f.path} lines ${f.range.start}-${f.range.end} of ${f.range.total}. The file is larger than one case, so it is cut into line ranges and the other ranges are other cases: read the whole file for context, but report only findings anchored inside lines ${f.range.start}-${f.range.end} — anything outside it is dropped.`),
|
|
324
|
-
];
|
|
325
|
-
}
|
|
326
|
-
function fileList(c, root) {
|
|
327
|
-
return c.files.map((f) => focusLine(f, root)).join("\n");
|
|
328
|
-
}
|
|
329
|
-
/** The stage-1 task string for one case. */
|
|
330
|
-
export function scanCasePrompt(c, ctx, deepLenses = SCAN_DEEP_LENSES) {
|
|
331
|
-
const part = c.parts > 1 ? ` (part ${c.part} of ${c.parts})` : "";
|
|
332
|
-
return [
|
|
333
|
-
`SCAN CASE ${ctx.caseId} — stage 1, unit ${c.unit}${part}.`,
|
|
334
|
-
`Scan sha: ${ctx.sha}`,
|
|
335
|
-
`Tree root (the repository at the scan sha, read-only): ${ctx.root}`,
|
|
336
|
-
TREE_PATHS_NOTE,
|
|
337
|
-
"",
|
|
338
|
-
`Focus files (${c.files.length}, ${c.lines} lines — read each in full):`,
|
|
339
|
-
fileList(c, ctx.root),
|
|
340
|
-
...rangeNote(c.files),
|
|
341
|
-
"",
|
|
342
|
-
`Deep lenses you may name in SUGGEST: ${deepLenses.join(", ")}.`,
|
|
343
|
-
"",
|
|
344
|
-
`Answer with the findings block headed \`FINDINGS ${SCAN_PASS} SCAN ${ctx.caseId} @${ctx.sha}\` (or NONE), then the SUGGEST block.`,
|
|
345
|
-
].join("\n");
|
|
346
|
-
}
|
|
347
|
-
/** One candidate as a validator reads it, in the findings grammar — the
|
|
348
|
-
* scan's skeptic and the replay's validator alike. */
|
|
349
|
-
export function candidateItem(f) {
|
|
350
|
-
return [
|
|
351
|
-
`- path: ${f.path}`,
|
|
352
|
-
` line: ${f.start === f.end ? f.end : `${f.start}-${f.end}`}`,
|
|
353
|
-
` kind: ${f.kind}`,
|
|
354
|
-
// The severity the LENS wrote, not the capped one: the validator audits
|
|
355
|
-
// reachability, and a corrected `routine` must be able to restore it —
|
|
356
|
-
// its survivors are re-read, so the cap applies again on the way out.
|
|
357
|
-
` severity: ${f.capped_from ?? f.severity}`,
|
|
358
|
-
...(f.confidence !== null ? [` confidence: ${f.confidence}`] : []),
|
|
359
|
-
...(f.reachability !== null ? [` reachability: ${f.reachability}`] : []),
|
|
360
|
-
` claim: ${f.claim}`,
|
|
361
|
-
...(f.evidence ? [` evidence: ${f.evidence}`] : []),
|
|
362
|
-
// The failure mechanism: the validator judges with it, and a survivor
|
|
363
|
-
// is re-read from the validator's answer, so it must travel both ways.
|
|
364
|
-
...(f.detail ? [` detail: ${f.detail}`] : []),
|
|
365
|
-
...(f.fix ? [` fix: ${f.fix}`] : []),
|
|
366
|
-
].join("\n");
|
|
367
|
-
}
|
|
368
|
-
/** The validator's task string over one case's stage-1 candidates. */
|
|
369
|
-
export function scanValidatorPrompt(c, candidates, ctx, lens = SCAN_PASS, label = `stage 1, unit ${c.unit}`) {
|
|
370
|
-
const block = candidates.map(candidateItem).join("\n");
|
|
371
|
-
return [
|
|
372
|
-
`SCAN VALIDATION ${ctx.caseId} — ${label}.`,
|
|
373
|
-
`Scan sha: ${ctx.sha}`,
|
|
374
|
-
`Tree root (read-only): ${ctx.root}`,
|
|
375
|
-
TREE_PATHS_NOTE,
|
|
376
|
-
"",
|
|
377
|
-
`The ${lens} lens read these files:`,
|
|
378
|
-
fileList(c, ctx.root),
|
|
379
|
-
"",
|
|
380
|
-
`Its ${candidates.length} candidate finding(s):`,
|
|
381
|
-
`FINDINGS ${lens} SCAN ${ctx.caseId} @${ctx.sha}`,
|
|
382
|
-
block,
|
|
383
|
-
"",
|
|
384
|
-
`Judge every candidate against the files under the tree root, as the scan's skeptic: try to refute each one. Return the survivors as a findings block headed \`FINDINGS ${VALIDATE_PASS} SCAN ${ctx.caseId} @${ctx.sha}\` (keep each survivor's path, line, kind and detail, and add its \`validity:\` — confirmed or likely — and its \`scenario:\`; a confirmed scenario cites a \`path:line\` for every hop it traced, or it is read as likely), then your DOUBTED block for every other candidate. Nothing is killed in a scan: write no KILLED block.`,
|
|
385
|
-
].join("\n");
|
|
386
|
-
}
|
|
387
|
-
// ---- reading the answer ----------------------------------------------------------
|
|
388
|
-
export const SEVERITIES = ["P0", "P1", "P2", "P3", "nit"];
|
|
389
|
-
/**
|
|
390
|
-
* The candidate a validator's survivor stands for: one not yet `used`, on the
|
|
391
|
-
* same file with overlapping lines — the same kind first. One survivor stands
|
|
392
|
-
* for at most one candidate, so a duplicate on the same lines or a neighbour
|
|
393
|
-
* of another kind is never silently claimed by a survivor that did not mean
|
|
394
|
-
* it. The scan and the replay attribute survivors through this one rule.
|
|
395
|
-
*/
|
|
396
|
-
export function survivorSource(survivor, candidates, used) {
|
|
397
|
-
const open = candidates.filter((c) => !used.has(c) &&
|
|
398
|
-
c.path === survivor.path &&
|
|
399
|
-
c.start <= survivor.end &&
|
|
400
|
-
survivor.start <= c.end);
|
|
401
|
-
return open.find((c) => c.kind === survivor.kind) ?? open[0];
|
|
402
|
-
}
|
|
403
|
-
/** At most this many suggestions per case are kept — `SCAN_DEEP_MAX_SUGGESTIONS`
|
|
404
|
-
* in a deep scan, where every focus file is to be named in one. */
|
|
405
|
-
export const MAX_SUGGESTIONS = 5;
|
|
406
|
-
export { SCAN_DEEP_MAX_SUGGESTIONS };
|
|
407
|
-
const ITEM_FIELD = /^(\s*)(-\s+)?([a-z][a-z0-9_-]{0,40}):(?:\s(.*)|$)/i;
|
|
408
|
-
/** Parse the `- key: value` items of one block (continuation lines join the
|
|
409
|
-
* previous field; the first occurrence of a key wins). */
|
|
410
|
-
function parseItems(text) {
|
|
411
|
-
const items = [];
|
|
412
|
-
let cur = null;
|
|
413
|
-
let last = "";
|
|
414
|
-
let itemIndent = -1;
|
|
415
|
-
for (const raw of text.split("\n")) {
|
|
416
|
-
const line = raw.replace(/\s+$/, "");
|
|
417
|
-
if (/^\s*```/.test(line) || line.trim() === "")
|
|
418
|
-
continue;
|
|
419
|
-
const indent = /^\s*/.exec(line)[0].length;
|
|
420
|
-
const m = ITEM_FIELD.exec(line);
|
|
421
|
-
if (m && m[2] !== undefined && (itemIndent < 0 || indent <= itemIndent)) {
|
|
422
|
-
cur = new Map();
|
|
423
|
-
items.push(cur);
|
|
424
|
-
itemIndent = indent;
|
|
425
|
-
last = m[3].toLowerCase();
|
|
426
|
-
cur.set(last, (m[4] ?? "").trim());
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
if (!cur || indent <= itemIndent) {
|
|
430
|
-
cur = null;
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
if (m) {
|
|
434
|
-
last = m[3].toLowerCase();
|
|
435
|
-
if (!cur.has(last))
|
|
436
|
-
cur.set(last, (m[4] ?? "").trim());
|
|
437
|
-
else
|
|
438
|
-
last = "";
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
if (last)
|
|
442
|
-
cur.set(last, `${cur.get(last) ?? ""} ${line.trim()}`.trim());
|
|
443
|
-
}
|
|
444
|
-
return items;
|
|
445
|
-
}
|
|
446
|
-
function lineRange(v) {
|
|
447
|
-
const m = /^\s*`?L?(\d+)(?:\s*[-–]\s*L?(\d+))?/.exec(v ?? "");
|
|
448
|
-
if (!m)
|
|
449
|
-
return null;
|
|
450
|
-
const a = Number(m[1]);
|
|
451
|
-
const b = m[2] !== undefined ? Number(m[2]) : a;
|
|
452
|
-
if (!(a > 0) || b < a)
|
|
453
|
-
return null;
|
|
454
|
-
return { start: a, end: b };
|
|
455
|
-
}
|
|
456
|
-
function cleanPath(v, root) {
|
|
457
|
-
let p = v.trim().replace(/^`+|`+$/g, "");
|
|
458
|
-
if (root && p.startsWith(`${root.replace(/\/+$/, "")}/`))
|
|
459
|
-
p = p.slice(root.replace(/\/+$/, "").length + 1);
|
|
460
|
-
return p.replace(/^\.?\/+/, "").replace(/:(\d+)(?:-\d+)?$/, "");
|
|
461
|
-
}
|
|
462
|
-
function severityOf(v) {
|
|
463
|
-
const s = (v ?? "").trim().replace(/[`*]/g, "");
|
|
464
|
-
const m = /^(p[0-3])\b/i.exec(s);
|
|
465
|
-
if (m)
|
|
466
|
-
return m[1].toUpperCase();
|
|
467
|
-
return /^nit\b/i.test(s) ? "nit" : "P3";
|
|
468
|
-
}
|
|
469
|
-
function reachabilityOf(v) {
|
|
470
|
-
const r = (v ?? "")
|
|
471
|
-
.trim()
|
|
472
|
-
.replace(/[`*]/g, "")
|
|
473
|
-
.toLowerCase()
|
|
474
|
-
.split(/[\s,;.(—–-]/)[0] ?? "";
|
|
475
|
-
return REACHABILITY_VALUES.includes(r) ? r : null;
|
|
476
|
-
}
|
|
477
|
-
/** One lowercase word naming the class — the identity half of a kind. */
|
|
478
|
-
export function normalizeKind(v) {
|
|
479
|
-
const k = (v ?? "")
|
|
480
|
-
.toLowerCase()
|
|
481
|
-
.replace(/[`*]/g, "")
|
|
482
|
-
.trim()
|
|
483
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
484
|
-
.replace(/^-+|-+$/g, "");
|
|
485
|
-
return k === "" ? "unclassified" : k.slice(0, 40);
|
|
486
|
-
}
|
|
487
|
-
/** The findings of one block of text (the part before SUGGEST / DOUBTED /
|
|
488
|
-
* KILLED). */
|
|
489
|
-
function readFindings(text, root) {
|
|
490
|
-
const { read, dropped } = readFindingItems(text, root);
|
|
491
|
-
return { candidates: read.map((r) => r.candidate), dropped };
|
|
492
|
-
}
|
|
493
|
-
/** `readFindings`, each candidate beside the raw fields it was read from. */
|
|
494
|
-
function readFindingItems(text, root) {
|
|
495
|
-
const read = [];
|
|
496
|
-
let dropped = 0;
|
|
497
|
-
for (const f of parseItems(text)) {
|
|
498
|
-
const path = cleanPath(f.get("path") ?? f.get("file") ?? "", root);
|
|
499
|
-
const claim = (f.get("claim") ?? "").trim();
|
|
500
|
-
const range = lineRange(f.get("line")) ?? lineRange(/:(\d+(?:-\d+)?)`?$/.exec(f.get("path") ?? "")?.[1]);
|
|
501
|
-
if (path === "" || claim === "" || range === null) {
|
|
502
|
-
dropped++;
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
const conf = Number((f.get("confidence") ?? "").trim());
|
|
506
|
-
const opt = (k) => {
|
|
507
|
-
const v = f.get(k)?.trim();
|
|
508
|
-
return v ? v : null;
|
|
509
|
-
};
|
|
510
|
-
const said = severityOf(f.get("severity"));
|
|
511
|
-
const reachability = reachabilityOf(f.get("reachability"));
|
|
512
|
-
const severity = reachabilityCappedSeverity(said, reachability);
|
|
513
|
-
const candidate = {
|
|
514
|
-
path,
|
|
515
|
-
...range,
|
|
516
|
-
kind: normalizeKind(f.get("kind")),
|
|
517
|
-
severity,
|
|
518
|
-
confidence: (f.get("confidence") ?? "").trim() !== "" && Number.isFinite(conf) ? conf : null,
|
|
519
|
-
reachability,
|
|
520
|
-
capped_from: severity === said ? null : said,
|
|
521
|
-
claim,
|
|
522
|
-
evidence: opt("evidence"),
|
|
523
|
-
// A nit's remedy rides `request:` (the findings contract); it is the
|
|
524
|
-
// candidate's fix when no `fix:` is written.
|
|
525
|
-
fix: opt("fix") ?? opt("request"),
|
|
526
|
-
detail: opt("detail"),
|
|
527
|
-
};
|
|
528
|
-
read.push({ candidate, fields: f });
|
|
529
|
-
}
|
|
530
|
-
return { read, dropped };
|
|
531
|
-
}
|
|
532
|
-
const BLOCK_RE = /^\s*(SUGGEST|DOUBTED|KILLED)\b.*$/m;
|
|
533
|
-
/** The text before the first trailing block (SUGGEST / DOUBTED / KILLED). */
|
|
534
|
-
function findingsPart(text) {
|
|
535
|
-
const m = BLOCK_RE.exec(text);
|
|
536
|
-
return m ? text.slice(0, m.index) : text;
|
|
537
|
-
}
|
|
538
|
-
/** The body of one named trailing block, up to the next one. */
|
|
539
|
-
function blockBody(text, name) {
|
|
540
|
-
const re = new RegExp(`^\\s*${name}\\b(.*)$`, "m");
|
|
541
|
-
const m = re.exec(text);
|
|
542
|
-
if (!m)
|
|
543
|
-
return null;
|
|
544
|
-
const rest = text.slice(m.index + m[0].length);
|
|
545
|
-
const next = BLOCK_RE.exec(rest);
|
|
546
|
-
return `${m[1] ?? ""}\n${next ? rest.slice(0, next.index) : rest}`;
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* Read a `pass-scan` answer: its findings (line anchors required — a scan
|
|
550
|
-
* finding without lines is dropped, per the scan-mode anchor rule) and its
|
|
551
|
-
* deep-lens suggestions (lenses from `deepLenses` only, files as given — the
|
|
552
|
-
* caller checks they exist; at most `maxSuggestions`, default
|
|
553
|
-
* `MAX_SUGGESTIONS`). `root` strips an absolute tree prefix a lens wrote by
|
|
554
|
-
* mistake.
|
|
555
|
-
*/
|
|
556
|
-
export function parseScanAnswer(text, opts = {}) {
|
|
557
|
-
const root = opts.root ?? null;
|
|
558
|
-
const lenses = opts.deepLenses ?? SCAN_DEEP_LENSES;
|
|
559
|
-
const cap = opts.maxSuggestions ?? MAX_SUGGESTIONS;
|
|
560
|
-
const head = findingsPart(text);
|
|
561
|
-
const { candidates, dropped } = readFindings(head, root);
|
|
562
|
-
const none = candidates.length === 0 && /^\s*NONE\s*$/m.test(head);
|
|
563
|
-
const suggestions = [];
|
|
564
|
-
let suggestionsDropped = 0;
|
|
565
|
-
const body = blockBody(text, "SUGGEST");
|
|
566
|
-
if (body !== null && !/^\s*NONE\b/.test(body)) {
|
|
567
|
-
for (const s of parseItems(body)) {
|
|
568
|
-
const lens = (s.get("lens") ?? "").trim().replace(/[`*]/g, "");
|
|
569
|
-
const files = (s.get("files") ?? s.get("file") ?? "")
|
|
570
|
-
.split(",")
|
|
571
|
-
.map((p) => cleanPath(p, root))
|
|
572
|
-
.filter((p) => p !== "");
|
|
573
|
-
const reason = (s.get("reason") ?? "").trim();
|
|
574
|
-
if (!lenses.includes(lens) ||
|
|
575
|
-
files.length === 0 ||
|
|
576
|
-
reason === "" ||
|
|
577
|
-
suggestions.length >= cap) {
|
|
578
|
-
suggestionsDropped++;
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
suggestions.push({ lens, files: [...new Set(files)], reason });
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
return { candidates, dropped, suggestions, suggestions_dropped: suggestionsDropped, none };
|
|
585
|
-
}
|
|
586
|
-
/** The slugs a doubt may name as its class — the ONE class list. */
|
|
587
|
-
export const SCAN_FALSE_POSITIVE_SLUGS = SCAN_FALSE_POSITIVE_CLASSES.map((c) => c.slug);
|
|
588
|
-
/** A `path:line` reference — a file with an extension, a colon, a line
|
|
589
|
-
* (`src/tui/app.ts:369`, `webhook.ts:88`, `a.py:3-7`); or an extensionless
|
|
590
|
-
* file named with its directory (`scripts/qcr:12`) or by one of the common
|
|
591
|
-
* extensionless build/CI names (`Dockerfile:5`, `Makefile:30`) — a scan
|
|
592
|
-
* reviews any repo, and those files are anchored like any other. */
|
|
593
|
-
const PATH_LINE = /(?:[A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*|[A-Za-z0-9_.][\w.-]*(?:\/[\w.-]+)+|\b(?:Dockerfile|Containerfile|Makefile|GNUmakefile|Jenkinsfile|Procfile|Gemfile|Rakefile|Vagrantfile|Brewfile|Justfile|justfile|Earthfile|Tiltfile))(?:\.[\w-]+)?:\d+/;
|
|
594
|
-
/**
|
|
595
|
-
* Whether a skeptic's scenario is anchored: it cites at least one `path:line`
|
|
596
|
-
* of the path it traced. A `confirmed` scenario told as a prose chain of
|
|
597
|
-
* names and arrows with no `path:line` hides the hop the skeptic did not
|
|
598
|
-
* read — that hop is exactly the one it cannot cite. The engine checks that
|
|
599
|
-
* structure; the rest of the tightening lives in SCAN_SKEPTIC_ADDENDUM.
|
|
600
|
-
*/
|
|
601
|
-
export function scenarioIsAnchored(scenario) {
|
|
602
|
-
return PATH_LINE.test(scenario);
|
|
603
|
-
}
|
|
604
|
-
/** One `- <lens> <path:line> — <word>: [<class>] <reason>` line per entry of
|
|
605
|
-
* a DOUBTED or KILLED block; a line that does not parse is skipped. */
|
|
606
|
-
function judgementLines(body, root) {
|
|
607
|
-
const out = [];
|
|
608
|
-
if (body === null)
|
|
609
|
-
return out;
|
|
610
|
-
for (const raw of body.split("\n")) {
|
|
611
|
-
const m = /^\s*-\s+(\S+)\s+(\S+?)(?::(\d+)(?:-\d+)?)?\s+[—–-]+\s+([a-z-]+):?\s*(.*)$/i.exec(raw);
|
|
612
|
-
if (!m)
|
|
613
|
-
continue;
|
|
614
|
-
let reason = (m[5] ?? "").trim();
|
|
615
|
-
let cls = null;
|
|
616
|
-
const c = /^\[\s*`?([a-z-]+)`?\s*\]\s*(.*)$/i.exec(reason);
|
|
617
|
-
if (c && SCAN_FALSE_POSITIVE_SLUGS.includes(c[1].toLowerCase())) {
|
|
618
|
-
cls = c[1].toLowerCase();
|
|
619
|
-
reason = (c[2] ?? "").trim();
|
|
620
|
-
}
|
|
621
|
-
out.push({
|
|
622
|
-
lens: m[1],
|
|
623
|
-
path: cleanPath(m[2], root),
|
|
624
|
-
line: m[3] !== undefined ? Number(m[3]) : null,
|
|
625
|
-
word: m[4].toLowerCase(),
|
|
626
|
-
reason,
|
|
627
|
-
class: cls,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
return out;
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Read the validator's answer over a scan case — the scan's skeptic: the
|
|
634
|
-
* survivors (its findings block — they carry the lens's anchors), each with
|
|
635
|
-
* its tier (`validity: confirmed` with a `scenario:` citing a `path:line`,
|
|
636
|
-
* else `likely`), its DOUBTED lines,
|
|
637
|
-
* `- <lens> <path:line> — <word>: [<class>] <reason>`, and its KILLED lines
|
|
638
|
-
* in the same shape. In a scan nothing is killed: the caller stores a KILLED
|
|
639
|
-
* line as a doubt exactly like a DOUBTED one (the frozen validator's kill
|
|
640
|
-
* paths — below-bar, restated, duplicate, a confidence under 0.5 — become a
|
|
641
|
-
* doubtful mark, never a deletion).
|
|
642
|
-
*/
|
|
643
|
-
export function parseValidatorAnswer(text, root) {
|
|
644
|
-
const head = findingsPart(text);
|
|
645
|
-
const { read, dropped } = readFindingItems(head, root);
|
|
646
|
-
const survivors = read.map(({ candidate, fields }) => {
|
|
647
|
-
const said = (fields.get("validity") ?? "").trim().replace(/[`*]/g, "").toLowerCase();
|
|
648
|
-
const scenario = fields.get("scenario")?.trim() || null;
|
|
649
|
-
const confirmed = said.startsWith("confirmed") && scenario !== null && scenarioIsAnchored(scenario);
|
|
650
|
-
return { ...candidate, validity: confirmed ? "confirmed" : "likely", scenario };
|
|
651
|
-
});
|
|
652
|
-
const body = blockBody(text, "DOUBTED");
|
|
653
|
-
const killedBody = blockBody(text, "KILLED");
|
|
654
|
-
const doubted = judgementLines(body, root);
|
|
655
|
-
const killed = judgementLines(killedBody, root);
|
|
656
|
-
// A validator's answer: survivors, a NONE, or a DOUBTED/KILLED block.
|
|
657
|
-
// Prose with none of them (a tool error told in words) judges nothing.
|
|
658
|
-
const answered = survivors.length > 0 ||
|
|
659
|
-
doubted.length > 0 ||
|
|
660
|
-
/^\s*NONE\s*$/m.test(head) ||
|
|
661
|
-
killedBody !== null ||
|
|
662
|
-
body !== null;
|
|
663
|
-
return { survivors, dropped, doubted, killed, answered };
|
|
664
|
-
}
|
|
665
|
-
// ---- identity ----------------------------------------------------------------------
|
|
666
|
-
/** The anchored code, whitespace-normalised: each line's runs of whitespace
|
|
667
|
-
* collapsed, blank lines dropped — so a re-indent or a reflow is the same
|
|
668
|
-
* code, and a changed token is not. */
|
|
669
|
-
export function normalizeCode(lines) {
|
|
670
|
-
return lines
|
|
671
|
-
.map((l) => l.trim().replace(/\s+/g, " "))
|
|
672
|
-
.filter((l) => l !== "")
|
|
673
|
-
.join("\n");
|
|
674
|
-
}
|
|
675
|
-
/** Lines `start..end` (1-based, inclusive) of `text`, or null when the range
|
|
676
|
-
* is not inside the file. */
|
|
677
|
-
export function anchoredLines(text, start, end) {
|
|
678
|
-
const lines = text.split("\n");
|
|
679
|
-
if (text.endsWith("\n"))
|
|
680
|
-
lines.pop();
|
|
681
|
-
if (start < 1 || end < start || end > lines.length)
|
|
682
|
-
return null;
|
|
683
|
-
return lines.slice(start - 1, end);
|
|
684
|
-
}
|
|
685
|
-
/** Whitespace collapsed and trimmed, for the quote check. */
|
|
686
|
-
const squash = (t) => t.replace(/\s+/g, " ").trim();
|
|
687
|
-
/** A fragment long enough to mean something: four characters or more with a
|
|
688
|
-
* letter or digit in it (never a lone `}` or `)`). */
|
|
689
|
-
const quotable = (t) => t.length >= 4 && /[A-Za-z0-9]/.test(t);
|
|
690
|
-
/**
|
|
691
|
-
* Does a finding's evidence quote its anchored code? True when the evidence
|
|
692
|
-
* contains a whole anchored line (whitespace-normalised), or a quoted
|
|
693
|
-
* fragment of it — a `backticked` or "double-quoted" run that sits inside one
|
|
694
|
-
* of the anchored lines. An anchor with nothing quotable in it (braces, blank
|
|
695
|
-
* lines) cannot be checked and is not held against the finding. A finding
|
|
696
|
-
* whose evidence quotes only code ABOVE or BELOW its anchor — the function
|
|
697
|
-
* signature for a dereference twelve lines down — is exactly the misanchor
|
|
698
|
-
* this catches.
|
|
699
|
-
*/
|
|
700
|
-
export function evidenceQuotesAnchor(evidence, anchored) {
|
|
701
|
-
const lines = anchored.map(squash).filter(quotable);
|
|
702
|
-
if (lines.length === 0)
|
|
703
|
-
return true;
|
|
704
|
-
if (evidence === null)
|
|
705
|
-
return false;
|
|
706
|
-
const ev = squash(evidence);
|
|
707
|
-
if (lines.some((l) => ev.includes(l)))
|
|
708
|
-
return true;
|
|
709
|
-
for (const m of evidence.matchAll(/`([^`]+)`|"([^"]+)"/g)) {
|
|
710
|
-
const frag = squash(m[1] ?? m[2] ?? "");
|
|
711
|
-
if (quotable(frag) && lines.some((l) => l.includes(frag)))
|
|
712
|
-
return true;
|
|
713
|
-
}
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* A scan finding's fingerprint: file + lens + kind + a hash of the
|
|
718
|
-
* anchored code, whitespace-normalised. Stable across a re-scan that only
|
|
719
|
-
* moved the code or re-indented it; different when the code itself changed.
|
|
720
|
-
*/
|
|
721
|
-
export function findingFingerprint(f) {
|
|
722
|
-
const code = createHash("sha256").update(f.code).digest("hex");
|
|
723
|
-
return createHash("sha256").update([f.path, f.lens, f.kind, code].join("\0")).digest("hex");
|
|
724
|
-
}
|
|
1
|
+
// Moved to the plugin (packages/pi-plugin/src/scan/scan.ts), the scan engine's
|
|
2
|
+
// home: this name stays for the app's importers.
|
|
3
|
+
export * from "@quickchatai/pi-quickreview/src/scan/scan.js";
|
|
725
4
|
//# sourceMappingURL=scan.js.map
|