@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/src/setup/scantree.ts
CHANGED
|
@@ -1,259 +1,3 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
// "Scan my local checkout at a sha I choose" means the tree AT THAT SHA, not
|
|
5
|
-
// whatever the working copy holds: the file list and every content hash come
|
|
6
|
-
// from git's own objects (`ls-tree` — the blob hash IS the ledger's content
|
|
7
|
-
// hash), and the files the lenses read are materialised from exactly those
|
|
8
|
-
// blobs (`git cat-file --batch`, raw bytes) into
|
|
9
|
-
// `<state home>/scan/tree/<sha>/`. Not `git archive`: it honours in-tree
|
|
10
|
-
// `export-ignore`/`export-subst`, so a listed file could be missing or read
|
|
11
|
-
// as bytes its blob hash never named; and it extracts symlinks. Only the
|
|
12
|
-
// listed regular files are written, so nothing in the tree is a link. The
|
|
13
|
-
// checkout itself is never written — the companion mounts it `:ro` — and no
|
|
14
|
-
// credential is involved: the objects are local.
|
|
15
|
-
//
|
|
16
|
-
// One materialised tree is kept per sha; opening another removes the earlier
|
|
17
|
-
// ones except those `keep` names (the one a running scan is reading). A tree
|
|
18
|
-
// is complete only once its marker is written, so an extraction cut short is
|
|
19
|
-
// redone rather than trusted. The marker is a SIBLING of the tree
|
|
20
|
-
// (`<sha>.complete`), never a path inside it: a name inside the tree is one
|
|
21
|
-
// the commit can occupy.
|
|
22
|
-
import { execFileSync } from "node:child_process";
|
|
23
|
-
import {
|
|
24
|
-
existsSync,
|
|
25
|
-
mkdirSync,
|
|
26
|
-
readdirSync,
|
|
27
|
-
readFileSync,
|
|
28
|
-
rmSync,
|
|
29
|
-
statSync,
|
|
30
|
-
writeFileSync,
|
|
31
|
-
} from "node:fs";
|
|
32
|
-
import { dirname, isAbsolute, join, normalize, relative } from "node:path";
|
|
33
|
-
import { inRoot, MAX_READ_BYTES } from "./adapters.js";
|
|
34
|
-
import type { ScanSource, ScanTree } from "./scanrun.js";
|
|
35
|
-
import type { UnreadableReason } from "./scan.js";
|
|
36
|
-
import { isBinaryContent, textLineCount } from "./textsniff.js";
|
|
37
|
-
import type { TreeFile } from "./scan.js";
|
|
38
|
-
|
|
39
|
-
/** Where materialised trees live, under the state home. */
|
|
40
|
-
export const SCAN_TREES_DIR = "scan/tree";
|
|
41
|
-
const COMPLETE_SUFFIX = ".complete";
|
|
42
|
-
/** Blobs, and their bytes, per `cat-file --batch` call — bound one call's
|
|
43
|
-
* output buffer (a single larger blob is a call of its own). */
|
|
44
|
-
const BLOBS_PER_BATCH = 256;
|
|
45
|
-
const BYTES_PER_BATCH = 64 * 1024 * 1024;
|
|
46
|
-
const SHA_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
47
|
-
/** A full commit sha (SHA-1 or SHA-256) — the only spelling a tree is opened at. */
|
|
48
|
-
export const isFullSha = (s: string): boolean => SHA_RE.test(s);
|
|
49
|
-
|
|
50
|
-
/** `treesDir` (relative to the state home) is where this source keeps its
|
|
51
|
-
* trees — the scan's own by default; another reader of trees (the PR
|
|
52
|
-
* replay) passes its own, so neither prunes the other's.
|
|
53
|
-
* `batchBytes` bounds one `cat-file --batch` call's output (tests shrink it). */
|
|
54
|
-
export function gitScanSource(
|
|
55
|
-
repoRoot: string,
|
|
56
|
-
stateRoot: string,
|
|
57
|
-
opts: { treesDir?: string; batchBytes?: number } = {},
|
|
58
|
-
): ScanSource {
|
|
59
|
-
const batchBytes = opts.batchBytes ?? BYTES_PER_BATCH;
|
|
60
|
-
// Why the last git call failed — its stderr's first line, or the error
|
|
61
|
-
// (a buffer overrun, git missing) — so a refusal names the cause.
|
|
62
|
-
let lastFailure = "";
|
|
63
|
-
const failed = (err: unknown): null => {
|
|
64
|
-
const e = err as { stderr?: Buffer | string; message?: string };
|
|
65
|
-
const stderr = (
|
|
66
|
-
Buffer.isBuffer(e.stderr) ? e.stderr.toString("utf8") : (e.stderr ?? "")
|
|
67
|
-
).trim();
|
|
68
|
-
lastFailure = (stderr.split("\n")[0] || e.message || String(err)).slice(0, 300);
|
|
69
|
-
return null;
|
|
70
|
-
};
|
|
71
|
-
const because = (): string => (lastFailure ? ` (${lastFailure})` : "");
|
|
72
|
-
const git = (args: string[], maxBuffer = 64 * 1024 * 1024): string | null => {
|
|
73
|
-
try {
|
|
74
|
-
return execFileSync("git", ["-c", `safe.directory=${repoRoot}`, "-C", repoRoot, ...args], {
|
|
75
|
-
encoding: "utf8",
|
|
76
|
-
maxBuffer,
|
|
77
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
78
|
-
});
|
|
79
|
-
} catch (err) {
|
|
80
|
-
return failed(err);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
const gitBytes = (args: string[], input: string, maxBuffer: number): Buffer | null => {
|
|
84
|
-
try {
|
|
85
|
-
return execFileSync("git", ["-c", `safe.directory=${repoRoot}`, "-C", repoRoot, ...args], {
|
|
86
|
-
input,
|
|
87
|
-
maxBuffer,
|
|
88
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
89
|
-
});
|
|
90
|
-
} catch (err) {
|
|
91
|
-
return failed(err);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
/** Write every listed file's blob, raw, under `root`. False when git
|
|
95
|
-
* cannot produce one — the tree is then not complete. */
|
|
96
|
-
const materialise = (root: string, files: TreeFile[]): boolean => {
|
|
97
|
-
const pathsOf = new Map<string, string[]>();
|
|
98
|
-
for (const f of files) pathsOf.set(f.blob, [...(pathsOf.get(f.blob) ?? []), f.path]);
|
|
99
|
-
const blobs = [...pathsOf.keys()];
|
|
100
|
-
if (blobs.length === 0) return true;
|
|
101
|
-
// Sizes first, so one call's output is bounded by BYTES as well as by
|
|
102
|
-
// count: a repository of large assets must not need one huge buffer.
|
|
103
|
-
const checked = gitBytes(
|
|
104
|
-
["cat-file", "--batch-check"],
|
|
105
|
-
blobs.join("\n") + "\n",
|
|
106
|
-
64 * 1024 * 1024,
|
|
107
|
-
);
|
|
108
|
-
if (checked === null) return false;
|
|
109
|
-
const sizeOf = new Map<string, number>();
|
|
110
|
-
for (const line of checked.toString("utf8").split("\n")) {
|
|
111
|
-
const m = /^([0-9a-f]+) blob (\d+)$/.exec(line);
|
|
112
|
-
if (m) sizeOf.set(m[1]!, Number(m[2]));
|
|
113
|
-
}
|
|
114
|
-
const chunks: Array<{ blobs: string[]; bytes: number }> = [];
|
|
115
|
-
for (const blob of blobs) {
|
|
116
|
-
const size = sizeOf.get(blob);
|
|
117
|
-
if (size === undefined) return false;
|
|
118
|
-
const last = chunks[chunks.length - 1];
|
|
119
|
-
if (last && last.blobs.length < BLOBS_PER_BATCH && last.bytes + size <= batchBytes) {
|
|
120
|
-
last.blobs.push(blob);
|
|
121
|
-
last.bytes += size;
|
|
122
|
-
} else chunks.push({ blobs: [blob], bytes: size });
|
|
123
|
-
}
|
|
124
|
-
for (const { blobs: chunk, bytes } of chunks) {
|
|
125
|
-
// Each blob's header and trailing newline on top of its bytes.
|
|
126
|
-
const out = gitBytes(
|
|
127
|
-
["cat-file", "--batch"],
|
|
128
|
-
chunk.join("\n") + "\n",
|
|
129
|
-
bytes + chunk.length * 128 + 1024,
|
|
130
|
-
);
|
|
131
|
-
if (out === null) return false;
|
|
132
|
-
let at = 0;
|
|
133
|
-
for (const blob of chunk) {
|
|
134
|
-
const nl = out.indexOf(10, at);
|
|
135
|
-
if (nl < 0) return false;
|
|
136
|
-
const m = /^([0-9a-f]+) blob (\d+)$/.exec(out.subarray(at, nl).toString("utf8"));
|
|
137
|
-
if (!m || m[1] !== blob) return false;
|
|
138
|
-
const size = Number(m[2]);
|
|
139
|
-
const body = out.subarray(nl + 1, nl + 1 + size);
|
|
140
|
-
if (body.length !== size) return false;
|
|
141
|
-
at = nl + 1 + size + 1;
|
|
142
|
-
for (const rel of pathsOf.get(blob)!) {
|
|
143
|
-
const full = normalize(join(root, rel));
|
|
144
|
-
const back = relative(root, full);
|
|
145
|
-
if (isAbsolute(rel) || !inRoot(back)) continue;
|
|
146
|
-
mkdirSync(dirname(full), { recursive: true });
|
|
147
|
-
writeFileSync(full, body);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return true;
|
|
152
|
-
};
|
|
153
|
-
const treesDir = join(stateRoot, opts.treesDir ?? SCAN_TREES_DIR);
|
|
154
|
-
const list = (sha: string): TreeFile[] | null => {
|
|
155
|
-
if (!SHA_RE.test(sha)) return null;
|
|
156
|
-
const listed = git(["ls-tree", "-r", "-z", "--full-tree", sha], 512 * 1024 * 1024);
|
|
157
|
-
if (listed === null) return null;
|
|
158
|
-
const files: TreeFile[] = [];
|
|
159
|
-
for (const rec of listed.split("\0")) {
|
|
160
|
-
// `<mode> <type> <object>\t<path>` — regular files only: no submodule
|
|
161
|
-
// (`commit`), no symlink (120000) a lens could follow out of the tree.
|
|
162
|
-
const m = /^(\d{6}) (\w+) ([0-9a-f]+)\t(.+)$/s.exec(rec);
|
|
163
|
-
if (!m || m[2] !== "blob" || m[1] === "120000") continue;
|
|
164
|
-
files.push({ path: m[4]!, blob: m[3]! });
|
|
165
|
-
}
|
|
166
|
-
return files;
|
|
167
|
-
};
|
|
168
|
-
return {
|
|
169
|
-
location: repoRoot,
|
|
170
|
-
resolve: (ref) => {
|
|
171
|
-
if (!/^[A-Za-z0-9._/@^~-]+$/.test(ref) || ref.startsWith("-")) return null;
|
|
172
|
-
const sha = git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`])?.trim() ?? "";
|
|
173
|
-
return SHA_RE.test(sha) ? sha : null;
|
|
174
|
-
},
|
|
175
|
-
open: (sha, keep) => {
|
|
176
|
-
if (!SHA_RE.test(sha))
|
|
177
|
-
return { ok: false, reason: `not a commit sha: ${JSON.stringify(sha)}` };
|
|
178
|
-
lastFailure = "";
|
|
179
|
-
const files = list(sha);
|
|
180
|
-
if (files === null)
|
|
181
|
-
return { ok: false, reason: `git cannot list that commit's tree${because()}` };
|
|
182
|
-
const root = join(treesDir, sha);
|
|
183
|
-
const marker = `${root}${COMPLETE_SUFFIX}`;
|
|
184
|
-
try {
|
|
185
|
-
mkdirSync(treesDir, { recursive: true });
|
|
186
|
-
for (const other of readdirSync(treesDir)) {
|
|
187
|
-
const owner = other.endsWith(COMPLETE_SUFFIX)
|
|
188
|
-
? other.slice(0, -COMPLETE_SUFFIX.length)
|
|
189
|
-
: other;
|
|
190
|
-
if (owner !== sha && !keep.includes(owner))
|
|
191
|
-
rmSync(join(treesDir, other), { recursive: true, force: true });
|
|
192
|
-
}
|
|
193
|
-
if (!existsSync(marker)) {
|
|
194
|
-
rmSync(root, { recursive: true, force: true });
|
|
195
|
-
mkdirSync(root, { recursive: true });
|
|
196
|
-
if (!materialise(root, files))
|
|
197
|
-
return { ok: false, reason: `git cannot read that commit's blobs${because()}` };
|
|
198
|
-
writeFileSync(marker, `${sha}\n`);
|
|
199
|
-
}
|
|
200
|
-
} catch (err) {
|
|
201
|
-
return {
|
|
202
|
-
ok: false,
|
|
203
|
-
reason: `cannot materialise the tree: ${err instanceof Error ? err.message : String(err)}`,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
return { ok: true, tree: treeAt(sha, root, files) };
|
|
207
|
-
},
|
|
208
|
-
list,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** A materialised tree as the runner reads it: text within the reader's
|
|
213
|
-
* size cap, never a binary (`isBinaryContent` — one stray NUL in a source
|
|
214
|
-
* file is still text), never a path that leaves the root. What it cannot
|
|
215
|
-
* read it says why (`whyUnreadable`), so the plan REPORTS a skipped binary
|
|
216
|
-
* rather than dropping it silently. */
|
|
217
|
-
export function treeAt(sha: string, root: string, files: TreeFile[]): ScanTree {
|
|
218
|
-
const why = new Map<string, UnreadableReason>();
|
|
219
|
-
const read = (rel: string): string | null => {
|
|
220
|
-
if (isAbsolute(rel)) return null;
|
|
221
|
-
const full = normalize(join(root, rel));
|
|
222
|
-
const back = relative(root, full);
|
|
223
|
-
if (!inRoot(back)) return null;
|
|
224
|
-
try {
|
|
225
|
-
const st = statSync(full);
|
|
226
|
-
if (!st.isFile()) return null;
|
|
227
|
-
if (st.size > MAX_READ_BYTES) {
|
|
228
|
-
why.set(rel, "too-big");
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
const buf = readFileSync(full);
|
|
232
|
-
if (isBinaryContent(buf, rel)) {
|
|
233
|
-
why.set(rel, "binary");
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
return buf.toString("utf8");
|
|
237
|
-
} catch {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
const cache = new Map<string, number | null>();
|
|
242
|
-
return {
|
|
243
|
-
sha,
|
|
244
|
-
root,
|
|
245
|
-
files,
|
|
246
|
-
read,
|
|
247
|
-
whyUnreadable: (rel) => {
|
|
248
|
-
if (read(rel) !== null) return null;
|
|
249
|
-
return why.get(rel) ?? "unreadable";
|
|
250
|
-
},
|
|
251
|
-
lines: (rel) => {
|
|
252
|
-
if (cache.has(rel)) return cache.get(rel)!;
|
|
253
|
-
const text = read(rel);
|
|
254
|
-
const n = text === null ? null : textLineCount(text);
|
|
255
|
-
cache.set(rel, n);
|
|
256
|
-
return n;
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
}
|
|
1
|
+
// Moved to the plugin (packages/pi-plugin/src/scan/scantree.ts), the scan engine's
|
|
2
|
+
// home: this name stays for the app's importers.
|
|
3
|
+
export * from "@quickchatai/pi-quickreview/src/scan/scantree.js";
|
|
@@ -1,347 +1,3 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
// setup engine — tests/single-shepherd.test.ts) can read the proposal and
|
|
5
|
-
// ask for its start. The click's whole TUI half lives here
|
|
6
|
-
// (`clickStartStage2`, `stage2ClickOutcome`); the [8] scan tab's plan pane
|
|
7
|
-
// (`P`, then ⏎) is its button.
|
|
8
|
-
//
|
|
9
|
-
// Stage 2 starts one of two ways, and neither keeps an approval record:
|
|
10
|
-
//
|
|
11
|
-
// * the operator's word — the companion calls `scan_stage2_start` with the
|
|
12
|
-
// proposal's key (and, for a narrowed "do it", the `rows` it may run —
|
|
13
|
-
// the same `keptRows` rule as the click's narrowing below);
|
|
14
|
-
// * a TUI click — the TUI writes `STAGE2_START_FILE` naming the key of the
|
|
15
|
-
// proposal it showed; the companion's scan belt picks it up within
|
|
16
|
-
// seconds and starts exactly that proposal, answering in
|
|
17
|
-
// `STAGE2_RESULT_FILE`.
|
|
18
|
-
//
|
|
19
|
-
// Either way the start quotes a key, and the engine re-plans before it runs:
|
|
20
|
-
// a proposal that moved since it was shown (a new proposal, a new commit, a
|
|
21
|
-
// price that changed) is refused, so nothing runs that the operator did not
|
|
22
|
-
// see priced. A request is answered once — the result names the request it
|
|
23
|
-
// answered — so a click is never replayed.
|
|
24
|
-
//
|
|
25
|
-
// The click may narrow the plan (space toggles a row off in the plan pane;
|
|
26
|
-
// the operator's word narrows the same way through `scan_stage2_start`'s
|
|
27
|
-
// `rows`): the request then names the runnable rows the operator kept, and
|
|
28
|
-
// the run is exactly those rows, stopped hard at their total — the sum
|
|
29
|
-
// the pane showed. It can only narrow: a row that was not runnable in the
|
|
30
|
-
// proposal (covered, over the budget) can never be named, so nothing runs
|
|
31
|
-
// that was not shown priced and runnable.
|
|
32
|
-
//
|
|
33
|
-
// Files, all in the state home: `scan/stage2.json` (the latest proposal, the
|
|
34
|
-
// engine's), `scan/stage2-start.json` (the TUI's request),
|
|
35
|
-
// `scan/stage2-start-result.json` (the engine's answer).
|
|
36
|
-
import { join } from "node:path";
|
|
37
|
-
import { readTextIfPresent, writeAtomic } from "../channelfs.js";
|
|
38
|
-
import type { ScanDepth } from "../config.js";
|
|
39
|
-
import type { RowPrice, Stage2Floor, Stage2Row, Stage2RowRequest } from "./stage2.js";
|
|
40
|
-
|
|
41
|
-
export const STAGE2_FILE = "scan/stage2.json";
|
|
42
|
-
export const STAGE2_START_FILE = "scan/stage2-start.json";
|
|
43
|
-
export const STAGE2_RESULT_FILE = "scan/stage2-start-result.json";
|
|
44
|
-
export const STAGE2_VERSION = 1;
|
|
45
|
-
|
|
46
|
-
/** The latest proposal, as stored — what the TUI shows and a start quotes. */
|
|
47
|
-
export interface StoredStage2Proposal {
|
|
48
|
-
key: string;
|
|
49
|
-
sha: string;
|
|
50
|
-
at: string;
|
|
51
|
-
/** What the companion asked for — the start re-plans from exactly this. */
|
|
52
|
-
request: Stage2RowRequest[];
|
|
53
|
-
budget_usd: number | null;
|
|
54
|
-
/** The plan it priced. */
|
|
55
|
-
rows: Stage2Row[];
|
|
56
|
-
/** The hard stop: the runnable rows' total. */
|
|
57
|
-
cap_usd: number;
|
|
58
|
-
runnable: number;
|
|
59
|
-
over_budget_usd: number;
|
|
60
|
-
validator: boolean;
|
|
61
|
-
/** The depth it was made at — a start re-plans at this depth, whatever
|
|
62
|
-
* `[scan] depth` says by then. Absent = standard (a proposal stored before
|
|
63
|
-
* depths). */
|
|
64
|
-
depth?: ScanDepth;
|
|
65
|
-
/** A deep proposal's floor: its lenses, and what it left out. */
|
|
66
|
-
floor?: Stage2Floor | null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** The depth a stored proposal was made at. */
|
|
70
|
-
export const proposalDepth = (p: Pick<StoredStage2Proposal, "depth">): ScanDepth =>
|
|
71
|
-
p.depth === "deep" ? "deep" : "standard";
|
|
72
|
-
|
|
73
|
-
export interface Stage2File {
|
|
74
|
-
version: number;
|
|
75
|
-
proposal: StoredStage2Proposal | null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface Stage2StartRequest {
|
|
79
|
-
/** Unique per click; the result names it. */
|
|
80
|
-
id: string;
|
|
81
|
-
key: string;
|
|
82
|
-
at: string;
|
|
83
|
-
by: "tui";
|
|
84
|
-
/** The runnable rows the operator kept (their ids); absent = all of them. */
|
|
85
|
-
rows?: string[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface Stage2StartResult {
|
|
89
|
-
request: string;
|
|
90
|
-
key: string;
|
|
91
|
-
at: string;
|
|
92
|
-
ok: boolean;
|
|
93
|
-
/** The run it started. */
|
|
94
|
-
run: string | null;
|
|
95
|
-
errors: string[];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const clean = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
99
|
-
|
|
100
|
-
/** Parse the proposal file; throws on anything this version does not read. */
|
|
101
|
-
export function parseStage2File(text: string): Stage2File {
|
|
102
|
-
const raw = JSON.parse(text) as Partial<Stage2File> | null;
|
|
103
|
-
if (typeof raw !== "object" || raw === null || raw.version !== STAGE2_VERSION)
|
|
104
|
-
throw new Error("not a version-1 stage 2 file");
|
|
105
|
-
const p = raw.proposal ?? null;
|
|
106
|
-
if (
|
|
107
|
-
p !== null &&
|
|
108
|
-
(typeof p !== "object" ||
|
|
109
|
-
typeof p.key !== "string" ||
|
|
110
|
-
typeof p.sha !== "string" ||
|
|
111
|
-
typeof p.cap_usd !== "number" ||
|
|
112
|
-
!Array.isArray(p.rows) ||
|
|
113
|
-
!Array.isArray(p.request))
|
|
114
|
-
) {
|
|
115
|
-
throw new Error("`proposal` is not a stage 2 proposal");
|
|
116
|
-
}
|
|
117
|
-
return { version: STAGE2_VERSION, proposal: p };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Parse a start request, or null when it is not one. */
|
|
121
|
-
export function parseStartRequest(text: string): Stage2StartRequest | null {
|
|
122
|
-
try {
|
|
123
|
-
const r = JSON.parse(text) as Partial<Stage2StartRequest> | null;
|
|
124
|
-
if (
|
|
125
|
-
typeof r !== "object" ||
|
|
126
|
-
r === null ||
|
|
127
|
-
typeof r.id !== "string" ||
|
|
128
|
-
r.id === "" ||
|
|
129
|
-
typeof r.key !== "string"
|
|
130
|
-
)
|
|
131
|
-
return null;
|
|
132
|
-
if (
|
|
133
|
-
r.rows !== undefined &&
|
|
134
|
-
(!Array.isArray(r.rows) || !r.rows.every((x) => typeof x === "string"))
|
|
135
|
-
)
|
|
136
|
-
return null;
|
|
137
|
-
return {
|
|
138
|
-
id: r.id,
|
|
139
|
-
key: r.key,
|
|
140
|
-
at: typeof r.at === "string" ? r.at : "",
|
|
141
|
-
by: "tui",
|
|
142
|
-
...(r.rows !== undefined ? { rows: [...r.rows] } : {}),
|
|
143
|
-
};
|
|
144
|
-
} catch {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Parse a start result, or null when it is not one. */
|
|
150
|
-
export function parseStartResult(text: string): Stage2StartResult | null {
|
|
151
|
-
try {
|
|
152
|
-
const r = JSON.parse(text) as Partial<Stage2StartResult> | null;
|
|
153
|
-
if (
|
|
154
|
-
typeof r !== "object" ||
|
|
155
|
-
r === null ||
|
|
156
|
-
typeof r.request !== "string" ||
|
|
157
|
-
typeof r.ok !== "boolean"
|
|
158
|
-
)
|
|
159
|
-
return null;
|
|
160
|
-
return {
|
|
161
|
-
request: r.request,
|
|
162
|
-
key: typeof r.key === "string" ? r.key : "",
|
|
163
|
-
at: typeof r.at === "string" ? r.at : "",
|
|
164
|
-
ok: r.ok,
|
|
165
|
-
run: typeof r.run === "string" ? r.run : null,
|
|
166
|
-
errors: Array.isArray(r.errors)
|
|
167
|
-
? r.errors.filter((e): e is string => typeof e === "string")
|
|
168
|
-
: [],
|
|
169
|
-
};
|
|
170
|
-
} catch {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** The latest stage-2 proposal in the state home at `home`, none, or why it
|
|
176
|
-
* cannot be read (then the TUI shows UNKNOWN — never "no proposal"). */
|
|
177
|
-
export function readStage2Proposal(
|
|
178
|
-
home: string,
|
|
179
|
-
): { ok: true; proposal: StoredStage2Proposal | null } | { ok: false; error: string } {
|
|
180
|
-
try {
|
|
181
|
-
const text = readTextIfPresent(join(home, STAGE2_FILE));
|
|
182
|
-
return { ok: true, proposal: text === null ? null : parseStage2File(text).proposal };
|
|
183
|
-
} catch (err) {
|
|
184
|
-
return { ok: false, error: `${STAGE2_FILE} cannot be read: ${clean(err)}` };
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* The runnable rows a start runs: all of them, or the subset `keep` names —
|
|
190
|
-
* refused when `keep` is empty or names a row that is not runnable in the
|
|
191
|
-
* proposal (the engine applies the same rule after it re-plans).
|
|
192
|
-
*/
|
|
193
|
-
export function keptRows(
|
|
194
|
-
rows: readonly Stage2Row[],
|
|
195
|
-
keep: readonly string[] | undefined,
|
|
196
|
-
): { ok: true; rows: Stage2Row[]; cap_usd: number | null } | { ok: false; errors: string[] } {
|
|
197
|
-
const runnable = rows.filter((r) => r.status === "run");
|
|
198
|
-
if (keep === undefined) return { ok: true, rows: runnable, cap_usd: null };
|
|
199
|
-
if (keep.length === 0)
|
|
200
|
-
return { ok: false, errors: ["every row is toggled off — keep at least one"] };
|
|
201
|
-
const bad = keep.filter((id) => !runnable.some((r) => r.id === id));
|
|
202
|
-
if (bad.length > 0)
|
|
203
|
-
return { ok: false, errors: [`not a runnable row of the proposal: ${bad.join(", ")}`] };
|
|
204
|
-
const kept = runnable.filter((r) => keep.includes(r.id));
|
|
205
|
-
return { ok: true, rows: kept, cap_usd: kept.reduce((n, r) => n + r.estimate_usd, 0) };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* The TUI's click: ask the companion to start the stored proposal whose key
|
|
210
|
-
* the TUI showed — all its runnable rows, or the ones in `keep`. Refused when
|
|
211
|
-
* there is no proposal, it is no longer the one shown (`key`), or `keep`
|
|
212
|
-
* names a row that is not runnable. Writes the request; the companion starts
|
|
213
|
-
* it (or says why not) in `STAGE2_RESULT_FILE`.
|
|
214
|
-
*/
|
|
215
|
-
export function requestStage2Start(
|
|
216
|
-
home: string,
|
|
217
|
-
key: string,
|
|
218
|
-
now: Date,
|
|
219
|
-
keep?: readonly string[],
|
|
220
|
-
):
|
|
221
|
-
| {
|
|
222
|
-
ok: true;
|
|
223
|
-
request: Stage2StartRequest;
|
|
224
|
-
proposal: StoredStage2Proposal;
|
|
225
|
-
rows: number;
|
|
226
|
-
cap_usd: number;
|
|
227
|
-
}
|
|
228
|
-
| { ok: false; errors: string[] } {
|
|
229
|
-
const read = readStage2Proposal(home);
|
|
230
|
-
if (!read.ok) return { ok: false, errors: [read.error] };
|
|
231
|
-
const p = read.proposal;
|
|
232
|
-
if (p === null)
|
|
233
|
-
return { ok: false, errors: ["no stage 2 proposal — ask the companion to propose one"] };
|
|
234
|
-
if (p.key !== key) {
|
|
235
|
-
return {
|
|
236
|
-
ok: false,
|
|
237
|
-
errors: [
|
|
238
|
-
`the proposal changed since it was shown (showed ${key}, now ${p.key}) — look at the new one first`,
|
|
239
|
-
],
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
if (p.runnable === 0) return { ok: false, errors: ["the proposal has no row to run"] };
|
|
243
|
-
const kept = keptRows(p.rows, keep);
|
|
244
|
-
if (!kept.ok) return kept;
|
|
245
|
-
const request: Stage2StartRequest = {
|
|
246
|
-
id: `${now.toISOString()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
247
|
-
key,
|
|
248
|
-
at: now.toISOString(),
|
|
249
|
-
by: "tui",
|
|
250
|
-
...(keep !== undefined ? { rows: [...keep] } : {}),
|
|
251
|
-
};
|
|
252
|
-
try {
|
|
253
|
-
writeAtomic(join(home, STAGE2_START_FILE), `${JSON.stringify(request)}\n`);
|
|
254
|
-
} catch (err) {
|
|
255
|
-
return { ok: false, errors: [`the start request could not be written: ${clean(err)}`] };
|
|
256
|
-
}
|
|
257
|
-
return {
|
|
258
|
-
ok: true,
|
|
259
|
-
request,
|
|
260
|
-
proposal: p,
|
|
261
|
-
rows: kept.rows.length,
|
|
262
|
-
cap_usd: kept.cap_usd ?? p.cap_usd,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** The companion's answer to the request `id`, or null while it has not
|
|
267
|
-
* answered (or answered another one); throws when the answer file exists
|
|
268
|
-
* but cannot be read. */
|
|
269
|
-
export function readStage2StartResult(home: string, id: string): Stage2StartResult | null {
|
|
270
|
-
const text = readTextIfPresent(join(home, STAGE2_RESULT_FILE));
|
|
271
|
-
const r = text === null ? null : parseStartResult(text);
|
|
272
|
-
return r !== null && r.request === id ? r : null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ---- the TUI's click -------------------------------------------------------------
|
|
276
|
-
|
|
277
|
-
const usd = (n: number): string => `$${n.toFixed(2)}`;
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* The click: ask the companion to start the proposal whose key the TUI
|
|
281
|
-
* showed — its kept rows when the operator toggled some off. The notice line
|
|
282
|
-
* says what was asked, or why nothing was.
|
|
283
|
-
*/
|
|
284
|
-
export function clickStartStage2(
|
|
285
|
-
home: string,
|
|
286
|
-
key: string,
|
|
287
|
-
now: Date = new Date(),
|
|
288
|
-
keep?: readonly string[],
|
|
289
|
-
): { ok: true; request: string; line: string } | { ok: false; line: string } {
|
|
290
|
-
const r = requestStage2Start(home, key, now, keep);
|
|
291
|
-
if (!r.ok) return { ok: false, line: `stage 2 not started: ${r.errors.join("; ")}` };
|
|
292
|
-
return {
|
|
293
|
-
ok: true,
|
|
294
|
-
request: r.request.id,
|
|
295
|
-
line: `stage 2 asked to start — ${r.rows} row(s), hard stop ${usd(r.cap_usd)}; the companion starts it within seconds`,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** What became of the click `request`: the companion's answer, or null while
|
|
300
|
-
* it has not answered (a companion that is down starts nothing) — or that
|
|
301
|
-
* the answer cannot be read. */
|
|
302
|
-
export function stage2ClickOutcome(home: string, request: string): string | null {
|
|
303
|
-
let r: Stage2StartResult | null;
|
|
304
|
-
try {
|
|
305
|
-
r = readStage2StartResult(home, request);
|
|
306
|
-
} catch (err) {
|
|
307
|
-
// Not "no answer yet": the answer may be there, unreadable — said once,
|
|
308
|
-
// never polled for silently.
|
|
309
|
-
return `stage 2's answer cannot be read (${STAGE2_RESULT_FILE}: ${clean(err)}) — check scan_status`;
|
|
310
|
-
}
|
|
311
|
-
if (r === null) return null;
|
|
312
|
-
return r.ok ? `stage 2 started: ${r.run}` : `stage 2 refused: ${r.errors.join("; ")}`;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ---- a row's price, as the preview names it -------------------------------------
|
|
316
|
-
|
|
317
|
-
/** Whether a row's price is a measurement of the row's own lens at its own
|
|
318
|
-
* settings and depth — every other basis is an estimate. */
|
|
319
|
-
export function priceIsMeasured(p: RowPrice): boolean {
|
|
320
|
-
return p.basis === "measured" && p.across_depth !== true;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** What the preview says a row's price stands on: `measured`, or
|
|
324
|
-
* `estimate: …` and how it was made. */
|
|
325
|
-
export function priceLabel(p: RowPrice): string {
|
|
326
|
-
const label = basisLabel(p);
|
|
327
|
-
return p.across_depth
|
|
328
|
-
? `${p.basis === "measured" ? "estimate: measured" : label} — from readings at the other depth`
|
|
329
|
-
: label;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function basisLabel(p: RowPrice): string {
|
|
333
|
-
const onOff = (v: boolean): string => (v ? "on" : "off");
|
|
334
|
-
switch (p.basis) {
|
|
335
|
-
case "measured":
|
|
336
|
-
return "measured";
|
|
337
|
-
case "borrowed":
|
|
338
|
-
return `estimate: borrowed from ${p.from}`;
|
|
339
|
-
case "learned":
|
|
340
|
-
return p.learned
|
|
341
|
-
? `estimate: learned from ${p.from} with the validator ${onOff(p.learned.validator)} (×${p.learned.factor.toFixed(2)} stage 1), ` +
|
|
342
|
-
`stage 1 validator ${onOff(!p.learned.validator)}/${onOff(p.learned.validator)} ×${p.learned.skeptic.toFixed(2)}`
|
|
343
|
-
: `estimate: learned from ${p.from}`;
|
|
344
|
-
default:
|
|
345
|
-
return `estimate: extrapolated from ${p.from}${p.factor !== undefined ? ` (×${p.factor})` : ""}`;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
1
|
+
// Moved to the plugin (packages/pi-plugin/src/scan/stage2request.ts), the scan engine's
|
|
2
|
+
// home: this name stays for the app's importers.
|
|
3
|
+
export * from "@quickchatai/pi-quickreview/src/scan/stage2request.js";
|