toolroll 0.9.71 → 0.9.72
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 +79 -0
- package/dist/agent-fence.d.ts +5 -2
- package/dist/agent-fence.js +5 -2
- package/dist/approval-authority.d.ts +20 -0
- package/dist/approval-authority.js +67 -0
- package/dist/assignment.js +26 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/builder.js +48 -6
- package/dist/chat-decide.js +17 -2
- package/dist/check-access.js +2 -2
- package/dist/check-report.d.ts +109 -0
- package/dist/check-report.js +204 -0
- package/dist/claim.js +1 -1
- package/dist/commit-extract.d.ts +94 -0
- package/dist/commit-extract.js +436 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/flow.d.ts +16 -0
- package/dist/contracts/flow.js +3 -0
- package/dist/contracts/release-check.d.ts +335 -0
- package/dist/contracts/release-check.js +159 -0
- package/dist/contracts/release-terms.d.ts +60 -0
- package/dist/contracts/release-terms.js +70 -0
- package/dist/contracts/verification-receipt.d.ts +42 -8
- package/dist/contracts/verification-receipt.js +24 -7
- package/dist/contracts/verification-view.d.ts +1 -0
- package/dist/deploy-gate.d.ts +17 -0
- package/dist/deploy-gate.js +68 -0
- package/dist/dispatch.js +3 -10
- package/dist/dispose.js +4 -1
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +20 -2
- package/dist/evidence.d.ts +1 -0
- package/dist/evidence.js +2 -1
- package/dist/flow-engine.js +5 -1
- package/dist/flow-pull-request.d.ts +5 -2
- package/dist/flow-pull-request.js +14 -5
- package/dist/flow-schedule.js +25 -6
- package/dist/flows-ui.js +7 -1
- package/dist/invoke.d.ts +17 -6
- package/dist/invoke.js +48 -18
- package/dist/lead-status.js +5 -2
- package/dist/live-terminal.d.ts +10 -6
- package/dist/live-terminal.js +55 -14
- package/dist/nightly-check.d.ts +104 -0
- package/dist/nightly-check.js +362 -0
- package/dist/operate.js +39 -2
- package/dist/person-proof.d.ts +2 -0
- package/dist/person-proof.js +14 -0
- package/dist/plan-auto.js +4 -4
- package/dist/planner-source.d.ts +1 -1
- package/dist/project-delete-ui.js +1 -1
- package/dist/proof.js +2 -0
- package/dist/publish.js +156 -144
- package/dist/pull-request-flow.d.ts +2 -0
- package/dist/pull-request-flow.js +24 -2
- package/dist/release-adapters.d.ts +8 -0
- package/dist/release-adapters.js +212 -15
- package/dist/release-authority.d.ts +112 -0
- package/dist/release-authority.js +126 -0
- package/dist/release-carry.d.ts +233 -0
- package/dist/release-carry.js +645 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +124 -5
- package/dist/release-coverage.js +3 -0
- package/dist/release-recheck.d.ts +146 -0
- package/dist/release-recheck.js +657 -0
- package/dist/release-view.d.ts +34 -0
- package/dist/release-view.js +27 -0
- package/dist/release.d.ts +131 -1
- package/dist/release.js +562 -244
- package/dist/remote-main.d.ts +2 -0
- package/dist/remote-main.js +1 -1
- package/dist/result-completion.d.ts +6 -0
- package/dist/result-completion.js +15 -1
- package/dist/result-follow-ups.js +29 -1
- package/dist/sandbox-runtime.d.ts +16 -5
- package/dist/sandbox-runtime.js +19 -6
- package/dist/scope.d.ts +115 -5
- package/dist/scope.js +219 -7
- package/dist/server/render-tasks.d.ts +8 -0
- package/dist/server/render-tasks.js +50 -9
- package/dist/server/route-table.js +1 -0
- package/dist/server/task-views.js +4 -0
- package/dist/server/tasks.js +12 -0
- package/dist/store.d.ts +227 -5
- package/dist/store.js +727 -37
- package/dist/task-outcome-cli.js +7 -3
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +25 -1
- package/dist/verification-evidence.js +57 -2
- package/dist/work-index.js +3 -6
- package/package.json +1 -1
package/dist/builder.js
CHANGED
|
@@ -8,6 +8,9 @@ import { queueBatchCheck } from "./batch-checks.js";
|
|
|
8
8
|
import { batchesFullCheck } from "./batch-policy.js";
|
|
9
9
|
import { skillsContext } from "./project-skills.js";
|
|
10
10
|
import { failedVerificationEvidence, sealVerificationReceipt, verificationEvidence, reuseObservationVerification } from "./verification-evidence.js";
|
|
11
|
+
import { CHECK_INVENTORY_CAPTURE, CHECK_REPORT_CAPTURE, prepareCheckAttempt, sealCheckOutcome } from "./check-report.js";
|
|
12
|
+
import { planTargetedRecheck, settleCoverage } from "./release-recheck.js";
|
|
13
|
+
import { effectiveCheckFacts } from "./verification-evidence.js";
|
|
11
14
|
import { learningContext } from "./project-learning.js";
|
|
12
15
|
import { knowledgeContext } from "./project-knowledge.js";
|
|
13
16
|
import { flowGoalCuts } from "./flow-engine.js";
|
|
@@ -322,12 +325,13 @@ export function proveApprovedProfile(scope, given) {
|
|
|
322
325
|
budgetMicrousd: scope.budgetMicrousd,
|
|
323
326
|
acceptance: scope.acceptance, candidate: scope.candidate ?? null,
|
|
324
327
|
qualityMode: scope.qualityMode ?? "default",
|
|
328
|
+
releaseTerms: scope.releaseTerms ?? null,
|
|
325
329
|
}, snapshot,
|
|
326
330
|
// v47: the SEALED route is part of the signed bytes whenever it
|
|
327
331
|
// says more than the legacy resolution — re-derived here from
|
|
328
332
|
// the seal's own snapshot, never from mutable configuration.
|
|
329
333
|
routeFromJson(scope.approvedRouteJson ?? null))
|
|
330
|
-
: digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null });
|
|
334
|
+
: digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, releaseTerms: scope.releaseTerms ?? null });
|
|
331
335
|
if (rederived !== scope.approvedDigest) {
|
|
332
336
|
return { ok: false, message: "the approval record does not verify against the stored terms — re-approve (stale-approval)" };
|
|
333
337
|
}
|
|
@@ -416,7 +420,7 @@ export async function build(store, request) {
|
|
|
416
420
|
return {
|
|
417
421
|
ok: false,
|
|
418
422
|
reason: "mode-ended",
|
|
419
|
-
message: `${taskId}'s approval
|
|
423
|
+
message: `${taskId}'s approval no longer stands (an ended mode or flow grant, or an approval that wasn't a person's) — a person approves it again`,
|
|
420
424
|
};
|
|
421
425
|
}
|
|
422
426
|
// v24 DISPATCH PROOF (foundations rulings 10/12, findings 6/17): what is
|
|
@@ -2251,6 +2255,9 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2251
2255
|
const batched = repo !== null && chosenCheck !== null && batchesFullCheck(store, repo, chosenCheck.level, configured);
|
|
2252
2256
|
const gateCommand = batched ? null : configured;
|
|
2253
2257
|
let verifyCommand;
|
|
2258
|
+
let checkAttempt = null;
|
|
2259
|
+
let releaseFacts = null;
|
|
2260
|
+
const verification = { last: null };
|
|
2254
2261
|
const checkLog = [];
|
|
2255
2262
|
const checkOutcomes = [];
|
|
2256
2263
|
const checkSuites = [];
|
|
@@ -2296,6 +2303,20 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2296
2303
|
else {
|
|
2297
2304
|
const verifyRunner = request.verify ?? run;
|
|
2298
2305
|
const verifyShell = approvedCommandShell(configured.command);
|
|
2306
|
+
// A Full check of a prepared candidate (a release gate, the nightly check): its attempt is sealed before the command
|
|
2307
|
+
// runs — intended mode, nonce, inventory, context — and the handshake goes on its stdin (check-report.ts).
|
|
2308
|
+
// A release task's check is targeted when the release's last full check allows it (release-recheck.ts): its plan and
|
|
2309
|
+
// obligation snapshot are sealed with the attempt; otherwise it runs in full, told which tests it must still report.
|
|
2310
|
+
if (repo !== null && chosenCheck?.level === "full" && (store.getScope(request.taskId)?.candidate ?? null) !== null) {
|
|
2311
|
+
const access = checkLaunchOf(store, runId, captured.effective.profile).access;
|
|
2312
|
+
const decision = planTargetedRecheck(store, root, { runId, taskId: request.taskId, repo, head: sealedHead, command: configured, access, now: now() });
|
|
2313
|
+
checkAttempt = prepareCheckAttempt(store, root, { runId, repo, head: sealedHead, command: configured, access, authority: decision.authority,
|
|
2314
|
+
...(decision.mode === "targeted" ? { plan: decision.plan, snapshot: decision.snapshot } : {}), watch: decision.watch }, now());
|
|
2315
|
+
if (checkAttempt !== null && decision.mode === "targeted")
|
|
2316
|
+
store.addRunNote(runId, "Toolroll", `Re-running only what failed at run #${decision.plan.base}: ${decision.plan.units.length} unit files, ${decision.plan.groups.reduce((n, one) => n + one.groups.length, 0)} journey groups, ${decision.plan.versions.length} upgrade versions${decision.plan.parts.length > 0 ? `, and in full: ${decision.plan.parts.join(", ")}` : ""}.`, now());
|
|
2317
|
+
}
|
|
2318
|
+
else
|
|
2319
|
+
checkAttempt = null;
|
|
2299
2320
|
const checkProgress = new CheckProgressTracker(snapshot => {
|
|
2300
2321
|
store.saveCheckProgress(runId, snapshot, now());
|
|
2301
2322
|
});
|
|
@@ -2315,6 +2336,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2315
2336
|
},
|
|
2316
2337
|
envAllowlist: SETUP_ENV_ALLOWLIST,
|
|
2317
2338
|
omitEnv: SETUP_ENV_DENYLIST,
|
|
2339
|
+
...(checkAttempt === null ? {} : { stdin: checkAttempt.stdin }),
|
|
2318
2340
|
onStdout: chunk => { streamed.stdout = true; checkProgress.feed(chunk, "stdout"); },
|
|
2319
2341
|
onStderr: chunk => { streamed.stderr = true; checkProgress.feed(chunk, "stderr"); },
|
|
2320
2342
|
}));
|
|
@@ -2327,6 +2349,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2327
2349
|
checkOutcomes.push(attemptOutcome(label, result));
|
|
2328
2350
|
noteCheckSuite(label, result);
|
|
2329
2351
|
checkLog.push(attemptLog(label, configured.command, result));
|
|
2352
|
+
verification.last = result;
|
|
2330
2353
|
return result;
|
|
2331
2354
|
};
|
|
2332
2355
|
const ownInstallIfChanged = async () => {
|
|
@@ -2497,6 +2520,22 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2497
2520
|
}
|
|
2498
2521
|
}
|
|
2499
2522
|
checkProgress.finish();
|
|
2523
|
+
// The check's own report (the last line carrying the nonce), sealed beside what was sealed before it ran.
|
|
2524
|
+
const last = verification.last;
|
|
2525
|
+
if (checkAttempt !== null && last !== null && !last.notFound) {
|
|
2526
|
+
sealCheckOutcome(store, root, checkAttempt, { stdout: last.stdout, exitCode: last.timedOut ? null : last.code }, now());
|
|
2527
|
+
}
|
|
2528
|
+
// What the check covered (receipt version 3): full, composed from the release's last full check, partial, or
|
|
2529
|
+
// identity-removed (which stops the release carrying).
|
|
2530
|
+
if (checkAttempt !== null) {
|
|
2531
|
+
const settled = settleCoverage(store, root, { runId, taskId: request.taskId, exitCode: last === null || last.notFound || last.timedOut ? null : last.code, now: now() }, run => { const gate = verificationEvidence(store, root, run); return gate.ok ? gate.digest : null; });
|
|
2532
|
+
const sha = (capture) => { const found = store.artifactsFor(runId).filter(a => a.kind === "structured-output" && a.capture === capture); return found.length === 1 ? found[0].sha256 : null; };
|
|
2533
|
+
// The one place coverage meets the outcome: a partial or identity-removed check is a failed attempt for every
|
|
2534
|
+
// reader from here on (the verdict, the run's check, the receipt, reuse, the deploy gates).
|
|
2535
|
+
const effective = effectiveCheckFacts(verifyCommand, { coverage: settled.coverage, check: { attempt: checkAttempt.mode, report: sha(CHECK_REPORT_CAPTURE), inventory: sha(CHECK_INVENTORY_CAPTURE) }, composed: settled.composed });
|
|
2536
|
+
verifyCommand = effective.result;
|
|
2537
|
+
releaseFacts = effective.facts;
|
|
2538
|
+
}
|
|
2500
2539
|
}
|
|
2501
2540
|
if (configured !== null && checkLog.length > 0) {
|
|
2502
2541
|
const summary = `=== Attempt summary ===\n${checkOutcomes.length === 0 ? "No command started." : checkOutcomes.map(one => `- ${one}`).join("\n")}`;
|
|
@@ -2513,14 +2552,17 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2513
2552
|
});
|
|
2514
2553
|
}
|
|
2515
2554
|
if (configured !== null && checkLog.length > 0)
|
|
2516
|
-
sealVerificationReceipt(store, root, runId, sealedHead, configured, verifyCommand, now());
|
|
2555
|
+
sealVerificationReceipt(store, root, runId, sealedHead, configured, verifyCommand, now(), undefined, releaseFacts ?? undefined);
|
|
2517
2556
|
// The receipt above remains the evidence. This compact projection is what
|
|
2518
2557
|
// `status` and `task wait` can read without opening logs. A reused gate did
|
|
2519
2558
|
// not execute here, so its label says so instead of implying a fresh run.
|
|
2559
|
+
// A release check that didn't cover what it owed (partial) or lost a required test reads failed, whatever its exit.
|
|
2560
|
+
const uncovered = releaseFacts !== null && releaseFacts.coverage !== "full" && releaseFacts.coverage !== "composed";
|
|
2520
2561
|
const checkStatus = verifyCommand.configured === false ? "not-run"
|
|
2521
|
-
:
|
|
2522
|
-
:
|
|
2523
|
-
: "
|
|
2562
|
+
: uncovered ? "failed"
|
|
2563
|
+
: verifyCommand.ran ? verifyCommand.exitCode === 0 ? "passed" : "failed"
|
|
2564
|
+
: checkSuites.some(one => one.status === "failed") ? "failed"
|
|
2565
|
+
: "not-run";
|
|
2524
2566
|
const suites = checkSuites.length > 0 ? checkSuites : verifyCommand.configured === false ? [] : [{
|
|
2525
2567
|
name: reused === null ? chosenCheck?.level === "quick" ? "Quick check" : "Project check" : "Project check (reused)",
|
|
2526
2568
|
status: checkStatus,
|
package/dist/chat-decide.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
|
|
13
13
|
* offer, mint and answer these through the same tokens (the `channel` column). Every act runs through the door the
|
|
14
14
|
* console uses. */
|
|
15
|
+
import { NIGHTLY_CHECK_ONLY, RELEASE_MERGES_ONLY, nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
15
16
|
import { isAcpProfile } from "./acp-agents.js";
|
|
16
17
|
import { DECIDE_ACTS, readDecideActionRow } from "./contracts/chat-callback-rows.js";
|
|
17
18
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -27,6 +28,7 @@ import { verifyApproverStanding } from "./principal.js";
|
|
|
27
28
|
import { pullRequestFollowOf } from "./pull-request-flow.js";
|
|
28
29
|
import { requestResultChanges } from "./result-actions.js";
|
|
29
30
|
import { revisionSourceOf } from "./result-review.js";
|
|
31
|
+
import { APPROVAL_PROOF, approvalOf, approvalProofDetail, quarantinedAuthor } from "./scope.js";
|
|
30
32
|
import { phoneText } from "./telegram-status.js";
|
|
31
33
|
/** The words, said once. */
|
|
32
34
|
export const DECIDE_LABELS = {
|
|
@@ -100,7 +102,9 @@ export function planInChat(store, taskId, approver, now, channel, root) {
|
|
|
100
102
|
const scope = store.getScope(taskId);
|
|
101
103
|
if (ref === null || ref.repo === null || scope === null)
|
|
102
104
|
return { ok: false, why: "This plan isn't available now." };
|
|
103
|
-
|
|
105
|
+
// Approved only as dispatch reads it (approval policy): an old seal a lead token made, or one whose authority has
|
|
106
|
+
// ended, is no approval, so the person can approve it here.
|
|
107
|
+
if (approvalOf(scope).approved && store.modeApprovalLive(ref.id, now))
|
|
104
108
|
return { ok: false, why: "This plan is already approved." };
|
|
105
109
|
if (ref.plan === "requested")
|
|
106
110
|
return { ok: false, why: "The plan is still being written." };
|
|
@@ -113,7 +117,7 @@ export function planInChat(store, taskId, approver, now, channel, root) {
|
|
|
113
117
|
if (family === null || family.problem !== null || family.current.id !== taskId)
|
|
114
118
|
return { ok: false, why: "A newer version of this task is current." };
|
|
115
119
|
// What the mode's seal refuses anyway, said first: model- or coordinator-written terms, an unreadable route.
|
|
116
|
-
if (ref.coordinatorCid !== null || (scope.proposedVia ?? null)
|
|
120
|
+
if (ref.coordinatorCid !== null || quarantinedAuthor(scope.proposedVia ?? null))
|
|
117
121
|
return { ok: false, why: "This plan was written for you, so you approve it in Toolroll." };
|
|
118
122
|
if (scope.profileState === "unresolved" || scope.routeEra == null || (scope.termsProblem ?? null) !== null)
|
|
119
123
|
return { ok: false, why: "This plan can't say exactly which agents run, so you approve it in Toolroll." };
|
|
@@ -158,6 +162,12 @@ export function planInChat(store, taskId, approver, now, channel, root) {
|
|
|
158
162
|
/** A ready pull request this person's chat may merge now, bound to the exact commit. */
|
|
159
163
|
export function mergeInChat(store, taskId, run, approver, now, channel) {
|
|
160
164
|
const ref = store.lookupRef(taskId);
|
|
165
|
+
// The nightly check of main never merges (release-once.md, part 3).
|
|
166
|
+
if (ref !== null && nightlyTaskRef(store, ref.id))
|
|
167
|
+
return { ok: false, why: NIGHTLY_CHECK_ONLY };
|
|
168
|
+
// A release's result merges only through its coordinator.
|
|
169
|
+
if (ref !== null && releaseTaskRef(store, ref.id))
|
|
170
|
+
return { ok: false, why: RELEASE_MERGES_ONLY };
|
|
161
171
|
const publication = store.publicationForRun(run);
|
|
162
172
|
const follow = publication === null ? null : pullRequestFollowOf(store, publication.id);
|
|
163
173
|
if (ref === null || ref.repo === null || publication === null || follow === null || publication.prNumber === null)
|
|
@@ -517,6 +527,11 @@ export function applyDecideTap(store, seat, input) {
|
|
|
517
527
|
// The owner's own yes, sealed as theirs ("chat"), never as a mode's: it stands like the console's after the mode or setting ends.
|
|
518
528
|
if (!store.sealScopeApproval(row.taskId, who.name, now, {}, { kind: "chat", modeDigest: plan.modeDigest }))
|
|
519
529
|
return notDone("the plan couldn't be approved from chat. Open it in Toolroll.");
|
|
530
|
+
// The person's proof beside the seal, bound to these exact words (approval policy §1.2): a paired-chat tap, never a
|
|
531
|
+
// password. Release, deploy and the protected counters read it as a person's.
|
|
532
|
+
const sealedDigest = store.getScope(row.taskId)?.approvedDigest ?? "";
|
|
533
|
+
store.recordAction({ at: now.toISOString(), actor: who.name, repo: ref.repo, taskId: row.taskId, runId: null, action: APPROVAL_PROOF, outcome: "chat",
|
|
534
|
+
source: "request", detail: approvalProofDetail(now.getTime(), store.accountOf(who.name)?.generation ?? 0, sealedDigest) });
|
|
520
535
|
store.recordAction({ at: now.toISOString(), actor: who.name, repo: ref.repo, taskId: row.taskId, runId: null, action: "plan approved in chat", outcome: "approved",
|
|
521
536
|
source: "request", detail: `via ${seat.channel} · chat binding #${seat.binding} · ${plan.source} ${plan.modeDigest}` });
|
|
522
537
|
return done("✓ Approved. Work starts when a worker is free.");
|
package/dist/check-access.js
CHANGED
|
@@ -17,11 +17,11 @@ export const widerThan = (a, b) => RANK[a] > RANK[b];
|
|
|
17
17
|
export const narrowestAccess = (levels) => levels.reduce((low, one) => RANK[one] < RANK[low] ? one : low, levels.length === 0 ? CHECK_ACCESS_DEFAULT : "unsandboxed");
|
|
18
18
|
/** The level as a person reads it, in a sentence's middle ("Checks: …"). */
|
|
19
19
|
export function checkAccessWords(level) {
|
|
20
|
-
return level === "sandboxed" ? "sandboxed" : level === "sign-ins" ? "sandboxed, with model and GitHub sign-ins
|
|
20
|
+
return level === "sandboxed" ? "sandboxed" : level === "sign-ins" ? "sandboxed, with model and GitHub sign-ins" : "not sandboxed";
|
|
21
21
|
}
|
|
22
22
|
/** What each wider level means, said before anyone chooses it. */
|
|
23
23
|
export const CHECK_ACCESS_WARNINGS = {
|
|
24
|
-
"sign-ins": "This project's checks can read your Claude, Codex and GitHub sign-ins, even where the project hides your home folder's files from builds.
|
|
24
|
+
"sign-ins": "This project's checks can read your Claude, Codex and GitHub sign-ins, even where the project hides your home folder's files from builds. Its tests could use or send those credentials anywhere its network setting allows. Only choose this for projects you trust.",
|
|
25
25
|
unsandboxed: "This project's checks get your full access to this machine and to Toolroll itself, including its database and every sign-in. Only for a project whose checks must create sandboxes themselves, such as Toolroll's own.",
|
|
26
26
|
};
|
|
27
27
|
/** The services whose sign-ins a `sign-ins` check reads: Claude's and Codex's own hosts (egress.ts), and GitHub's. */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type VerifiedEntry } from "./commit-extract.js";
|
|
2
|
+
import { CHECK_PARTS, type CheckContext, type CheckInventory, type CheckReport, type ObligationSnapshot, type TargetedPlan } from "./contracts/release-check.js";
|
|
3
|
+
import { type Store, type VerifyCommand } from "./store.js";
|
|
4
|
+
/** The sealed artifacts' capture labels: what a reader looks for, by exact name. */
|
|
5
|
+
export declare const CHECK_REPORT_CAPTURE = "release check report v1";
|
|
6
|
+
export declare const CHECK_INVENTORY_CAPTURE = "release check inventory v1";
|
|
7
|
+
export declare const TARGETED_PLAN_CAPTURE = "release check targeted plan v1";
|
|
8
|
+
export declare const OBLIGATION_SNAPSHOT_CAPTURE = "release check obligation snapshot v1";
|
|
9
|
+
export declare const CHECK_CONTEXT_CAPTURE = "release check context v1";
|
|
10
|
+
/** How large a sealed check artifact may be (the report's own bound). */
|
|
11
|
+
export declare const CHECK_ARTIFACT_CAP: number;
|
|
12
|
+
/** The release check's harness, named: the orchestrator, its runners and helpers, Vitest's configuration and setup, and
|
|
13
|
+
* the reporter. These never carry (release-carry.ts). The execution context digests far more (inHarnessDigest). */
|
|
14
|
+
export declare const HARNESS_FILES: readonly string[];
|
|
15
|
+
/** Vitest's include globs (vitest.config.ts), as the plane reads them: a change to that file is a harness change. */
|
|
16
|
+
export declare const UNIT_INCLUDE: readonly RegExp[];
|
|
17
|
+
export declare const isUnitFile: (path: string) => boolean;
|
|
18
|
+
/** The project's own Git directory as Toolroll records it: `<realpath(repo)>/.git`, a real directory. */
|
|
19
|
+
export declare function projectGitDir(repo: string): string | null;
|
|
20
|
+
/** A digest of a payload, over its canonical JSON. */
|
|
21
|
+
export declare const payloadDigest: (value: unknown) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a tree path is part of the harness for the execution context: every file EXCEPT the unit test files
|
|
24
|
+
* themselves, the two journey scripts, docs and Markdown. No import tracing: a harness dependency reached by any road
|
|
25
|
+
* (a `require`, a computed `import()`, a side-effect import, a `src/` helper, a config file) is inside this set, so a
|
|
26
|
+
* change to it always changes the digest (fail closed).
|
|
27
|
+
*/
|
|
28
|
+
export declare function inHarnessDigest(path: string): boolean;
|
|
29
|
+
/** What the plane reads from a commit for a check: its tree's entries, its unit files and its harness. */
|
|
30
|
+
export type TreeFacts = {
|
|
31
|
+
tree: string;
|
|
32
|
+
entries: VerifiedEntry[];
|
|
33
|
+
units: string[];
|
|
34
|
+
harness: string[];
|
|
35
|
+
harnessDigest: string;
|
|
36
|
+
lockfile: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** The tree facts of `sha`, from objects hashed here (never the checkout). Null with why when they can't be read. */
|
|
39
|
+
export declare function treeFactsOf(gitDir: string, sha: string): {
|
|
40
|
+
ok: true;
|
|
41
|
+
facts: TreeFacts;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
problem: string;
|
|
45
|
+
};
|
|
46
|
+
/** The execution context a check runs in: what must match for results to stand beside each other. The machine's load
|
|
47
|
+
* budget is left out: it changes how fast a check runs, never what passes (scripts/release-check.mjs checkBudget). */
|
|
48
|
+
export declare function checkContextOf(facts: TreeFacts, command: VerifyCommand, access: string): CheckContext;
|
|
49
|
+
/** The parts a full check selects, as far as the plane knows them before it runs. */
|
|
50
|
+
export declare const FULL_PARTS: readonly (typeof CHECK_PARTS[number])[];
|
|
51
|
+
/** What a check must account for, sealed before it runs: every unit file of the tree, whatever runs (a targeted
|
|
52
|
+
* check accounts for the rest through its base), and the parts it selects. */
|
|
53
|
+
export declare function inventoryOf(facts: TreeFacts, plan: TargetedPlan | null): CheckInventory;
|
|
54
|
+
/** A check about to run: its handshake (written on stdin, then closed) and the facts sealed with it. */
|
|
55
|
+
export type PreparedAttempt = {
|
|
56
|
+
runId: number;
|
|
57
|
+
nonce: string;
|
|
58
|
+
stdin: string;
|
|
59
|
+
mode: "full" | "targeted";
|
|
60
|
+
inventory: CheckInventory;
|
|
61
|
+
context: CheckContext;
|
|
62
|
+
plan: TargetedPlan | null;
|
|
63
|
+
snapshot: ObligationSnapshot | null;
|
|
64
|
+
/** The identities the check was asked to report one by one (required ones). */
|
|
65
|
+
watch: string[];
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Seal a check's attempt row before its command is spawned: intended mode, nonce hash, inventory and context (and for a
|
|
69
|
+
* targeted check its plan and obligation snapshot, sealed as artifacts first). Null when the plane can't read the
|
|
70
|
+
* commit, which leaves the check running exactly as it always has, with no handshake.
|
|
71
|
+
*/
|
|
72
|
+
export declare function prepareCheckAttempt(store: Store, root: string, input: {
|
|
73
|
+
runId: number;
|
|
74
|
+
repo: string;
|
|
75
|
+
head: string;
|
|
76
|
+
command: VerifyCommand;
|
|
77
|
+
access: string;
|
|
78
|
+
authority?: number | null;
|
|
79
|
+
plan?: TargetedPlan | null;
|
|
80
|
+
snapshot?: ObligationSnapshot | null;
|
|
81
|
+
watch?: readonly string[];
|
|
82
|
+
}, now: Date): PreparedAttempt | null;
|
|
83
|
+
/** Seal one JSON payload as a structured-output artifact under its capture label. */
|
|
84
|
+
export declare function sealJson(store: Store, root: string, runId: number, name: string, value: unknown, capture: string, now: Date): number;
|
|
85
|
+
/**
|
|
86
|
+
* The check's report from its output: the LAST `TOOLROLL_CHECK_REPORT <json>` line carrying this nonce, at most 256 KiB,
|
|
87
|
+
* that matches the contract. Lines without the nonce (a test's print, a leftover process's write) are passed over.
|
|
88
|
+
*/
|
|
89
|
+
export declare function reportFromOutput(stdout: string, nonce: string): {
|
|
90
|
+
ok: true;
|
|
91
|
+
report: CheckReport;
|
|
92
|
+
raw: string;
|
|
93
|
+
} | {
|
|
94
|
+
ok: false;
|
|
95
|
+
problem: string;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* After the check: seal its report (when it gave a valid one) and the inventory artifact. `exitCode` is the check's own
|
|
99
|
+
* exit; a report that disagrees with it is no report. Returns the report, or null.
|
|
100
|
+
*/
|
|
101
|
+
export declare function sealCheckOutcome(store: Store, root: string, attempt: PreparedAttempt, output: {
|
|
102
|
+
stdout: string;
|
|
103
|
+
exitCode: number | null;
|
|
104
|
+
}, now: Date): CheckReport | null;
|
|
105
|
+
/** One sealed check artifact of a run, read back and verified, or null (missing, ambiguous, cut short or changed). */
|
|
106
|
+
export declare function readSealedJson(store: Store, root: string, runId: number, capture: string): {
|
|
107
|
+
value: unknown;
|
|
108
|
+
digest: string;
|
|
109
|
+
} | null;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release check's structured report, on the plane's side (docs/plans/release-once.md, part 2 and rollout phase A).
|
|
3
|
+
*
|
|
4
|
+
* Before a Full check of a prepared candidate runs, the builder seals a `release_check_attempt` row: the intended mode
|
|
5
|
+
* (`full`, or `targeted` with its plan and obligation snapshot), the hash of a fresh nonce, the inventory the check must
|
|
6
|
+
* account for (the unit files, from the verified tree and Vitest's include globs, and the parts selected) and the
|
|
7
|
+
* execution context. It then writes `{"nonce","mode","plan","watch"}` on the check's stdin and closes it.
|
|
8
|
+
*
|
|
9
|
+
* After the check, the report is the last `TOOLROLL_CHECK_REPORT <json>` line of its output that carries that nonce
|
|
10
|
+
* (a line a test prints, or one a leftover process writes, has no nonce and is ignored). It is read strictly and sealed
|
|
11
|
+
* as `check-report.json`, beside `check-inventory.json` (what was sealed, and the groups and versions the check
|
|
12
|
+
* declared). A run with no valid report still passes or fails as before; it just can't be the base of a targeted check.
|
|
13
|
+
*
|
|
14
|
+
* Coverage trusts test code (the trust decision in the plan). Nothing here decides who may release or deploy.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
17
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { readVerifiedCommit, verifiedTreeEntries } from "./commit-extract.js";
|
|
20
|
+
import { CHECK_PARTS, CHECK_REPORT_MAX_BYTES, CHECK_REPORT_PREFIX, readCheckReport, } from "./contracts/release-check.js";
|
|
21
|
+
import { readVerifiedArtifact, storeEvidence } from "./evidence.js";
|
|
22
|
+
import { checkAttemptOf, recordCheckAttempt } from "./store.js";
|
|
23
|
+
/** The sealed artifacts' capture labels: what a reader looks for, by exact name. */
|
|
24
|
+
export const CHECK_REPORT_CAPTURE = "release check report v1";
|
|
25
|
+
export const CHECK_INVENTORY_CAPTURE = "release check inventory v1";
|
|
26
|
+
export const TARGETED_PLAN_CAPTURE = "release check targeted plan v1";
|
|
27
|
+
export const OBLIGATION_SNAPSHOT_CAPTURE = "release check obligation snapshot v1";
|
|
28
|
+
export const CHECK_CONTEXT_CAPTURE = "release check context v1";
|
|
29
|
+
/** How large a sealed check artifact may be (the report's own bound). */
|
|
30
|
+
export const CHECK_ARTIFACT_CAP = CHECK_REPORT_MAX_BYTES;
|
|
31
|
+
/** The release check's harness, named: the orchestrator, its runners and helpers, Vitest's configuration and setup, and
|
|
32
|
+
* the reporter. These never carry (release-carry.ts). The execution context digests far more (inHarnessDigest). */
|
|
33
|
+
export const HARNESS_FILES = Object.freeze([
|
|
34
|
+
"scripts/release-check.mjs", "scripts/release-check-ci.mjs", "scripts/suite-lifecycle.mjs", "scripts/provider-gate.mjs",
|
|
35
|
+
"scripts/e2e-parallel.mjs", "scripts/e2e-kit.mjs", "scripts/upgrade-path.mjs", "scripts/check-results.mjs",
|
|
36
|
+
"vitest.config.ts", "test/attacks/vitest.config.ts",
|
|
37
|
+
"test/setup-state.ts", "test/fresh-store.ts", "test/temp-root.ts", "test/ensure-build.ts", "test/release-reporter.ts",
|
|
38
|
+
]);
|
|
39
|
+
/** Vitest's include globs (vitest.config.ts), as the plane reads them: a change to that file is a harness change. */
|
|
40
|
+
export const UNIT_INCLUDE = Object.freeze([/^src\/.+\.test\.ts$/, /^test\/attacks\/.+\.test\.ts$/]);
|
|
41
|
+
export const isUnitFile = (path) => UNIT_INCLUDE.some(one => one.test(path));
|
|
42
|
+
/** The project's own Git directory as Toolroll records it: `<realpath(repo)>/.git`, a real directory. */
|
|
43
|
+
export function projectGitDir(repo) {
|
|
44
|
+
try {
|
|
45
|
+
const named = join(realpathSync(repo), ".git");
|
|
46
|
+
return lstatSync(named).isDirectory() ? realpathSync(named) : null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
53
|
+
/** A digest of a payload, over its canonical JSON. */
|
|
54
|
+
export const payloadDigest = (value) => sha256(JSON.stringify(value));
|
|
55
|
+
/**
|
|
56
|
+
* Whether a tree path is part of the harness for the execution context: every file EXCEPT the unit test files
|
|
57
|
+
* themselves, the two journey scripts, docs and Markdown. No import tracing: a harness dependency reached by any road
|
|
58
|
+
* (a `require`, a computed `import()`, a side-effect import, a `src/` helper, a config file) is inside this set, so a
|
|
59
|
+
* change to it always changes the digest (fail closed).
|
|
60
|
+
*/
|
|
61
|
+
export function inHarnessDigest(path) {
|
|
62
|
+
if (isUnitFile(path))
|
|
63
|
+
return false;
|
|
64
|
+
if (path === "scripts/app-e2e.mjs" || path === "scripts/flows-e2e.mjs")
|
|
65
|
+
return false;
|
|
66
|
+
if (path.startsWith("docs/") || path.endsWith(".md"))
|
|
67
|
+
return false;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/** The tree facts of `sha`, from objects hashed here (never the checkout). Null with why when they can't be read. */
|
|
71
|
+
export function treeFactsOf(gitDir, sha) {
|
|
72
|
+
const commit = readVerifiedCommit(gitDir, sha);
|
|
73
|
+
if (!commit.ok)
|
|
74
|
+
return { ok: false, problem: commit.message };
|
|
75
|
+
const entries = verifiedTreeEntries(gitDir, commit.value.tree);
|
|
76
|
+
if (!entries.ok)
|
|
77
|
+
return { ok: false, problem: entries.message };
|
|
78
|
+
const harness = entries.value.filter(one => inHarnessDigest(one.path)).map(one => one.path).sort();
|
|
79
|
+
const ids = new Map(entries.value.map(one => [one.path, one.id]));
|
|
80
|
+
return { ok: true, facts: {
|
|
81
|
+
tree: commit.value.tree,
|
|
82
|
+
entries: entries.value,
|
|
83
|
+
units: entries.value.filter(one => (one.mode === "100644" || one.mode === "100755") && isUnitFile(one.path)).map(one => one.path).sort(),
|
|
84
|
+
harness,
|
|
85
|
+
harnessDigest: payloadDigest(entries.value.filter(one => inHarnessDigest(one.path)).map(one => [one.path, one.mode, one.id])),
|
|
86
|
+
lockfile: ids.get("package-lock.json") ?? null,
|
|
87
|
+
} };
|
|
88
|
+
}
|
|
89
|
+
/** The execution context a check runs in: what must match for results to stand beside each other. The machine's load
|
|
90
|
+
* budget is left out: it changes how fast a check runs, never what passes (scripts/release-check.mjs checkBudget). */
|
|
91
|
+
export function checkContextOf(facts, command, access) {
|
|
92
|
+
return { version: 1, node: process.version, platform: process.platform, arch: process.arch, lockfile: facts.lockfile, harness: facts.harnessDigest, grant: { id: command.id, digest: command.digest }, access };
|
|
93
|
+
}
|
|
94
|
+
/** The parts a full check selects, as far as the plane knows them before it runs. */
|
|
95
|
+
export const FULL_PARTS = CHECK_PARTS;
|
|
96
|
+
/** What a check must account for, sealed before it runs: every unit file of the tree, whatever runs (a targeted
|
|
97
|
+
* check accounts for the rest through its base), and the parts it selects. */
|
|
98
|
+
export function inventoryOf(facts, plan) {
|
|
99
|
+
return { version: 1, units: facts.units, parts: plan === null ? [...FULL_PARTS] : ["typecheck", "build", ...plan.parts.filter(one => one !== "typecheck" && one !== "build")] };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Seal a check's attempt row before its command is spawned: intended mode, nonce hash, inventory and context (and for a
|
|
103
|
+
* targeted check its plan and obligation snapshot, sealed as artifacts first). Null when the plane can't read the
|
|
104
|
+
* commit, which leaves the check running exactly as it always has, with no handshake.
|
|
105
|
+
*/
|
|
106
|
+
export function prepareCheckAttempt(store, root, input, now) {
|
|
107
|
+
if (checkAttemptOf(store, input.runId) !== null)
|
|
108
|
+
return null;
|
|
109
|
+
const gitDir = projectGitDir(input.repo);
|
|
110
|
+
if (gitDir === null)
|
|
111
|
+
return null;
|
|
112
|
+
const read = treeFactsOf(gitDir, input.head);
|
|
113
|
+
if (!read.ok)
|
|
114
|
+
return null;
|
|
115
|
+
const plan = input.plan ?? null, snapshot = input.snapshot ?? null;
|
|
116
|
+
const inventory = inventoryOf(read.facts, plan);
|
|
117
|
+
const context = checkContextOf(read.facts, input.command, input.access);
|
|
118
|
+
const nonce = randomBytes(32).toString("hex");
|
|
119
|
+
const mode = plan === null ? "full" : "targeted";
|
|
120
|
+
sealJson(store, root, input.runId, "check-context.json", context, CHECK_CONTEXT_CAPTURE, now);
|
|
121
|
+
if (plan !== null)
|
|
122
|
+
sealJson(store, root, input.runId, "targeted-plan.json", plan, TARGETED_PLAN_CAPTURE, now);
|
|
123
|
+
if (snapshot !== null)
|
|
124
|
+
sealJson(store, root, input.runId, "obligation-snapshot.json", snapshot, OBLIGATION_SNAPSHOT_CAPTURE, now);
|
|
125
|
+
recordCheckAttempt(store, {
|
|
126
|
+
run: input.runId, authority: input.authority ?? null, mode, baseRun: plan?.base ?? null,
|
|
127
|
+
planDigest: plan === null ? null : payloadDigest(plan), snapshotDigest: snapshot === null ? null : payloadDigest(snapshot),
|
|
128
|
+
inventoryDigest: payloadDigest(inventory), contextDigest: payloadDigest(context), nonceHash: sha256(nonce),
|
|
129
|
+
}, now);
|
|
130
|
+
const stdin = JSON.stringify({ nonce, mode, plan, watch: [...(input.watch ?? [])] });
|
|
131
|
+
return { runId: input.runId, nonce, stdin, mode, inventory, context, plan, snapshot, watch: [...(input.watch ?? [])] };
|
|
132
|
+
}
|
|
133
|
+
/** Seal one JSON payload as a structured-output artifact under its capture label. */
|
|
134
|
+
export function sealJson(store, root, runId, name, value, capture, now) {
|
|
135
|
+
const bytes = Buffer.from(JSON.stringify(value), "utf8");
|
|
136
|
+
if (bytes.length > CHECK_ARTIFACT_CAP)
|
|
137
|
+
throw new Error(`${name} is over ${CHECK_ARTIFACT_CAP} bytes`);
|
|
138
|
+
return storeEvidence(store, root, runId, "structured-output", name, bytes, capture, now, { captureStatus: "ok", cap: CHECK_ARTIFACT_CAP });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The check's report from its output: the LAST `TOOLROLL_CHECK_REPORT <json>` line carrying this nonce, at most 256 KiB,
|
|
142
|
+
* that matches the contract. Lines without the nonce (a test's print, a leftover process's write) are passed over.
|
|
143
|
+
*/
|
|
144
|
+
export function reportFromOutput(stdout, nonce) {
|
|
145
|
+
const lines = stdout.split("\n");
|
|
146
|
+
for (let at = lines.length - 1; at >= 0; at--) {
|
|
147
|
+
const line = lines[at];
|
|
148
|
+
if (!line.startsWith(CHECK_REPORT_PREFIX))
|
|
149
|
+
continue;
|
|
150
|
+
if (Buffer.byteLength(line) > CHECK_REPORT_MAX_BYTES)
|
|
151
|
+
continue;
|
|
152
|
+
const raw = line.slice(CHECK_REPORT_PREFIX.length);
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(raw);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (parsed === null || typeof parsed !== "object" || parsed["nonce"] !== nonce)
|
|
161
|
+
continue;
|
|
162
|
+
const read = readCheckReport(parsed);
|
|
163
|
+
if (!read.ok)
|
|
164
|
+
return { ok: false, problem: `The check's report doesn't match its contract: ${read.issues.slice(0, 3).map(one => one.line).join("; ")}` };
|
|
165
|
+
return { ok: true, report: read.value, raw };
|
|
166
|
+
}
|
|
167
|
+
return { ok: false, problem: "The check gave no report for this run." };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* After the check: seal its report (when it gave a valid one) and the inventory artifact. `exitCode` is the check's own
|
|
171
|
+
* exit; a report that disagrees with it is no report. Returns the report, or null.
|
|
172
|
+
*/
|
|
173
|
+
export function sealCheckOutcome(store, root, attempt, output, now) {
|
|
174
|
+
const read = reportFromOutput(output.stdout, attempt.nonce);
|
|
175
|
+
const report = read.ok && read.report.mode === attempt.mode && read.report.exitCode === output.exitCode &&
|
|
176
|
+
read.report.planDigest === (attempt.plan === null ? null : payloadDigest(attempt.plan)) ? read.report : null;
|
|
177
|
+
const declared = report === null ? { groups: [], versions: [] } : {
|
|
178
|
+
groups: report.journeys.map(one => ({ script: one.script, kind: one.kind, groups: one.groups.map(group => group.group) })),
|
|
179
|
+
versions: report.upgrades?.versions.map(one => one.version) ?? [],
|
|
180
|
+
};
|
|
181
|
+
const sealedInventory = { version: 1, sealed: attempt.inventory, declared };
|
|
182
|
+
sealJson(store, root, attempt.runId, "check-inventory.json", sealedInventory, CHECK_INVENTORY_CAPTURE, now);
|
|
183
|
+
if (report !== null)
|
|
184
|
+
sealJson(store, root, attempt.runId, "check-report.json", report, CHECK_REPORT_CAPTURE, now);
|
|
185
|
+
return report;
|
|
186
|
+
}
|
|
187
|
+
/** One sealed check artifact of a run, read back and verified, or null (missing, ambiguous, cut short or changed). */
|
|
188
|
+
export function readSealedJson(store, root, runId, capture) {
|
|
189
|
+
const found = store.artifactsFor(runId).filter(one => one.kind === "structured-output" && one.capture === capture);
|
|
190
|
+
if (found.length !== 1)
|
|
191
|
+
return null;
|
|
192
|
+
const artifact = found[0];
|
|
193
|
+
if (artifact.truncated || artifact.redacted || artifact.captureStatus !== "ok")
|
|
194
|
+
return null;
|
|
195
|
+
const read = readVerifiedArtifact(root, artifact);
|
|
196
|
+
if (!read.ok)
|
|
197
|
+
return null;
|
|
198
|
+
try {
|
|
199
|
+
return { value: JSON.parse(read.content.toString("utf8")), digest: artifact.sha256 };
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
package/dist/claim.js
CHANGED
|
@@ -128,7 +128,7 @@ function acquireLocked(store, taskRef, runner, options) {
|
|
|
128
128
|
return {
|
|
129
129
|
ok: false,
|
|
130
130
|
reason: "mode-ended",
|
|
131
|
-
message: "the
|
|
131
|
+
message: "the approval it was sealed with no longer stands (an ended mode or flow grant, or an approval that wasn't a person's) — a person approves it again",
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
// A LEAD SWEEP'S LIMIT, inside this write transaction so two runners can't both take the last slot: at most
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type Extraction = {
|
|
2
|
+
ok: true;
|
|
3
|
+
files: number;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
/** What one extraction may hold, all checked before anything is written: the bytes written (every path, a blob shared
|
|
9
|
+
* by many paths counted once per path), the files, the tree entries walked and how deep the trees nest. */
|
|
10
|
+
export type ExtractionLimits = {
|
|
11
|
+
bytes: number;
|
|
12
|
+
files: number;
|
|
13
|
+
entries: number;
|
|
14
|
+
depth: number;
|
|
15
|
+
};
|
|
16
|
+
export declare const EXTRACTION_LIMITS: ExtractionLimits;
|
|
17
|
+
/** A tree path that stays where it says: relative, no empty, `.` or `..` part, no backslash or control character. */
|
|
18
|
+
export declare function normalizedTreePath(path: string): boolean;
|
|
19
|
+
/** The object id Git gives `<type> <size>\0<bytes>` in this repository's object format. */
|
|
20
|
+
export declare const objectIdOf: (format: "sha1" | "sha256", type: string, bytes: Buffer) => string;
|
|
21
|
+
type TreeEntry = {
|
|
22
|
+
mode: string;
|
|
23
|
+
name: string;
|
|
24
|
+
id: string;
|
|
25
|
+
};
|
|
26
|
+
/** A tree object's entries, parsed from its verified bytes: `<mode> <name>\0<raw id>` each. Null when malformed. */
|
|
27
|
+
export declare function parseTree(bytes: Buffer, idBytes: number): TreeEntry[] | null;
|
|
28
|
+
/** Write commit `sha` of the repository whose Git directory is `gitDir` (a realpath Toolroll took from its own
|
|
29
|
+
* records) into `dest`, an empty folder this process made. Every object on the way — the commit, each tree, each
|
|
30
|
+
* blob — is hashed here and must be the id that names it: Git's own answer about an id is never taken on trust, so a
|
|
31
|
+
* corrupted or replaced object in a store a build could write to refuses the extraction. */
|
|
32
|
+
export declare function extractCommit(gitDir: string, sha: string, dest: string, limits?: ExtractionLimits): Extraction;
|
|
33
|
+
/** A commit read from its verified bytes: its tree and its parents, in order. */
|
|
34
|
+
export type VerifiedCommit = {
|
|
35
|
+
id: string;
|
|
36
|
+
tree: string;
|
|
37
|
+
parents: string[];
|
|
38
|
+
message: string;
|
|
39
|
+
};
|
|
40
|
+
/** One entry of a verified tree, at its full path: a file (100644, 100755), a symlink (120000) or a submodule (160000). */
|
|
41
|
+
export type VerifiedEntry = {
|
|
42
|
+
path: string;
|
|
43
|
+
mode: string;
|
|
44
|
+
id: string;
|
|
45
|
+
};
|
|
46
|
+
/** One path that differs between two verified trees, with its line counts when both sides are text. */
|
|
47
|
+
export type TreeChange = {
|
|
48
|
+
path: string;
|
|
49
|
+
kind: "added" | "modified" | "deleted";
|
|
50
|
+
before: VerifiedEntry | null;
|
|
51
|
+
after: VerifiedEntry | null;
|
|
52
|
+
added: number;
|
|
53
|
+
deleted: number;
|
|
54
|
+
binary: boolean;
|
|
55
|
+
};
|
|
56
|
+
export type Verified<T> = {
|
|
57
|
+
ok: true;
|
|
58
|
+
value: T;
|
|
59
|
+
} | {
|
|
60
|
+
ok: false;
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
/** One commit, hashed here and parsed from its own bytes. */
|
|
64
|
+
export declare function readVerifiedCommit(gitDir: string, sha: string): Verified<VerifiedCommit>;
|
|
65
|
+
/**
|
|
66
|
+
* The commits from `from` back to `until` along first parents, newest first, `until` itself left out. Refuses when
|
|
67
|
+
* `until` isn't reached within `max` commits (it isn't an ancestor, or too much lies between), and hands back each
|
|
68
|
+
* commit's full parent list so a caller can refuse merges.
|
|
69
|
+
*/
|
|
70
|
+
export declare function verifiedFirstParentWalk(gitDir: string, from: string, until: string, max?: number): Verified<VerifiedCommit[]>;
|
|
71
|
+
/** Every entry of a verified tree at its full path, symlinks and submodules included (a caller decides on them). An
|
|
72
|
+
* unnormalized path, or one a tree lists twice, refuses the whole read. */
|
|
73
|
+
export declare function verifiedTreeEntries(gitDir: string, tree: string, limits?: ExtractionLimits): Verified<VerifiedEntry[]>;
|
|
74
|
+
/** Whether bytes look binary: a NUL in the first 8000 bytes, as Git judges. */
|
|
75
|
+
export declare const looksBinary: (bytes: Buffer) => boolean;
|
|
76
|
+
/** The lines added and deleted from `before` to `after`, by the shortest edit (Myers): each counted once, stopped at
|
|
77
|
+
* `cap` total edits (the counts then add up to more than `cap`, which every limit here refuses). */
|
|
78
|
+
export declare function lineChanges(before: Buffer, after: Buffer, cap?: number): {
|
|
79
|
+
added: number;
|
|
80
|
+
deleted: number;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* What differs between two verified trees, path by path (no rename pairing: a moved file is a deletion and an
|
|
84
|
+
* addition). Text files get their added and deleted lines; a binary side, a symlink or a submodule is marked so a caller
|
|
85
|
+
* can refuse it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function verifiedTreeDiff(gitDir: string, beforeTree: string, afterTree: string): Verified<TreeChange[]>;
|
|
88
|
+
/** The ids of verified blobs at these paths of a tree (absent paths left out): a digest's inputs, read without
|
|
89
|
+
* running anything. */
|
|
90
|
+
export declare function verifiedBlobs(gitDir: string, tree: string, paths: readonly string[]): Verified<Map<string, {
|
|
91
|
+
id: string;
|
|
92
|
+
bytes: Buffer;
|
|
93
|
+
}>>;
|
|
94
|
+
export {};
|