toolroll 0.9.92 → 0.9.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +4 -0
- package/dist/agent-fence.js +4 -1
- package/dist/approval-authority.d.ts +7 -1
- package/dist/approval-authority.js +11 -2
- package/dist/approval-password.d.ts +31 -0
- package/dist/approval-password.js +26 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +3 -3
- package/dist/assignment.js +16 -3
- package/dist/boot-identity.d.ts +2 -1
- package/dist/boot-identity.js +6 -4
- package/dist/bounded-spawn.d.ts +57 -0
- package/dist/bounded-spawn.js +220 -0
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +2 -2
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +48 -7
- package/dist/browser-workspace.js +3 -3
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +73 -25
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +12 -2
- package/dist/chat-actions.js +25 -2
- package/dist/chat-channel.js +11 -0
- package/dist/chat-decide.d.ts +7 -2
- package/dist/chat-decide.js +37 -9
- package/dist/chat-quiet.js +3 -3
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +126 -0
- package/dist/check-facts.js +318 -0
- package/dist/check-failure.d.ts +22 -3
- package/dist/check-failure.js +57 -13
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +10 -1
- package/dist/check-load.d.ts +85 -0
- package/dist/check-load.js +147 -0
- package/dist/check-only-standing.d.ts +36 -0
- package/dist/check-only-standing.js +39 -0
- package/dist/check-only.d.ts +15 -0
- package/dist/check-only.js +99 -0
- package/dist/check-report.d.ts +7 -0
- package/dist/check-report.js +4 -3
- package/dist/check-stamp.d.ts +46 -0
- package/dist/check-stamp.js +93 -0
- package/dist/checkout-cleanup.d.ts +36 -5
- package/dist/checkout-cleanup.js +166 -14
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +47 -30
- package/dist/cli.js +16 -1
- package/dist/code-extract.d.ts +44 -0
- package/dist/code-extract.js +88 -0
- package/dist/code-map-acts.d.ts +24 -0
- package/dist/code-map-acts.js +53 -0
- package/dist/code-map-admission.d.ts +52 -0
- package/dist/code-map-admission.js +295 -0
- package/dist/code-map-build.d.ts +63 -0
- package/dist/code-map-build.js +415 -0
- package/dist/code-map-cli.d.ts +153 -0
- package/dist/code-map-cli.js +103 -0
- package/dist/code-map-main.d.ts +1 -0
- package/dist/code-map-main.js +48 -0
- package/dist/code-map-read.d.ts +106 -0
- package/dist/code-map-read.js +171 -0
- package/dist/code-map-report.d.ts +37 -0
- package/dist/code-map-report.js +95 -0
- package/dist/code-map-shape.d.ts +172 -0
- package/dist/code-map-shape.js +327 -0
- package/dist/code-map-store.d.ts +136 -0
- package/dist/code-map-store.js +233 -0
- package/dist/coding-provider.d.ts +2 -0
- package/dist/coding-provider.js +3 -2
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.js +4 -4
- package/dist/console-chrome.js +2 -2
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +14 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +12 -5
- package/dist/contracts/console-api.js +12 -5
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +61 -0
- package/dist/contracts/lead-tools.js +10 -1
- package/dist/contracts/project-knowledge.d.ts +38 -5
- package/dist/contracts/project-knowledge.js +11 -5
- package/dist/contracts/project-memory.d.ts +3 -1
- package/dist/contracts/project-memory.js +3 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/youiyoux-request.d.ts +1 -1
- package/dist/contracts/youiyoux-request.js +1 -1
- package/dist/desktop-access.js +6 -2
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispatch.js +18 -15
- package/dist/dispose.js +8 -2
- package/dist/easy-start.js +2 -3
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/evals.js +2 -1
- package/dist/fast-start.d.ts +136 -0
- package/dist/fast-start.js +431 -0
- package/dist/fast-terms.d.ts +9 -0
- package/dist/fast-terms.js +23 -0
- package/dist/fenced-check-coverage.d.ts +62 -0
- package/dist/fenced-check-coverage.js +54 -0
- package/dist/first-run.js +1 -1
- package/dist/flaky-ci.d.ts +31 -0
- package/dist/flaky-ci.js +149 -0
- package/dist/flaky-cli.d.ts +60 -0
- package/dist/flaky-cli.js +60 -0
- package/dist/flaky.d.ts +83 -0
- package/dist/flaky.js +264 -0
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/invoke.d.ts +5 -1
- package/dist/invoke.js +49 -14
- package/dist/job-object-helper.ps1 +10 -3
- package/dist/keymap.d.ts +41 -0
- package/dist/keymap.js +51 -0
- package/dist/knowledge-cli.js +1 -1
- package/dist/knowledge-ui.d.ts +21 -4
- package/dist/knowledge-ui.js +75 -7
- package/dist/laptop-background.d.ts +16 -0
- package/dist/laptop-background.js +108 -2
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-progress.js +3 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +51 -2
- package/dist/lead.js +1 -1
- package/dist/lesson-notes.d.ts +81 -0
- package/dist/lesson-notes.js +336 -0
- package/dist/limits-ui.js +1 -1
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/machine-check.d.ts +16 -0
- package/dist/machine-check.js +26 -0
- package/dist/machine-identity.d.ts +75 -0
- package/dist/machine-identity.js +170 -0
- package/dist/mcp-core.js +11 -1
- package/dist/memory-cli.d.ts +300 -1
- package/dist/memory-cli.js +62 -9
- package/dist/memory-pass.d.ts +16 -0
- package/dist/memory-pass.js +67 -3
- package/dist/memory-tidy.d.ts +181 -0
- package/dist/memory-tidy.js +523 -0
- package/dist/memory-use.d.ts +35 -0
- package/dist/memory-use.js +109 -0
- package/dist/merge-checklist.d.ts +55 -0
- package/dist/merge-checklist.js +182 -0
- package/dist/needs-you.d.ts +32 -13
- package/dist/needs-you.js +108 -49
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +6 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +540 -70
- package/dist/phase-routing.js +2 -2
- package/dist/plain-schema.d.ts +23 -0
- package/dist/plain-schema.js +94 -0
- package/dist/plain-words.d.ts +20 -0
- package/dist/plain-words.js +66 -0
- package/dist/planner.js +2 -1
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +7 -2
- package/dist/project-delete-ui.js +12 -1
- package/dist/project-delete.js +13 -2
- package/dist/project-knowledge.d.ts +18 -11
- package/dist/project-knowledge.js +39 -9
- package/dist/project-learning.d.ts +2 -1
- package/dist/project-learning.js +15 -45
- package/dist/project-memory.d.ts +5 -0
- package/dist/project-memory.js +22 -2
- package/dist/proof.js +30 -5
- package/dist/provider-auth.js +1 -1
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +26 -3
- package/dist/push.js +55 -15
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +87 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +26 -17
- package/dist/release-card.d.ts +74 -6
- package/dist/release-card.js +179 -23
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +39 -9
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +38 -23
- package/dist/release-coverage.js +1 -1
- package/dist/release-next.d.ts +48 -0
- package/dist/release-next.js +161 -0
- package/dist/release.d.ts +50 -1
- package/dist/release.js +118 -11
- package/dist/remote-destination.d.ts +36 -0
- package/dist/remote-destination.js +217 -0
- package/dist/remote-main.d.ts +48 -0
- package/dist/remote-main.js +84 -5
- package/dist/repository-context.d.ts +3 -2
- package/dist/repository-context.js +112 -70
- package/dist/restart-certification.d.ts +1 -0
- package/dist/restart-certification.js +8 -6
- package/dist/result-acts.d.ts +29 -21
- package/dist/result-acts.js +36 -25
- package/dist/result-follow-ups.d.ts +2 -2
- package/dist/result-follow-ups.js +18 -4
- package/dist/result-review.d.ts +1 -1
- package/dist/route-change-check.d.ts +4 -0
- package/dist/route-change-check.js +13 -0
- package/dist/route-change.d.ts +22 -0
- package/dist/route-change.js +89 -0
- package/dist/route-hooks.js +3 -2
- package/dist/route-results.d.ts +6 -1
- package/dist/route-results.js +14 -5
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +7 -5
- package/dist/runner-process.js +3 -2
- package/dist/runner.js +2 -1
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +22 -9
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +24 -18
- package/dist/server/chat.js +14 -12
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +23 -0
- package/dist/server/guards.js +4 -0
- package/dist/server/home.d.ts +6 -0
- package/dist/server/home.js +21 -13
- package/dist/server/http.js +1 -1
- package/dist/server/kit.js +1 -1
- package/dist/server/merge-ready-view.d.ts +10 -0
- package/dist/server/merge-ready-view.js +30 -0
- package/dist/server/pages-assets.d.ts +1 -0
- package/dist/server/pages-assets.js +8 -5
- package/dist/server/pages.js +103 -82
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-code-map.d.ts +30 -0
- package/dist/server/render-code-map.js +137 -0
- package/dist/server/render-flaky.d.ts +20 -0
- package/dist/server/render-flaky.js +59 -0
- package/dist/server/render-history.js +1 -1
- package/dist/server/render-new-task.d.ts +26 -1
- package/dist/server/render-new-task.js +30 -5
- package/dist/server/render-pages.d.ts +1 -175
- package/dist/server/render-pages.js +38 -313
- package/dist/server/render-projects.d.ts +2 -1
- package/dist/server/render-projects.js +9 -7
- package/dist/server/render-release.d.ts +19 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +2 -2
- package/dist/server/render-results.js +71 -73
- package/dist/server/render-tasks.d.ts +13 -2
- package/dist/server/render-tasks.js +156 -139
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +4 -1
- package/dist/server/route-table.js +52 -13
- package/dist/server/settings.js +129 -22
- package/dist/server/task-views.js +61 -26
- package/dist/server/tasks.d.ts +17 -6
- package/dist/server/tasks.js +273 -338
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +295 -0
- package/dist/storage-sweep-process.js +1 -1
- package/dist/storage-sweep.d.ts +5 -3
- package/dist/storage-sweep.js +30 -9
- package/dist/storage-ui.js +28 -17
- package/dist/storage.d.ts +27 -2
- package/dist/storage.js +39 -2
- package/dist/store.d.ts +148 -34
- package/dist/store.js +626 -177
- package/dist/surface.js +46 -5
- package/dist/sweep-quarantine.d.ts +1 -0
- package/dist/sweep-quarantine.js +6 -4
- package/dist/tamper-scan.d.ts +4 -0
- package/dist/tamper-scan.js +90 -0
- package/dist/task-aliases.d.ts +5 -0
- package/dist/task-aliases.js +9 -0
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +36 -17
- package/dist/task-sizing.d.ts +5 -0
- package/dist/task-sizing.js +2 -0
- package/dist/task-source.d.ts +7 -0
- package/dist/task-source.js +14 -0
- package/dist/task-status.d.ts +9 -0
- package/dist/task-status.js +40 -49
- package/dist/teardown-git.d.ts +16 -0
- package/dist/teardown-git.js +63 -5
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/test-infra.d.ts +8 -0
- package/dist/test-infra.js +18 -0
- package/dist/tidy.js +1 -1
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +18 -5
- package/dist/tui-contract.d.ts +24 -0
- package/dist/tui-source.js +67 -8
- package/dist/tui.js +51 -51
- package/dist/vanished-retry.d.ts +30 -0
- package/dist/vanished-retry.js +174 -0
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +82 -70
- package/dist/work-summary.d.ts +2 -2
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +28 -22
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +68 -13
- package/package.json +1 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-tap release approval (docs/plans/trusted-release-reports.md, slice A v3): the plane's side of one push, one
|
|
3
|
+
* screen and one proof for a release a person must approve.
|
|
4
|
+
*
|
|
5
|
+
* - `releaseApprovalCard`: the authoritative card snapshot of a release task waiting for its person, for the sheet
|
|
6
|
+
* (`/t/<id>/release`) and `toolroll release approve`: either sealed form (release terms or a release check's purpose),
|
|
7
|
+
* the change list, GitHub CI as the coordinator last read it on the exact commit, why it can't be approved now (shown,
|
|
8
|
+
* never hidden) and the card digest, which binds all of it.
|
|
9
|
+
* - `approveReleaseCard`: the one wrapper both doors call. It rebuilds the card, compares the digest the person saw,
|
|
10
|
+
* re-reads the store's parts inside one transaction and only then passes THAT snapshot's scope digest into the
|
|
11
|
+
* unchanged transactional `approve()` the caller hands in. The digest is never swapped after the person was asked.
|
|
12
|
+
* - The notification's whole life: `noteReleaseReady` (one per task and scope digest, older ones resolved), the push
|
|
13
|
+
* class derived at send time from the plane's own state (`releasePushOf`), `noteReleaseStopped` for a manual
|
|
14
|
+
* release's stop, and the coordinator's CI snapshots (`recordReleaseCi`).
|
|
15
|
+
* Nothing here proves a person, widens a proof or seals anything itself.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { permissionPlainWords } from "./chat-terms.js";
|
|
21
|
+
import { approvalTermsOf, releaseAccessWords, releaseCardOf, releaseSealOf } from "./release-card.js";
|
|
22
|
+
import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
|
|
23
|
+
import { approvalOf } from "./scope.js";
|
|
24
|
+
import { releaseProgressFor } from "./store.js";
|
|
25
|
+
import { taskArchived } from "./task-archive.js";
|
|
26
|
+
import { cleanTerminalLine } from "./terminal-text.js";
|
|
27
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
28
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
29
|
+
/** A scope digest as scope.ts writes it. */
|
|
30
|
+
const SCOPE_DIGEST = /^[0-9a-f]{32,64}$/;
|
|
31
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
32
|
+
// ---- GitHub CI as the coordinator read it -------------------------------------------------------------------------
|
|
33
|
+
/** The ledger act holding the coordinator's latest reading of the pull request's CI on the exact candidate. */
|
|
34
|
+
export const RELEASE_CI_SEEN_ACTION = "release CI observed";
|
|
35
|
+
const ciName = (value) => cleanTerminalLine(String(value ?? "")).trim().slice(0, 120);
|
|
36
|
+
/**
|
|
37
|
+
* Record what the coordinator just read of the pull request's CI on `candidate` (its verdict and every check's newest
|
|
38
|
+
* attempt), when it differs from the last reading recorded: the sheet and the CLI show it, and the card digest binds
|
|
39
|
+
* it. The coordinator is Toolroll's own runtime reading GitHub through the plane's recorded repository.
|
|
40
|
+
*/
|
|
41
|
+
export function recordReleaseCi(store, input) {
|
|
42
|
+
if (!SHA.test(input.candidate) || store.lookupRef(input.taskId) === null)
|
|
43
|
+
return false;
|
|
44
|
+
// The newest attempt of each check (the adapter lists attempts oldest first), by name.
|
|
45
|
+
const latest = new Map();
|
|
46
|
+
for (const run of input.runs.slice(0, 2_000)) {
|
|
47
|
+
const name = ciName(run.name);
|
|
48
|
+
if (name === "")
|
|
49
|
+
continue;
|
|
50
|
+
latest.set(name, { id: typeof run.id === "number" && Number.isSafeInteger(run.id) ? run.id : null, name, status: ciName(run.status).slice(0, 40), conclusion: run.conclusion === null ? null : ciName(run.conclusion).slice(0, 40) });
|
|
51
|
+
}
|
|
52
|
+
const runs = [...latest.values()].sort((a, b) => a.name.localeCompare(b.name)).slice(0, 200);
|
|
53
|
+
const detail = JSON.stringify({ v: 1, candidate: input.candidate, state: input.state, failed: input.failed.map(ciName).slice(0, 50), runs });
|
|
54
|
+
const last = store.handle.prepare("SELECT detail FROM action_ledger WHERE task_id = ? AND action = ? ORDER BY id DESC LIMIT 1").get(input.taskId, RELEASE_CI_SEEN_ACTION);
|
|
55
|
+
if (last !== undefined && last["detail"] === detail)
|
|
56
|
+
return false;
|
|
57
|
+
store.recordAction({ at: input.now.toISOString(), actor: "toolroll", repo: store.lookupRef(input.taskId)?.repo ?? null, taskId: input.taskId, runId: null,
|
|
58
|
+
action: RELEASE_CI_SEEN_ACTION, outcome: input.state, source: "work", detail });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/** The coordinator's latest CI reading for this task on exactly `candidate`, or null (not read yet, or another commit). */
|
|
62
|
+
export function releaseCiOf(store, taskId, candidate) {
|
|
63
|
+
const row = store.handle.prepare("SELECT detail, at FROM action_ledger WHERE task_id = ? AND action = ? ORDER BY id DESC LIMIT 1").get(taskId, RELEASE_CI_SEEN_ACTION);
|
|
64
|
+
if (row === undefined)
|
|
65
|
+
return null;
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(String(row["detail"]));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const one = parsed;
|
|
74
|
+
if (one.v !== 1 || one.candidate !== candidate || !["passed", "running", "failed"].includes(String(one.state)) || !Array.isArray(one.failed) || !Array.isArray(one.runs))
|
|
75
|
+
return null;
|
|
76
|
+
return { candidate, at: String(row["at"]), state: one.state, failed: one.failed.map(ciName),
|
|
77
|
+
runs: one.runs.map(run => { const r = run; return { id: typeof r["id"] === "number" ? r["id"] : null, name: ciName(r["name"]), status: ciName(r["status"]), conclusion: r["conclusion"] == null ? null : ciName(r["conclusion"]) }; }) };
|
|
78
|
+
}
|
|
79
|
+
/** What of a CI reading the card binds: its state and each check's attempt, whether it finished, and how. Queued and
|
|
80
|
+
* in progress read alike (neither is finished); the time it was read is not bound. */
|
|
81
|
+
export function ciBinding(ci) {
|
|
82
|
+
return ci === null ? null : { candidate: ci.candidate, state: ci.state, failed: ci.failed, runs: ci.runs.map(run => [run.id, run.name, run.status === "completed", run.conclusion]) };
|
|
83
|
+
}
|
|
84
|
+
/** GitHub CI in one line, as the sheet and the CLI say it. */
|
|
85
|
+
export function ciWords(ci, candidate) {
|
|
86
|
+
const short = candidate.slice(0, 7);
|
|
87
|
+
if (ci === null)
|
|
88
|
+
return `GitHub CI on ${short} hasn't been read yet.`;
|
|
89
|
+
if (ci.state === "passed")
|
|
90
|
+
return `GitHub CI passed on ${short}.`;
|
|
91
|
+
if (ci.state === "failed")
|
|
92
|
+
return `GitHub CI failed on ${short}: ${ci.failed.join(", ") || "a check"}.`;
|
|
93
|
+
const finished = ci.runs.filter(run => run.status === "completed").length;
|
|
94
|
+
return `GitHub CI is running on ${short}: ${finished} of ${ci.runs.length} checks finished.`;
|
|
95
|
+
}
|
|
96
|
+
/** The release check failed on this task already (the coordinator's record): nobody approves it again here. */
|
|
97
|
+
function checkFailed(store, taskId) {
|
|
98
|
+
return store.handle.prepare("SELECT 1 FROM release_step WHERE task_id = ? AND step = 'check' AND state = 'failed' LIMIT 1").get(taskId) !== undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Why a release can't be approved now, from the plane's own state, or null. */
|
|
101
|
+
function refusalOf(store, taskId, scope, ci) {
|
|
102
|
+
if (taskArchived(store, taskId))
|
|
103
|
+
return "This release is put away. Restore it before approving.";
|
|
104
|
+
const hold = releaseCiHoldOf(store, taskId);
|
|
105
|
+
if (hold !== null)
|
|
106
|
+
return releaseCiHoldWords(hold);
|
|
107
|
+
if (ci?.state === "failed")
|
|
108
|
+
return `${ciWords(ci, scope.candidate)} Fix the branch or rerun the failed job on GitHub, then release again.`;
|
|
109
|
+
if (checkFailed(store, taskId))
|
|
110
|
+
return "The release check already failed on this commit. Fix the branch and release the new commit.";
|
|
111
|
+
if (scope.approvedAt !== null && scope.approvedDigest !== scope.digest)
|
|
112
|
+
return "The release's terms changed after an earlier approval. Read them again.";
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/** A release task from the plane's own state: a sealed form (terms or a release check's purpose) whose repository is the
|
|
116
|
+
* task's project and whose approved commit is the scope's candidate. Never from a title or a task id. */
|
|
117
|
+
export function releaseTaskOf(store, taskId) {
|
|
118
|
+
const ref = store.lookupRef(taskId);
|
|
119
|
+
const scope = store.getScope(taskId);
|
|
120
|
+
const seal = releaseSealOf(scope);
|
|
121
|
+
if (ref === null || ref.repo === null || scope === null || seal === null || scope.candidate == null || scope.candidate !== seal.identity.approvedSha || seal.identity.repo !== ref.repo)
|
|
122
|
+
return null;
|
|
123
|
+
return { ref: { id: ref.id, repo: ref.repo }, scope, seal };
|
|
124
|
+
}
|
|
125
|
+
/** The authoritative card for a release task waiting for its person, or why there is none. Reads only. */
|
|
126
|
+
export function releaseApprovalCard(store, taskId, options = {}) {
|
|
127
|
+
const found = releaseTaskOf(store, taskId);
|
|
128
|
+
if (found === null)
|
|
129
|
+
return { ok: false, why: "This isn't a release waiting for approval." };
|
|
130
|
+
const { ref, scope, seal } = found;
|
|
131
|
+
const terms = approvalTermsOf(store, scope, { profile: permissionPlainWords });
|
|
132
|
+
const card = releaseCardOf(store, scope, scope.digest, { profile: permissionPlainWords }, options.read);
|
|
133
|
+
const ci = releaseCiOf(store, taskId, scope.candidate);
|
|
134
|
+
const approval = approvalOf(scope);
|
|
135
|
+
const approved = approval.approved ? { by: approval.by, at: approval.at } : null;
|
|
136
|
+
const refusal = approved === null ? refusalOf(store, taskId, scope, ci) : null;
|
|
137
|
+
const progress = releaseProgressFor(store, taskId);
|
|
138
|
+
const stopped = progress?.stopped == null ? null : { step: progress.stopped.step, message: cleanTerminalLine(progress.stopped.message).slice(0, 300) };
|
|
139
|
+
const digest = sha256(`release-sheet:v1:${JSON.stringify([card.digest, scope.digest, ciBinding(ci), refusal, approved])}`);
|
|
140
|
+
return { ok: true, card: { taskId, taskRef: ref.id, repo: ref.repo, scope, seal, card, terms, ci, refusal, approved, stopped, digest } };
|
|
141
|
+
}
|
|
142
|
+
/** What approving does, in the sheet's and the CLI's words: the consequence, the check's access, and whether later
|
|
143
|
+
* commits carry. */
|
|
144
|
+
export function releaseConsequence(card) {
|
|
145
|
+
const id = card.seal.identity;
|
|
146
|
+
const carries = card.seal.kind === "terms" && card.seal.terms.policy.carry;
|
|
147
|
+
return [
|
|
148
|
+
`Approving runs the release check on ${id.approvedSha.slice(0, 7)}, then deploys, merges, tags and publishes once GitHub CI and the check pass.`,
|
|
149
|
+
...(releaseAccessWords(id.access) === null ? [] : [releaseAccessWords(id.access)]),
|
|
150
|
+
carries ? "Later commits that only change tests, docs or the changelog carry this approval." : "Every later commit asks again.",
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The one card-approval wrapper (Codex round 2, finding 2): rebuild the authoritative card and compare the digest the
|
|
155
|
+
* person saw; then, in one transaction, re-read the store's parts (the scope digest, the CI reading, the refusal state)
|
|
156
|
+
* and refuse on any change; then hand THAT snapshot's scope digest to `approveNow`, the caller's unchanged
|
|
157
|
+
* transactional `approve()`, which proves the person and seals. Nothing here proves anyone.
|
|
158
|
+
*/
|
|
159
|
+
export function approveReleaseCard(store, input) {
|
|
160
|
+
if (!DIGEST.test(input.cardDigest))
|
|
161
|
+
return { ok: false, reason: "changed", message: "This release changed while you were reading it. Read it again." };
|
|
162
|
+
const built = releaseApprovalCard(store, input.taskId, input.read === undefined ? {} : { read: input.read });
|
|
163
|
+
if (!built.ok)
|
|
164
|
+
return { ok: false, reason: "not-release", message: built.why };
|
|
165
|
+
const card = built.card;
|
|
166
|
+
if (card.digest !== input.cardDigest)
|
|
167
|
+
return { ok: false, reason: "changed", message: "This release changed while you were reading it. Read it again." };
|
|
168
|
+
if (card.approved !== null)
|
|
169
|
+
return { ok: false, reason: "approved", message: `Already approved by ${card.approved.by}.` };
|
|
170
|
+
if (card.refusal !== null)
|
|
171
|
+
return { ok: false, reason: "refused", message: card.refusal };
|
|
172
|
+
return store.transact(() => {
|
|
173
|
+
const scope = store.getScope(input.taskId);
|
|
174
|
+
if (scope === null || scope.digest !== card.scope.digest || approvalOf(scope).approved)
|
|
175
|
+
return { ok: false, reason: "changed", message: "This release changed while you were reading it. Read it again." };
|
|
176
|
+
const ci = releaseCiOf(store, input.taskId, card.scope.candidate);
|
|
177
|
+
const refusal = refusalOf(store, input.taskId, scope, ci);
|
|
178
|
+
if (JSON.stringify(ciBinding(ci)) !== JSON.stringify(ciBinding(card.ci)) || refusal !== card.refusal) {
|
|
179
|
+
return { ok: false, reason: "changed", message: "This release changed while you were reading it. Read it again." };
|
|
180
|
+
}
|
|
181
|
+
const result = input.approveNow(card.scope.digest);
|
|
182
|
+
return result.ok ? { ok: true, scope: result.scope } : { ok: false, reason: "approve", result };
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// ---- notifications --------------------------------------------------------------------------------------------
|
|
186
|
+
export const RELEASE_READY_KIND = "release-ready";
|
|
187
|
+
export const RELEASE_STOPPED_KIND = "release-stopped";
|
|
188
|
+
/** The one push phrase for a release waiting on its person (push.ts PUSH_WORDS.release). */
|
|
189
|
+
export const RELEASE_PUSH_WORDS = "A release is ready for your approval";
|
|
190
|
+
const READY_KEY = /^release-ready:([1-9][0-9]{0,14}):([0-9a-f]{32,64})$/;
|
|
191
|
+
/** The sheet's own address for a task (the in-app link; a push carries the opaque `/approve/<task ref>` instead). */
|
|
192
|
+
export const releaseSheetHref = (taskId) => `/t/${encodeURIComponent(taskId)}/release`;
|
|
193
|
+
/**
|
|
194
|
+
* The coordinator asked a person (on entering the approval step, a fresh filing or a resumed one): one notification per
|
|
195
|
+
* task and stable scope digest, so a crash between filing and asking is healed by the next run and a repeat says nothing
|
|
196
|
+
* new. Every older `release-ready` row of the project (another task or digest) is resolved first. Nothing when the task
|
|
197
|
+
* isn't a release waiting for a person.
|
|
198
|
+
*/
|
|
199
|
+
export function noteReleaseReady(store, input) {
|
|
200
|
+
return store.transact(() => {
|
|
201
|
+
const found = releaseTaskOf(store, input.taskId);
|
|
202
|
+
if (found === null || approvalOf(found.scope).approved || taskArchived(store, input.taskId))
|
|
203
|
+
return false;
|
|
204
|
+
const key = `release-ready:${found.ref.id}:${found.scope.digest}`;
|
|
205
|
+
store.handle.prepare("UPDATE notification SET resolved_at = ? WHERE kind = ? AND resolved_at IS NULL AND project = ? AND dedupe_key <> ?")
|
|
206
|
+
.run(input.now.toISOString(), RELEASE_READY_KIND, found.ref.repo, key);
|
|
207
|
+
const id = found.seal.identity;
|
|
208
|
+
const short = id.approvedSha.slice(0, 7);
|
|
209
|
+
const ci = releaseCiOf(store, input.taskId, id.approvedSha);
|
|
210
|
+
const body = ci?.state === "passed"
|
|
211
|
+
? `GitHub CI passed on ${short}. Approve to run the release check, deploy, merge and publish.`
|
|
212
|
+
: `Release ${id.packageVersion} (${short}) is ready to approve. You can approve now: Toolroll runs the release check after your yes, and deploys, merges and publishes only once GitHub CI and the check pass. You're told if something fails.`;
|
|
213
|
+
return store.enqueueNotification({ dedupeKey: key, kind: RELEASE_READY_KIND, pushClass: "decision", link: releaseSheetHref(input.taskId), source: { taskRef: found.ref.id },
|
|
214
|
+
subject: `Release ${id.packageVersion} is ready for your approval`, body }, input.now);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/** Resolve a task's `release-ready` rows (it was approved, or the release stopped): a push not sent yet never is. */
|
|
218
|
+
export function retireReleaseReady(store, taskRef, now) {
|
|
219
|
+
return Number(store.handle.prepare("UPDATE notification SET resolved_at = ? WHERE kind = ? AND resolved_at IS NULL AND task_ref = ?").run(now.toISOString(), RELEASE_READY_KIND, taskRef).changes);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The push class `release` and its link, derived at send time from the plane's own state, never from a title, a task id,
|
|
223
|
+
* the stored link or the stored class: the row is a `release-ready` row for exactly its task ref and a scope digest, and
|
|
224
|
+
* that task is a release scope (terms or a release check's purpose, its candidate the approved commit), not put away,
|
|
225
|
+
* not approved, at that same digest. Null otherwise.
|
|
226
|
+
*/
|
|
227
|
+
export function releasePushOf(store, notification) {
|
|
228
|
+
if (notification.kind !== RELEASE_READY_KIND || notification.taskRef === null || notification.resolvedAt !== null)
|
|
229
|
+
return null;
|
|
230
|
+
const key = READY_KEY.exec(notification.dedupeKey);
|
|
231
|
+
if (key === null || Number(key[1]) !== notification.taskRef)
|
|
232
|
+
return null;
|
|
233
|
+
const ref = store.refForId(notification.taskRef);
|
|
234
|
+
if (ref === null || ref.backend !== "built-in")
|
|
235
|
+
return null;
|
|
236
|
+
const found = releaseTaskOf(store, ref.externalId);
|
|
237
|
+
if (found === null || found.ref.id !== notification.taskRef)
|
|
238
|
+
return null;
|
|
239
|
+
if (found.scope.digest !== key[2] || approvalOf(found.scope).approved || taskArchived(store, ref.externalId))
|
|
240
|
+
return null;
|
|
241
|
+
return { link: `/approve/${notification.taskRef}` };
|
|
242
|
+
}
|
|
243
|
+
/** A `release-ready` row that no longer stands (the push pass resolves it instead of sending it). */
|
|
244
|
+
export function releaseReadyObsolete(store, notification) {
|
|
245
|
+
return notification.kind === RELEASE_READY_KIND && releasePushOf(store, notification) === null;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* A person's release stopped (CI failed, the check failed, the deploy, the merge, the tag or publication failed, or it
|
|
249
|
+
* stopped waiting): one attention notification per task, step and reason, with the stop's words in the app (the push
|
|
250
|
+
* says only the fixed attention phrase). Its `release-ready` row is resolved, except while it only stopped waiting
|
|
251
|
+
* for the approval (the sheet still approves it then).
|
|
252
|
+
*/
|
|
253
|
+
export function noteReleaseStopped(store, input) {
|
|
254
|
+
const ref = store.lookupRef(input.taskId);
|
|
255
|
+
if (ref === null)
|
|
256
|
+
return false;
|
|
257
|
+
const step = /^[a-z-]{1,20}$/.test(input.step) ? input.step : "release";
|
|
258
|
+
const reason = /^[a-z-]{1,40}$/.test(input.reason) ? input.reason : "error";
|
|
259
|
+
return store.transact(() => {
|
|
260
|
+
if (!(step === "approval" && reason === "timeout"))
|
|
261
|
+
retireReleaseReady(store, ref.id, input.now);
|
|
262
|
+
const seal = releaseSealOf(store.getScope(input.taskId));
|
|
263
|
+
const words = cleanTerminalLine(input.message).replace(/\s+/g, " ").trim().slice(0, 600);
|
|
264
|
+
return store.enqueueNotification({ dedupeKey: `release-stopped:${ref.id}:${step}:${reason}`, kind: RELEASE_STOPPED_KIND, pushClass: "attention",
|
|
265
|
+
link: `/t/${encodeURIComponent(input.taskId)}`, source: { taskRef: ref.id },
|
|
266
|
+
subject: `Release${seal === null ? "" : ` ${seal.identity.packageVersion}`} stopped at ${step}`, body: words === "" ? "The release stopped. Open it to see why." : words }, input.now);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// ---- the terminal -------------------------------------------------------------------------------------------------
|
|
270
|
+
/** The card as the CLI prints it, every line control-free (src/terminal-text.ts). */
|
|
271
|
+
export function releaseCardLines(card) {
|
|
272
|
+
const id = card.seal.identity;
|
|
273
|
+
const changes = card.card.changes;
|
|
274
|
+
const lines = [
|
|
275
|
+
`Release ${id.packageVersion} · pull request #${id.pr} · ${id.branch}`,
|
|
276
|
+
`Commit ${id.approvedSha}`,
|
|
277
|
+
ciWords(card.ci, id.approvedSha),
|
|
278
|
+
...releaseConsequence(card),
|
|
279
|
+
...(card.stopped === null ? [] : [`The release isn't running (it stopped at ${card.stopped.step}). After you approve, run \`toolroll release ${id.branch}\`.`]),
|
|
280
|
+
"",
|
|
281
|
+
changes.commits === null
|
|
282
|
+
? `What changed couldn't be read${changes.why === undefined ? "" : ` (${changes.why})`}.`
|
|
283
|
+
: changes.baseline === null ? `Pull request #${id.pr}'s commits (${changes.commits.length}):` : `Changes since the last verified release (${changes.baseline.slice(0, 7)}), ${changes.commits.length}:`,
|
|
284
|
+
...(changes.commits ?? []).map(one => ` ${one.sha.slice(0, 7)} ${one.subject}`),
|
|
285
|
+
"",
|
|
286
|
+
"Every term:",
|
|
287
|
+
...card.terms.map(line => ` ${line.label}: ${line.value}`),
|
|
288
|
+
...(card.refusal === null ? [] : ["", `Can't approve now: ${card.refusal}`]),
|
|
289
|
+
];
|
|
290
|
+
return lines.map(cleanTerminalLine);
|
|
291
|
+
}
|
|
292
|
+
const BRANCH = /^[A-Za-z0-9._/-]{1,255}$/;
|
|
293
|
+
const TASK_ID = /^[A-Za-z0-9._-]{1,200}$/;
|
|
294
|
+
const VERSION = /^\d+\.\d+\.\d+$/;
|
|
295
|
+
/** A journal's own fields, strictly; null when any is missing or not its exact shape. */
|
|
296
|
+
export function readWaitingJournal(file, raw, expectedFile) {
|
|
297
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
298
|
+
return null;
|
|
299
|
+
const j = raw;
|
|
300
|
+
const task = j["task"];
|
|
301
|
+
const candidate = j["candidate"];
|
|
302
|
+
if (j["version"] !== 1 || (j["step"] !== "approval" && j["step"] !== "gate"))
|
|
303
|
+
return null;
|
|
304
|
+
if (typeof j["branch"] !== "string" || !BRANCH.test(j["branch"]) || j["branch"].startsWith("-") || j["branch"].includes(".."))
|
|
305
|
+
return null;
|
|
306
|
+
if (typeof j["checkout"] !== "string" || !j["checkout"].startsWith("/") || typeof j["packageVersion"] !== "string" || !VERSION.test(j["packageVersion"]))
|
|
307
|
+
return null;
|
|
308
|
+
if (typeof j["pr"] !== "number" || !Number.isSafeInteger(j["pr"]) || j["pr"] < 1)
|
|
309
|
+
return null;
|
|
310
|
+
if (task === null || typeof task !== "object" || typeof task["id"] !== "string" || !TASK_ID.test(task["id"]) || typeof task["scopeDigest"] !== "string" || !SCOPE_DIGEST.test(task["scopeDigest"]))
|
|
311
|
+
return null;
|
|
312
|
+
if (candidate === null || typeof candidate !== "object" || typeof candidate["sha"] !== "string" || !SHA.test(candidate["sha"]))
|
|
313
|
+
return null;
|
|
314
|
+
// The file must be the one its own branch maps to: two branches that map to one file name never stand in for each other.
|
|
315
|
+
if (expectedFile(j["branch"]) !== file)
|
|
316
|
+
return null;
|
|
317
|
+
return { file, branch: j["branch"], checkout: j["checkout"], taskId: task["id"], scopeDigest: task["scopeDigest"], candidate: candidate["sha"], version: j["packageVersion"], pr: j["pr"] };
|
|
318
|
+
}
|
|
319
|
+
const real = (path) => { try {
|
|
320
|
+
return realpathSync(path);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
} };
|
|
325
|
+
/**
|
|
326
|
+
* Whether a journal (discovery only) names exactly the release the plane sealed: the task is a release scope whose
|
|
327
|
+
* sealed identity has this exact branch, version, pull request and candidate at this scope digest, and the journal's
|
|
328
|
+
* checkout, the task's project and the sealed repository are one real folder (`repo` too, when given). Why not, in words.
|
|
329
|
+
*/
|
|
330
|
+
export function journalMatchesPlane(store, journal, repo) {
|
|
331
|
+
const found = releaseTaskOf(store, journal.taskId);
|
|
332
|
+
if (found === null)
|
|
333
|
+
return { ok: false, why: `${journal.branch}'s release task isn't a release this plane can approve` };
|
|
334
|
+
const id = found.seal.identity;
|
|
335
|
+
if (id.branch !== journal.branch || id.packageVersion !== journal.version || id.pr !== journal.pr || id.approvedSha !== journal.candidate) {
|
|
336
|
+
return { ok: false, why: `${journal.branch}'s journal doesn't name the release the plane sealed (branch ${id.branch}, ${id.packageVersion}, #${id.pr}, ${id.approvedSha.slice(0, 7)})` };
|
|
337
|
+
}
|
|
338
|
+
if (found.scope.digest !== journal.scopeDigest)
|
|
339
|
+
return { ok: false, why: `the release's terms changed since ${journal.branch} was filed; run \`toolroll release\` again` };
|
|
340
|
+
const checkout = real(journal.checkout), project = real(found.ref.repo), sealed = real(id.repo);
|
|
341
|
+
if (checkout === null || project === null || checkout !== project || sealed !== project)
|
|
342
|
+
return { ok: false, why: `${journal.branch}'s checkout isn't the project the release was sealed for` };
|
|
343
|
+
if (repo !== null && real(repo) !== project)
|
|
344
|
+
return { ok: false, why: `${journal.branch} isn't a release of ${repo}` };
|
|
345
|
+
return { ok: true };
|
|
346
|
+
}
|
|
347
|
+
/** Every release journal in the state folder, read strictly; unreadable ones are counted, never guessed at. */
|
|
348
|
+
export function waitingJournals(stateDir, expectedFile) {
|
|
349
|
+
const folder = join(stateDir, "releases");
|
|
350
|
+
let names = [];
|
|
351
|
+
try {
|
|
352
|
+
names = readdirSync(folder).filter(name => name.endsWith(".json"));
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return { journals: [], unreadable: [] };
|
|
356
|
+
}
|
|
357
|
+
const journals = [], unreadable = [];
|
|
358
|
+
for (const name of names.sort()) {
|
|
359
|
+
const file = join(folder, name);
|
|
360
|
+
let raw;
|
|
361
|
+
try {
|
|
362
|
+
if (!lstatSync(file).isFile()) {
|
|
363
|
+
unreadable.push(name);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
unreadable.push(name);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const step = raw?.step;
|
|
373
|
+
if (step !== "approval" && step !== "gate")
|
|
374
|
+
continue;
|
|
375
|
+
const read = readWaitingJournal(file, raw, expectedFile);
|
|
376
|
+
if (read === null)
|
|
377
|
+
unreadable.push(name);
|
|
378
|
+
else
|
|
379
|
+
journals.push(read);
|
|
380
|
+
}
|
|
381
|
+
return { journals, unreadable };
|
|
382
|
+
}
|
package/dist/release-auto.js
CHANGED
|
@@ -18,8 +18,8 @@ import { fullCheckProblem, projectGitDir } from "./check-report.js";
|
|
|
18
18
|
import { sealedRun } from "./release-recheck.js";
|
|
19
19
|
import { runCheckLevel } from "./check-levels.js";
|
|
20
20
|
import { readVerifiedCommit, verifiedBlobs, verifiedTreeDiff, verifiedTreeEntries } from "./commit-extract.js";
|
|
21
|
-
import { EVAL_HOOK_FILES, evalHookProblem, storeRewriteProblem } from "./eval-hooks.js";
|
|
22
|
-
import { mightName, testInfrastructure } from "./test-infra.js";
|
|
21
|
+
import { EVAL_HOOK_FILES, evalHookProblem, LEARNING_HOOK_FILES, learningHookProblem, storeRewriteProblem } from "./eval-hooks.js";
|
|
22
|
+
import { isTestPath, mightName, testImportProblem, testInfrastructure } from "./test-infra.js";
|
|
23
23
|
import { chatControlHref } from "./chat-controls.js";
|
|
24
24
|
import { INFRA_RERUNS_PER_CANDIDATE, REQUIRED_PR_CHECKS, REQUIRED_PR_CHECKS_WORKFLOW, prChecksVerdict } from "./release.js";
|
|
25
25
|
import { releaseCheckStanding } from "./check-failure.js";
|
|
@@ -185,8 +185,11 @@ const TEST_FILE = /^(?!.*(^|\/)fixtures?\/)[^]*\.test\.ts$/;
|
|
|
185
185
|
* accounting readings evals trust (reported-usage.ts, run-efficiency.ts). Where they are called from, and the store's
|
|
186
186
|
* accounting reads, are held by eval-hooks.ts instead (evalHookProblem). Results routing's policy is here too
|
|
187
187
|
* (docs/plans/results-routing.md, Protection 1): the record evaluator, the rules, the one writer module, this scan's
|
|
188
|
-
* manifest and the record contract; their callers and pinned bodies are route-hooks.ts's (routingHookProblem).
|
|
189
|
-
|
|
188
|
+
* manifest and the record contract; their callers and pinned bodies are route-hooks.ts's (routingHookProblem). The
|
|
189
|
+
* learning-loops policy modules are here whole (docs/plans/learning-loops.md, Protection 1): check facts, flaky tests
|
|
190
|
+
* (local and CI), lesson notes, memory use and tidy-up; their callers, pinned bodies and SQL writers are eval-hooks.ts's
|
|
191
|
+
* (learningHookProblem). */
|
|
192
|
+
const EVAL_POLICY = /^(evals\/|src\/evals[^/]*\.ts$|src\/eval-[^/]*\.ts$|src\/reviewer-evals\.ts$|src\/nightly-github\.ts$|src\/contracts\/evals[^/]*\.ts$|src\/reported-usage\.ts$|src\/run-efficiency\.ts$|src\/route-(?:results|rules|store|hooks)\.ts$|src\/contracts\/route-results\.ts$|src\/(?:check-facts|flaky|flaky-ci|lesson-notes|memory-use|memory-tidy)\.ts$)/;
|
|
190
193
|
/** Build or type configuration anywhere: a person approves it. */
|
|
191
194
|
const BUILD_CONFIG = /(^|\/)(tsconfig[^/]*\.json|[^/]*\.config\.[cm]?[jt]s|\.npmrc|\.nvmrc|\.node-version)$/;
|
|
192
195
|
/**
|
|
@@ -212,7 +215,7 @@ export function autoPathProblem(path) {
|
|
|
212
215
|
// Test files under test/ carry the same trust as src's (Alex: test authors are trusted) — only the `.test.ts` files
|
|
213
216
|
// themselves, never a helper, fixture, setup or config file beside them, nor one inside a fixtures folder.
|
|
214
217
|
if (EVAL_POLICY.test(normal))
|
|
215
|
-
return "it's eval, nightly
|
|
218
|
+
return "it's eval, nightly, model routing or learning policy code";
|
|
216
219
|
if (testUnderTest)
|
|
217
220
|
return null;
|
|
218
221
|
if (BUILD_CONFIG.test(normal))
|
|
@@ -343,6 +346,12 @@ function changeProblem(repo, base, candidate) {
|
|
|
343
346
|
const routing = routingHookProblem({ candidate: routingAt(after.value.tree), base: routingAt(before.value.tree) }, { paths: changed, candidate: changedAt(after.value.tree), base: changedAt(before.value.tree) });
|
|
344
347
|
if (routing !== null)
|
|
345
348
|
return `${routing}: a person decides`;
|
|
349
|
+
// Learning loops (eval-hooks.ts learningHookProblem): the calls that record a check's facts, the strictly pinned
|
|
350
|
+
// memory and check bodies the base holds, and no changed file writing a learning-loops table outside its writers.
|
|
351
|
+
const learningAt = (tree) => { const blobs = verifiedBlobs(gitDir, tree, LEARNING_HOOK_FILES); return (file) => blobs.ok ? blobs.value.get(file)?.bytes ?? null : null; };
|
|
352
|
+
const learning = learningHookProblem({ candidate: learningAt(after.value.tree), base: learningAt(before.value.tree) }, { paths: changed, candidate: changedAt(after.value.tree), base: changedAt(before.value.tree) });
|
|
353
|
+
if (learning !== null)
|
|
354
|
+
return `${learning}: a person decides`;
|
|
346
355
|
for (const change of diff.value) {
|
|
347
356
|
const modes = [change.before?.mode, change.after?.mode].filter((one) => one !== undefined);
|
|
348
357
|
if (modes.includes("120000") || modes.includes("160000"))
|
|
@@ -350,18 +359,18 @@ function changeProblem(repo, base, candidate) {
|
|
|
350
359
|
// Taking a test away is never automatic, wherever it lives.
|
|
351
360
|
if (change.kind === "deleted" && TEST_FILE.test(rulePath(change.path)))
|
|
352
361
|
return `${change.path} was deleted (removing a test needs a person)`;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
362
|
+
// Counts cannot prove that an assertion survived, and an appended line can disarm what it follows (`vi.mock` hoists,
|
|
363
|
+
// a top-level hook reaches every case): a test file that was there stays exactly as it was (H4).
|
|
364
|
+
if (change.before !== null && TEST_FILE.test(rulePath(change.path)))
|
|
365
|
+
return `${change.path} changes existing test code (new tests go in a new file; rewriting tests needs a person)`;
|
|
366
|
+
// A new test may not reach into another: it would run that file's code, and its mocks, here. From the verified tree.
|
|
367
|
+
// Any test extension: `src/` passes autoPathProblem whatever the file is called, so `.test.ts` alone would miss `.test.tsx`.
|
|
368
|
+
if (change.kind === "added" && isTestPath(rulePath(change.path))) {
|
|
369
|
+
const read = verifiedBlobs(gitDir, after.value.tree, [change.path]);
|
|
370
|
+
const bytes = read.ok ? read.value.get(change.path)?.bytes : undefined;
|
|
371
|
+
const why = bytes === undefined ? `${change.path}'s test code couldn't be read` : testImportProblem(change.path, bytes.toString("utf8"));
|
|
372
|
+
if (why !== null)
|
|
373
|
+
return `${why} (a person decides)`;
|
|
365
374
|
}
|
|
366
375
|
const why = autoPathProblem(change.path);
|
|
367
376
|
if (why !== null)
|
package/dist/release-card.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type ReleaseTerms } from "./contracts/release-terms.js";
|
|
1
|
+
import { type ReleaseCheckPurpose, type ReleaseTerms } from "./contracts/release-terms.js";
|
|
2
2
|
import type { Scope } from "./scope.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type Store } from "./store.js";
|
|
4
4
|
/** One signed term as the card says it. `key` names the scope field or the release term's path. */
|
|
5
5
|
export type TermLine = {
|
|
6
6
|
key: string;
|
|
@@ -29,6 +29,23 @@ export declare const SCOPE_TERM_LABELS: {
|
|
|
29
29
|
/** Every field of the release terms, by its path in the terms (contracts/release-terms.ts), against the schema's
|
|
30
30
|
* own leaves: the contract test checks every one has a label. */
|
|
31
31
|
export declare const RELEASE_TERM_LABELS: Readonly<Record<string, string>>;
|
|
32
|
+
/** Every field of a release check's purpose (contracts/release-terms.ts releaseCheckPurposeSchema): the sealed form
|
|
33
|
+
* Toolroll's own releases take while their checks run outside the sandbox. A contract test checks every leaf has one. */
|
|
34
|
+
export declare const RELEASE_CHECK_LABELS: Readonly<Record<string, string>>;
|
|
35
|
+
/** What a release scope sealed, either form: release terms (later test-only commits may carry) or a release check's
|
|
36
|
+
* purpose (every later commit asks again). The identity both share is read from here, never from a title or an id. */
|
|
37
|
+
export type ReleaseSeal = {
|
|
38
|
+
kind: "terms";
|
|
39
|
+
terms: ReleaseTerms;
|
|
40
|
+
identity: ReleaseTerms;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "check";
|
|
43
|
+
purpose: ReleaseCheckPurpose;
|
|
44
|
+
identity: ReleaseCheckPurpose;
|
|
45
|
+
};
|
|
46
|
+
export declare function releaseSealOf(scope: Pick<Scope, "releaseTerms" | "releaseCheck"> | null): ReleaseSeal | null;
|
|
47
|
+
/** What an unsandboxed (or sign-ins) release check means, said before anyone approves it. Null in the sandbox. */
|
|
48
|
+
export declare function releaseAccessWords(access: ReleaseTerms["access"]): string | null;
|
|
32
49
|
/** The leaf paths of a zod object schema (nested objects joined with dots). */
|
|
33
50
|
export declare function schemaLeaves(schema: unknown, prefix?: string): string[];
|
|
34
51
|
/** Every signed term of `scope`, its release terms included, one line each. Reads only. */
|
|
@@ -48,6 +65,9 @@ export type ChangeList = {
|
|
|
48
65
|
digest: string;
|
|
49
66
|
/** A release is on the record but its merge commit can't be read (eval-harness review 2, finding 4). */
|
|
50
67
|
baselineUnreadable?: true;
|
|
68
|
+
/** Why the list couldn't be read whole (Codex round 2, finding 6): the pull request moved, or it is longer than
|
|
69
|
+
* GitHub lists. Part of the digest; never a shortened list. */
|
|
70
|
+
why?: string;
|
|
51
71
|
};
|
|
52
72
|
/**
|
|
53
73
|
* The plane's own record of the last verified release of `repo`: the merge commit GitHub answered with, recorded once
|
|
@@ -70,8 +90,57 @@ export declare function commitsBetween(repo: string, from: string, to: string):
|
|
|
70
90
|
sha: string;
|
|
71
91
|
subject: string;
|
|
72
92
|
}[] | null;
|
|
73
|
-
/**
|
|
74
|
-
export declare
|
|
93
|
+
/** GitHub lists at most this many of a pull request's commits; a longer one is never shown as complete. */
|
|
94
|
+
export declare const PULL_REQUEST_COMMITS_MAX = 250;
|
|
95
|
+
/** A commit subject is someone else's text: one line, control-free (src/terminal-text.ts), bounded. The card digest
|
|
96
|
+
* binds this cleaned text, so every surface shows exactly what was bound (Codex round 2, finding 7). */
|
|
97
|
+
export declare const subjectText: (subject: string) => string;
|
|
98
|
+
/** How the plane reads GitHub for a pull request's commits (a seam for tests). */
|
|
99
|
+
export type PullRequestReader = (args: readonly string[]) => {
|
|
100
|
+
status: number | null;
|
|
101
|
+
stdout: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* A pull request's own commits from GitHub, for the recorded owner/name, complete and bound to the candidate (Codex
|
|
105
|
+
* round 2, finding 6): every page (`--paginate`), accepted only when the pull request's head is `head`, the newest
|
|
106
|
+
* listed commit is `head` and the count is the pull request's own count. Otherwise why, never a shortened list.
|
|
107
|
+
*/
|
|
108
|
+
export declare function pullRequestCommits(slug: string, pr: number, head: string, read?: PullRequestReader): {
|
|
109
|
+
ok: true;
|
|
110
|
+
commits: {
|
|
111
|
+
sha: string;
|
|
112
|
+
subject: string;
|
|
113
|
+
}[];
|
|
114
|
+
} | {
|
|
115
|
+
ok: false;
|
|
116
|
+
why: string;
|
|
117
|
+
};
|
|
118
|
+
/** The ledger act that records a verified release (the merged tree proved equal to the approved one). */
|
|
119
|
+
export declare const RELEASE_VERIFIED_ACTION = "release verified";
|
|
120
|
+
/**
|
|
121
|
+
* Record a verified release (Codex round 2, finding 1: verified release history apart from carrying). The coordinator
|
|
122
|
+
* calls it once main's merge commit was proved to hold the approved tree; the plane proves it again here, from its own
|
|
123
|
+
* records only: the task is a release scope of a project, and `merge` read as a verified Git object from the project's
|
|
124
|
+
* recorded Git directory has exactly the sealed approved tree. False when anything doesn't hold (nothing written).
|
|
125
|
+
*/
|
|
126
|
+
export declare function recordVerifiedRelease(store: Store, input: {
|
|
127
|
+
taskId: string;
|
|
128
|
+
merge: string;
|
|
129
|
+
now: Date;
|
|
130
|
+
}): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* The last verified release of `repo`, from either record: a merged release authority's succeeded merge, or a verified
|
|
133
|
+
* release row (recordVerifiedRelease). The newer stands. Shaped as releaseBaselineRecord.
|
|
134
|
+
*/
|
|
135
|
+
export declare function verifiedReleaseBaseline(store: Store, repo: string): {
|
|
136
|
+
recorded: false;
|
|
137
|
+
} | {
|
|
138
|
+
recorded: true;
|
|
139
|
+
sha: string | null;
|
|
140
|
+
};
|
|
141
|
+
/** What changed since the last verified release, for a release's sealed identity (terms or a release check's purpose),
|
|
142
|
+
* from Toolroll's records only. */
|
|
143
|
+
export declare function releaseChangeList(store: Store, terms: Pick<ReleaseTerms, "repo" | "approvedSha" | "pr">, read?: PullRequestReader): ChangeList;
|
|
75
144
|
export type ReleaseCard = {
|
|
76
145
|
/** Binds the scope digest, the plan form's digest, the baseline and the change list. */
|
|
77
146
|
digest: string;
|
|
@@ -82,7 +151,6 @@ export type ReleaseCard = {
|
|
|
82
151
|
};
|
|
83
152
|
/** The card digest: everything the person is shown that can change under them. */
|
|
84
153
|
export declare function cardDigestOf(scopeDigest: string, formDigest: string, changes: Pick<ChangeList, "baseline" | "digest">): string;
|
|
85
|
-
/** The release card for a deploy gate, against the live records now. */
|
|
86
154
|
export declare function releaseCardOf(store: Store, scope: Scope, formDigest: string, words: {
|
|
87
155
|
profile: (profile: Scope["profile"]) => string | null;
|
|
88
|
-
}): ReleaseCard;
|
|
156
|
+
}, read?: PullRequestReader): ReleaseCard;
|