approval-md 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -914
- package/SPEC.md +101 -13
- package/cli.js +134 -7
- package/dist/src/adapters/smtp.d.ts +18 -0
- package/dist/src/adapters/smtp.js +24 -3
- package/dist/src/adapters/smtp.js.map +1 -1
- package/dist/src/channels/contract.d.ts +72 -0
- package/dist/src/channels/contract.js +161 -1
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/relay-server.d.ts +90 -0
- package/dist/src/channels/relay-server.js +295 -0
- package/dist/src/channels/relay-server.js.map +1 -0
- package/dist/src/channels/relay.d.ts +273 -0
- package/dist/src/channels/relay.js +564 -0
- package/dist/src/channels/relay.js.map +1 -0
- package/dist/src/channels/tagging.d.ts +25 -0
- package/dist/src/channels/tagging.js +63 -6
- package/dist/src/channels/tagging.js.map +1 -1
- package/dist/src/channels/telegram-minimal.d.ts +251 -0
- package/dist/src/channels/telegram-minimal.js +618 -0
- package/dist/src/channels/telegram-minimal.js.map +1 -0
- package/dist/src/channels/telegram-webhook.d.ts +273 -0
- package/dist/src/channels/telegram-webhook.js +536 -0
- package/dist/src/channels/telegram-webhook.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +725 -20
- package/dist/src/channels/telegram.js +1332 -123
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/cli/amend.js +358 -5
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.js +43 -6
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit-card.d.ts +1 -1
- package/dist/src/cli/audit-card.js +40 -1
- package/dist/src/cli/audit-card.js.map +1 -1
- package/dist/src/cli/audit.d.ts +7 -5
- package/dist/src/cli/audit.js +22 -10
- package/dist/src/cli/audit.js.map +1 -1
- package/dist/src/cli/channel-relay.d.ts +108 -0
- package/dist/src/cli/channel-relay.js +334 -0
- package/dist/src/cli/channel-relay.js.map +1 -0
- package/dist/src/cli/channel-telegram-webhook.d.ts +275 -0
- package/dist/src/cli/channel-telegram-webhook.js +716 -0
- package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +347 -28
- package/dist/src/cli/channel-telegram.js +648 -56
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +1 -1
- package/dist/src/cli/channel.js +5 -1
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/daemon.js +37 -5
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +63 -0
- package/dist/src/cli/doctor.js +856 -111
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.d.ts +29 -1
- package/dist/src/cli/execute.js +387 -21
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +12 -0
- package/dist/src/cli/exit-codes.js +13 -0
- package/dist/src/cli/exit-codes.js.map +1 -1
- package/dist/src/cli/gate.d.ts +2 -0
- package/dist/src/cli/gate.js +233 -2
- package/dist/src/cli/gate.js.map +1 -1
- package/dist/src/cli/help.d.ts +10 -1
- package/dist/src/cli/help.js +283 -54
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook.d.ts +485 -6
- package/dist/src/cli/hook.js +2370 -70
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/init.js +16 -7
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.js +24 -3
- package/dist/src/cli/instructions.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +55 -0
- package/dist/src/cli/log-anchor.js +199 -60
- package/dist/src/cli/log-anchor.js.map +1 -1
- package/dist/src/cli/log-unlock.d.ts +23 -0
- package/dist/src/cli/log-unlock.js +109 -0
- package/dist/src/cli/log-unlock.js.map +1 -0
- package/dist/src/cli/main.d.ts +0 -12
- package/dist/src/cli/main.js +72 -4
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/muse.d.ts +2 -0
- package/dist/src/cli/muse.js +62 -0
- package/dist/src/cli/muse.js.map +1 -0
- package/dist/src/cli/payload.js +79 -1
- package/dist/src/cli/payload.js.map +1 -1
- package/dist/src/cli/scaffold.js +6 -0
- package/dist/src/cli/scaffold.js.map +1 -1
- package/dist/src/cli/serve.d.ts +53 -0
- package/dist/src/cli/serve.js +270 -0
- package/dist/src/cli/serve.js.map +1 -0
- package/dist/src/cli/up.js +26 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +2 -0
- package/dist/src/cli/verb-registry.js +338 -39
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/core/attest-bootstrap.d.ts +95 -0
- package/dist/src/core/attest-bootstrap.js +135 -0
- package/dist/src/core/attest-bootstrap.js.map +1 -0
- package/dist/src/core/attest.d.ts +20 -3
- package/dist/src/core/attest.js +25 -0
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +153 -12
- package/dist/src/core/audit.js +415 -20
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-lease.d.ts +322 -0
- package/dist/src/core/channel-lease.js +663 -0
- package/dist/src/core/channel-lease.js.map +1 -0
- package/dist/src/core/command-class.d.ts +51 -0
- package/dist/src/core/command-class.js +766 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/daemon-host.d.ts +178 -0
- package/dist/src/core/daemon-host.js +228 -0
- package/dist/src/core/daemon-host.js.map +1 -0
- package/dist/src/core/daemon-identity.d.ts +238 -0
- package/dist/src/core/daemon-identity.js +252 -0
- package/dist/src/core/daemon-identity.js.map +1 -0
- package/dist/src/core/gate.d.ts +364 -6
- package/dist/src/core/gate.js +802 -27
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/harness-version.d.ts +128 -4
- package/dist/src/core/harness-version.js +179 -12
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +82 -3
- package/dist/src/core/harness-wait.js +89 -3
- package/dist/src/core/harness-wait.js.map +1 -1
- package/dist/src/core/log-lock.d.ts +435 -0
- package/dist/src/core/log-lock.js +1093 -0
- package/dist/src/core/log-lock.js.map +1 -0
- package/dist/src/core/log-subscribe.d.ts +21 -0
- package/dist/src/core/log-subscribe.js +16 -6
- package/dist/src/core/log-subscribe.js.map +1 -1
- package/dist/src/core/log-write-layer.d.ts +55 -0
- package/dist/src/core/log-write-layer.js +91 -0
- package/dist/src/core/log-write-layer.js.map +1 -0
- package/dist/src/core/log.d.ts +130 -22
- package/dist/src/core/log.js +473 -50
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/payload-census.d.ts +51 -0
- package/dist/src/core/payload-census.js +125 -2
- package/dist/src/core/payload-census.js.map +1 -1
- package/dist/src/core/payload-store.d.ts +31 -0
- package/dist/src/core/payload-store.js +113 -8
- package/dist/src/core/payload-store.js.map +1 -1
- package/dist/src/core/payload.d.ts +18 -3
- package/dist/src/core/payload.js +20 -4
- package/dist/src/core/payload.js.map +1 -1
- package/dist/src/core/policy-diff.d.ts +10 -0
- package/dist/src/core/policy-diff.js +10 -2
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +7 -1
- package/dist/src/core/policy-explain.js +16 -1
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +24 -0
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +38 -0
- package/dist/src/core/policy-match.js +35 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +40 -0
- package/dist/src/core/policy-proposal.js +37 -16
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/prompt-layout.d.ts +186 -1
- package/dist/src/core/prompt-layout.js +345 -3
- package/dist/src/core/prompt-layout.js.map +1 -1
- package/dist/src/core/read-scope.d.ts +10 -0
- package/dist/src/core/read-scope.js +4 -0
- package/dist/src/core/read-scope.js.map +1 -1
- package/dist/src/core/run-payload.d.ts +160 -0
- package/dist/src/core/run-payload.js +304 -0
- package/dist/src/core/run-payload.js.map +1 -0
- package/dist/src/core/sender-identity.d.ts +59 -11
- package/dist/src/core/sender-identity.js +79 -11
- package/dist/src/core/sender-identity.js.map +1 -1
- package/dist/src/core/state.d.ts +52 -1
- package/dist/src/core/state.js +52 -6
- package/dist/src/core/state.js.map +1 -1
- package/dist/src/core/telegram-config.d.ts +79 -0
- package/dist/src/core/telegram-config.js +108 -0
- package/dist/src/core/telegram-config.js.map +1 -1
- package/dist/src/core/validate.d.ts +20 -6
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -1
- package/dist/src/core/verify.d.ts +23 -1
- package/dist/src/core/verify.js +20 -2
- package/dist/src/core/verify.js.map +1 -1
- package/dist/src/daemon/audit.d.ts +80 -5
- package/dist/src/daemon/audit.js +126 -6
- package/dist/src/daemon/audit.js.map +1 -1
- package/dist/src/daemon/daemon.d.ts +174 -2
- package/dist/src/daemon/daemon.js +298 -7
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +13 -2
- package/dist/src/daemon/projection.js +39 -6
- package/dist/src/daemon/projection.js.map +1 -1
- package/dist/src/mcp/server.d.ts +39 -3
- package/dist/src/mcp/server.js +22 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/muse/server.d.ts +16 -0
- package/dist/src/muse/server.js +238 -0
- package/dist/src/muse/server.js.map +1 -0
- package/dist/src/serve/archive.d.ts +166 -0
- package/dist/src/serve/archive.js +363 -0
- package/dist/src/serve/archive.js.map +1 -0
- package/dist/src/serve/arguments.d.ts +93 -0
- package/dist/src/serve/arguments.js +228 -0
- package/dist/src/serve/arguments.js.map +1 -0
- package/dist/src/serve/credentials.d.ts +91 -0
- package/dist/src/serve/credentials.js +143 -0
- package/dist/src/serve/credentials.js.map +1 -0
- package/dist/src/serve/follow.d.ts +86 -0
- package/dist/src/serve/follow.js +101 -0
- package/dist/src/serve/follow.js.map +1 -0
- package/dist/src/serve/hook-thread.d.ts +134 -0
- package/dist/src/serve/hook-thread.js +333 -0
- package/dist/src/serve/hook-thread.js.map +1 -0
- package/dist/src/serve/hook-worker.d.ts +134 -0
- package/dist/src/serve/hook-worker.js +173 -0
- package/dist/src/serve/hook-worker.js.map +1 -0
- package/dist/src/serve/server.d.ts +412 -0
- package/dist/src/serve/server.js +1251 -0
- package/dist/src/serve/server.js.map +1 -0
- package/docs/README-extended.md +768 -0
- package/docs/cli-reference.md +2139 -67
- package/package.json +4 -2
- package/schema/event.schema.json +413 -30
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-age-without-created.json +19 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-lockfile-path.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-operator-cleared-system-actor.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-unknown-reason.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-without-age.json +13 -0
- package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +6 -1
- package/schema/fixtures/event/invalid/audit-reviewed-bad-payload-hash.json +21 -0
- package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +5 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-subject-hash.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-subject-seq.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-verdict-source.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-verdict.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +6 -1
- package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
- package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
- package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
- package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
- package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
- package/schema/fixtures/event/valid/audit-sampled.json +2 -1
- package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
- package/schema/fixtures/event/valid/daemon-identity.json +16 -0
- package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
- package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
- package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
- package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
- package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
- package/schema/policy.schema.json +102 -6
package/dist/src/cli/amend.js
CHANGED
|
@@ -60,6 +60,7 @@ import { accessSync, constants, existsSync, mkdtempSync, readFileSync, rmSync, s
|
|
|
60
60
|
import { tmpdir } from "node:os";
|
|
61
61
|
import { basename, dirname, isAbsolute, join, relative, resolve as resolvePathSegments, sep } from "node:path";
|
|
62
62
|
import { HUMAN_ACTOR_ENV, appendAttestation, attestedPolicyPayloadHash, checkAttestation, policyBytesHash, resolveHumanActor, } from "../core/attest.js";
|
|
63
|
+
import { settleTerminationGuards } from "../core/log-lock.js";
|
|
63
64
|
import { compareChains } from "../core/log-reconcile.js";
|
|
64
65
|
import { diffPolicies, renderDiff, SPEC_NAMESPACES } from "../core/policy-diff.js";
|
|
65
66
|
import { checkPolicyExpectations, describeFailure, describePinChange, diffPinSources, expectationsFor, DOGFOOD_SUITE_BUILT, DOGFOOD_SUITE_SOURCE, EXPECTATIONS_MODULE, } from "../core/policy-expectations.js";
|
|
@@ -117,8 +118,14 @@ const DEFAULT_ATTESTATION_WAIT_MS = 15 * 60 * 1000;
|
|
|
117
118
|
const DEFAULT_ATTESTATION_INTERVAL_MS = 2000;
|
|
118
119
|
/** An agent identity, the one `--as` form that routes to the channel path. */
|
|
119
120
|
const AGENT_ACTOR = /^agent:.+/u;
|
|
120
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Synchronous sleep with no dependency and no busy-spin (as `cli/execute.ts`).
|
|
123
|
+
* The poll follows this verb's own append, so the log lock's signal guard is
|
|
124
|
+
* settled first: Ctrl-C or a supervisor's SIGTERM during the wait ends the
|
|
125
|
+
* process at once (APRV-479, S2).
|
|
126
|
+
*/
|
|
121
127
|
function sleepSync(ms) {
|
|
128
|
+
settleTerminationGuards();
|
|
122
129
|
if (ms <= 0)
|
|
123
130
|
return;
|
|
124
131
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -1100,6 +1107,14 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1100
1107
|
: EXIT_IO;
|
|
1101
1108
|
return refuse(streams, json, read.code, read.message, exitCode);
|
|
1102
1109
|
}
|
|
1110
|
+
/**
|
|
1111
|
+
* The amendment branch's name, `--branch` or the seq-derived default.
|
|
1112
|
+
*
|
|
1113
|
+
* Declared HERE rather than beside the flow selection below (APRV-420): the
|
|
1114
|
+
* no-op path needs it too, because a re-run's whole job is to find the branch
|
|
1115
|
+
* a previous run left standing.
|
|
1116
|
+
*/
|
|
1117
|
+
const branchName = (seq) => branchFlag ?? `policy-amend-${seq}`;
|
|
1103
1118
|
const status = checkAttestation(read.records, policyPath);
|
|
1104
1119
|
const attested = status.status === "attested"
|
|
1105
1120
|
? { sha256: status.sha256, seq: status.seq }
|
|
@@ -1109,7 +1124,27 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1109
1124
|
// (a) Nothing to amend. A no-op ceremony is a SUCCESS, not an error: an
|
|
1110
1125
|
// operator (or a script) that runs `amend` on an already-attested policy has
|
|
1111
1126
|
// established exactly what they wanted to establish.
|
|
1127
|
+
//
|
|
1128
|
+
// APRV-420, one exception: an amendment branch already standing on origin.
|
|
1129
|
+
// The live policy does match its attestation, and saying only that leaves the
|
|
1130
|
+
// pull request carrying it open, unmergeable and unarmed, which is the state a
|
|
1131
|
+
// records advance landing first puts it in. So the re-run repairs it, or names
|
|
1132
|
+
// exactly what to run — never "nothing to amend" over a dirty amendment.
|
|
1112
1133
|
if (status.status === "attested") {
|
|
1134
|
+
const noopRoot = repoRoot(dirname(policyPath));
|
|
1135
|
+
const repair = wantCommit && noopRoot !== null && hasOrigin(noopRoot)
|
|
1136
|
+
? repairAmendBranch({
|
|
1137
|
+
root: noopRoot,
|
|
1138
|
+
policyPath,
|
|
1139
|
+
logPath,
|
|
1140
|
+
payloadFile,
|
|
1141
|
+
seq: status.seq,
|
|
1142
|
+
branch: branchName(String(status.seq)),
|
|
1143
|
+
// `--dry-run` and `--no-publish` mean the same thing here as
|
|
1144
|
+
// everywhere else in this verb: compute it, print it, run nothing.
|
|
1145
|
+
act: !dryRun && !noPublish,
|
|
1146
|
+
})
|
|
1147
|
+
: null;
|
|
1113
1148
|
if (json) {
|
|
1114
1149
|
emitReport(streams, {
|
|
1115
1150
|
policyPath,
|
|
@@ -1123,12 +1158,24 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1123
1158
|
noop: true,
|
|
1124
1159
|
dryRun,
|
|
1125
1160
|
aborted: false,
|
|
1161
|
+
repair,
|
|
1126
1162
|
});
|
|
1127
1163
|
}
|
|
1128
|
-
else {
|
|
1164
|
+
else if (repair === null) {
|
|
1129
1165
|
streams.out(`nothing to amend: ${relPath(policyPath, cwd)} already matches its attestation at seq ${status.seq} (sha256 ${shortHash(liveSha256)})\n`);
|
|
1130
1166
|
}
|
|
1131
|
-
|
|
1167
|
+
else {
|
|
1168
|
+
const headline = repair.state === "failed" ? st.glyph("fail") : st.glyph(repair.state === "owed" ? "point" : "ok");
|
|
1169
|
+
streams.out(`${headline} ${repair.branch}: ${repair.message}\n`);
|
|
1170
|
+
for (const step of repair.commands) {
|
|
1171
|
+
streams.err(` ${step.ok === null ? st.glyph("point") : step.ok ? st.glyph("ok") : st.glyph("fail")} ${step.command}\n`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
// A repair that BROKE keeps the I/O exit, which is the split the rest of
|
|
1175
|
+
// this verb uses: the exit code speaks to scripts, the text to people.
|
|
1176
|
+
// `owed` is not a failure — it is `--dry-run` or `--no-publish` answering
|
|
1177
|
+
// the question they were asked — and `landed` and `repaired` are successes.
|
|
1178
|
+
return repair !== null && repair.state === "failed" ? EXIT_IO : EXIT_OK;
|
|
1132
1179
|
}
|
|
1133
1180
|
// (b) The baseline, and only a verifiable one. See the module header.
|
|
1134
1181
|
progress.phase("recovering the attested baseline and diffing it against the live policy");
|
|
@@ -1169,7 +1216,6 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1169
1216
|
// that cannot reach GitHub answers `unknown`, which used to mean "commit in
|
|
1170
1217
|
// place"; an operator who typed `--pr` has said what they want either way.
|
|
1171
1218
|
const useBranch = branchFlag !== null || wantPr || (!forceDirect && onProtectedDefault);
|
|
1172
|
-
const branchName = (seq) => branchFlag ?? `policy-amend-${seq}`;
|
|
1173
1219
|
// The direct flow's push is about to hit a protected branch. Say so before
|
|
1174
1220
|
// the human types it, rather than after GitHub says it.
|
|
1175
1221
|
const pushWarning = !useBranch && onProtectedDefault
|
|
@@ -1680,6 +1726,14 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1680
1726
|
const why = commandOutputLines(merge.stderr, merge.stdout)[0];
|
|
1681
1727
|
note(`${prLabel(publishing.prUrl)} is open — merge it with a MERGE COMMIT when CI is green. auto-merge was not armed${why === undefined ? "" : `: ${why}`}`);
|
|
1682
1728
|
};
|
|
1729
|
+
/**
|
|
1730
|
+
* Who publishes the log for this amendment (APRV-420).
|
|
1731
|
+
*
|
|
1732
|
+
* Settled at commit-assembly time against the base the commit is parented
|
|
1733
|
+
* on, and reported, so the pull request's file list is never a surprise. The
|
|
1734
|
+
* default is the behaviour that predates the task: this commit carries it.
|
|
1735
|
+
*/
|
|
1736
|
+
let carriage = { carry: true, reason: null, publisher: null };
|
|
1683
1737
|
if (commitPlan !== null && commitBase !== null) {
|
|
1684
1738
|
// APRV-203. The commit is ASSEMBLED, never checked out: a scratch index is
|
|
1685
1739
|
// filled from the remote's tree, the two ceremony files are laid over it
|
|
@@ -1689,6 +1743,10 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1689
1743
|
const message = `Policy: ${summary} (attested seq ${seq})`;
|
|
1690
1744
|
/** `origin/main abc123`, or `HEAD abc123` where there is no remote. */
|
|
1691
1745
|
const baseLabel = `${commitBase.remote === null ? "HEAD" : `${commitBase.remote}/${commitBase.branch}`} ${commitBase.sha.slice(0, 12)}`;
|
|
1746
|
+
// APRV-420: who publishes the log. Decided here, from the base this commit
|
|
1747
|
+
// is about to be parented on, so the answer is about the tree the commit
|
|
1748
|
+
// will actually sit against.
|
|
1749
|
+
carriage = decideLogCarriage(commitPlan.root, commitBase, logPath);
|
|
1692
1750
|
progress.phase(`building the amendment commit on ${baseLabel} (nothing is checked out)`);
|
|
1693
1751
|
const built = commitOnBase(commitPlan.root, {
|
|
1694
1752
|
base: commitBase.sha,
|
|
@@ -1697,7 +1755,11 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1697
1755
|
// keeps this ceremony from reverting a pins edit somebody else landed.
|
|
1698
1756
|
paths: [
|
|
1699
1757
|
commitPlan.policyArg,
|
|
1700
|
-
|
|
1758
|
+
// APRV-420: the log joins the commit only when this amendment is the
|
|
1759
|
+
// thing publishing it. When a records advance is, two commits carrying
|
|
1760
|
+
// the same appended records is a conflict on `events.jsonl` whichever
|
|
1761
|
+
// lands first, and the repair is a hand merge of the log.
|
|
1762
|
+
...(carriage.carry ? [commitPlan.logArg] : []),
|
|
1701
1763
|
...(pinsChange === null ? [] : [pinsChange.arg]),
|
|
1702
1764
|
// APRV-356: the attested policy text, so the commit carrying the
|
|
1703
1765
|
// binding carries the bytes it binds. Without it the in-force policy is
|
|
@@ -1708,6 +1770,8 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1708
1770
|
message,
|
|
1709
1771
|
});
|
|
1710
1772
|
progress.done();
|
|
1773
|
+
if (carriage.reason !== null)
|
|
1774
|
+
note(`${st.glyph("ok")} ${carriage.reason}`);
|
|
1711
1775
|
if (!built.ok) {
|
|
1712
1776
|
return gitFailed(built.step, built.quote.join(" | "), `the attestation was appended at seq ${seq}, but ${built.message}; the checkout is untouched, so run the printed commands by hand`, commands);
|
|
1713
1777
|
}
|
|
@@ -2321,6 +2385,292 @@ function readLogText(path) {
|
|
|
2321
2385
|
return "";
|
|
2322
2386
|
}
|
|
2323
2387
|
}
|
|
2388
|
+
/**
|
|
2389
|
+
* The open records-advance branch on `origin`, or `null`.
|
|
2390
|
+
*
|
|
2391
|
+
* `git ls-remote` rather than `gh`, deliberately: this is a question about refs
|
|
2392
|
+
* and it must answer the same way on a box with no GitHub CLI, no token and no
|
|
2393
|
+
* network beyond the remote itself. `approval log advance` names its branch
|
|
2394
|
+
* `records-log-<YYYY-MM-DD>` and falls back to `records-log-<date>-<n>`
|
|
2395
|
+
* (`cli/log-advance.ts`), and merged branches are deleted by the reconcile
|
|
2396
|
+
* script, so a branch matching that pattern on origin is a live advance.
|
|
2397
|
+
*/
|
|
2398
|
+
function recordsPublisher(root) {
|
|
2399
|
+
const listed = git(["ls-remote", "--heads", "origin", "records-log-*"], root);
|
|
2400
|
+
if (!listed.ok)
|
|
2401
|
+
return null;
|
|
2402
|
+
for (const line of listed.stdout.split("\n")) {
|
|
2403
|
+
const [sha, ref] = line.trim().split(/\s+/u);
|
|
2404
|
+
if (ref === undefined || sha === undefined)
|
|
2405
|
+
continue;
|
|
2406
|
+
const name = ref.replace(/^refs\/heads\//u, "");
|
|
2407
|
+
if (name.startsWith("records-log-"))
|
|
2408
|
+
return { name, sha };
|
|
2409
|
+
}
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Whether the records branch at `sha` already carries every record of the
|
|
2414
|
+
* working log, this attestation included.
|
|
2415
|
+
*
|
|
2416
|
+
* A branch pushed BEFORE the attestation was appended does not, and that is
|
|
2417
|
+
* the ordinary order (the cadence advance ran, then the human signed), so
|
|
2418
|
+
* saying "this attestation included" of it would be a claim the committed
|
|
2419
|
+
* log contradicts. What publishes the attestation then is the NEXT advance,
|
|
2420
|
+
* and the pull request's protected-path guard holds the policy change until
|
|
2421
|
+
* a records branch or main carries the record. `null` when the branch's log
|
|
2422
|
+
* could not be fetched or read, which the caller reports as unknown rather
|
|
2423
|
+
* than as either answer.
|
|
2424
|
+
*/
|
|
2425
|
+
function publisherCarriesLog(root, sha, logArg, logPath) {
|
|
2426
|
+
const fetched = git(["fetch", "--quiet", "origin", sha], root);
|
|
2427
|
+
if (!fetched.ok)
|
|
2428
|
+
return null;
|
|
2429
|
+
const branchLog = showBlob(root, sha, logArg);
|
|
2430
|
+
if (branchLog === null)
|
|
2431
|
+
return null;
|
|
2432
|
+
const compared = compareChains({ label: "the working log", text: readLogText(logPath) }, { label: `records branch ${sha.slice(0, 12)}:${logArg}`, text: branchLog.toString("utf8") });
|
|
2433
|
+
if (!compared.ok)
|
|
2434
|
+
return null;
|
|
2435
|
+
return compared.drift.relation === "equal" || compared.drift.relation === "behind";
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Decide who publishes the log for this amendment. See the block above.
|
|
2439
|
+
*
|
|
2440
|
+
* Fails toward CARRYING it: every answer this function cannot establish (a log
|
|
2441
|
+
* that does not verify, a blob it could not read, a remote it could not list)
|
|
2442
|
+
* leaves the amendment holding the log, which is the behaviour that predates
|
|
2443
|
+
* this task. A dirty pull request is a nuisance; an attestation that reaches
|
|
2444
|
+
* `main` in nobody's commit is a policy in force that the committed log does
|
|
2445
|
+
* not record, and between those two there is no contest.
|
|
2446
|
+
*/
|
|
2447
|
+
function decideLogCarriage(root, base, logPath) {
|
|
2448
|
+
const logArg = repoPath(root, logPath);
|
|
2449
|
+
const baseLabel = base.remote === null ? base.branch : `${base.remote}/${base.branch}`;
|
|
2450
|
+
const baseLog = showBlob(root, base.sha, logArg);
|
|
2451
|
+
const compared = compareChains({ label: "the working log", text: readLogText(logPath) }, { label: `${baseLabel}:${logArg}`, text: baseLog === null ? "" : baseLog.toString("utf8") });
|
|
2452
|
+
if (compared.ok && (compared.drift.relation === "equal" || compared.drift.relation === "behind")) {
|
|
2453
|
+
return {
|
|
2454
|
+
carry: false,
|
|
2455
|
+
reason: `${baseLabel} already carries every record this attestation added, so this amendment does not touch the log`,
|
|
2456
|
+
publisher: null,
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
const publisher = recordsPublisher(root);
|
|
2460
|
+
if (publisher !== null) {
|
|
2461
|
+
const carries = publisherCarriesLog(root, publisher.sha, logArg, logPath);
|
|
2462
|
+
return {
|
|
2463
|
+
carry: false,
|
|
2464
|
+
reason: carries === true
|
|
2465
|
+
? `the records advance on ${publisher.name} publishes the whole log, this attestation included, so this amendment carries only the policy and cannot conflict with it`
|
|
2466
|
+
: `a records advance is live on ${publisher.name}${carries === null ? " (its log could not be read from here)" : ", pushed before this attestation was appended"}, so this amendment carries only the policy and cannot conflict with it; the attestation record lands with the next advance, and the pull request's protected-path check holds the policy change until a records branch or main carries it`,
|
|
2467
|
+
publisher: publisher.name,
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
return { carry: true, reason: null, publisher: null };
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Repair the amendment pull request a previous run left standing (APRV-420).
|
|
2474
|
+
*
|
|
2475
|
+
* The second run of a ceremony that already attested reaches "nothing to
|
|
2476
|
+
* amend", which is true and useless: the live policy does match its
|
|
2477
|
+
* attestation, and the pull request carrying that attestation to the trunk is
|
|
2478
|
+
* open, unmergeable and unarmed. On 2026-09-21 the repair was a hand merge of
|
|
2479
|
+
* `events.jsonl` in a throwaway worktree, which is the work every idiom here
|
|
2480
|
+
* exists to remove.
|
|
2481
|
+
*
|
|
2482
|
+
* So the re-run rebuilds. The trunk's log is a superset of the amendment's by
|
|
2483
|
+
* construction — every record the amendment appended reaches the trunk through
|
|
2484
|
+
* a records advance as well — so a commit built fresh on the current trunk,
|
|
2485
|
+
* carrying only what the trunk still LACKS, is the merge without a merge.
|
|
2486
|
+
*
|
|
2487
|
+
* Two things this does not do. It does not check anything out: the rebuild is
|
|
2488
|
+
* `commitOnBase`'s scratch index, exactly as the first run's was, so a live
|
|
2489
|
+
* appender never has the log moved underneath it. And it does not touch a
|
|
2490
|
+
* branch it cannot account for: the remote tip must be a single commit on top
|
|
2491
|
+
* of an ancestor of the base, which is the shape this ceremony creates and
|
|
2492
|
+
* nothing else. Anything else is reported with its commands rather than
|
|
2493
|
+
* force-pushed over.
|
|
2494
|
+
*
|
|
2495
|
+
* Returns `null` when there is nothing standing, which leaves the caller's
|
|
2496
|
+
* "nothing to amend" exactly as it was.
|
|
2497
|
+
*/
|
|
2498
|
+
function repairAmendBranch(input) {
|
|
2499
|
+
const { root, policyPath, logPath, payloadFile, seq, branch, act } = input;
|
|
2500
|
+
const listed = git(["ls-remote", "--heads", "origin", branch], root);
|
|
2501
|
+
const tipLine = listed.ok
|
|
2502
|
+
? listed.stdout.split("\n").find((line) => line.trim().length > 0)
|
|
2503
|
+
: undefined;
|
|
2504
|
+
if (tipLine === undefined)
|
|
2505
|
+
return null;
|
|
2506
|
+
const commands = [];
|
|
2507
|
+
const stop = (state, message, extra = []) => {
|
|
2508
|
+
for (const command of extra)
|
|
2509
|
+
commands.push({ command, ok: null });
|
|
2510
|
+
return {
|
|
2511
|
+
branch,
|
|
2512
|
+
state,
|
|
2513
|
+
base: null,
|
|
2514
|
+
carried: [],
|
|
2515
|
+
commit: null,
|
|
2516
|
+
pushed: false,
|
|
2517
|
+
autoMerge: "not-attempted",
|
|
2518
|
+
commands,
|
|
2519
|
+
message,
|
|
2520
|
+
};
|
|
2521
|
+
};
|
|
2522
|
+
const probe = probeProtection(root);
|
|
2523
|
+
const baseBranch = probe.defaultBranch ?? probe.currentBranch;
|
|
2524
|
+
if (baseBranch === null) {
|
|
2525
|
+
return stop("failed", `${branch} is on origin and no branch could be resolved to rebuild it on (${probe.reason})`);
|
|
2526
|
+
}
|
|
2527
|
+
commands.push({ command: `git fetch origin ${baseBranch}`, ok: null });
|
|
2528
|
+
const fetched = fetchBase(root, "origin", baseBranch);
|
|
2529
|
+
if (!fetched.ok) {
|
|
2530
|
+
commands[commands.length - 1] = { command: `git fetch origin ${baseBranch}`, ok: false };
|
|
2531
|
+
return stop("failed", `${branch} is on origin and ${fetched.message}`);
|
|
2532
|
+
}
|
|
2533
|
+
commands[commands.length - 1] = { command: `git fetch origin ${baseBranch}`, ok: true };
|
|
2534
|
+
const base = { remote: "origin", branch: baseBranch, sha: fetched.sha };
|
|
2535
|
+
const baseLabel = `origin/${baseBranch}`;
|
|
2536
|
+
// Fetched rather than read off `ls-remote`, because the count below needs the
|
|
2537
|
+
// tip to be an OBJECT in this repository: a commit somebody else pushed to
|
|
2538
|
+
// the branch has never been near this checkout, and `rev-list` over a sha it
|
|
2539
|
+
// does not have answers nothing at all.
|
|
2540
|
+
commands.push({ command: `git fetch origin ${branch}`, ok: null });
|
|
2541
|
+
const fetchedTip = fetchBase(root, "origin", branch);
|
|
2542
|
+
if (!fetchedTip.ok) {
|
|
2543
|
+
commands[commands.length - 1] = { command: `git fetch origin ${branch}`, ok: false };
|
|
2544
|
+
return stop("failed", `${branch} is on origin and ${fetchedTip.message}`);
|
|
2545
|
+
}
|
|
2546
|
+
commands[commands.length - 1] = { command: `git fetch origin ${branch}`, ok: true };
|
|
2547
|
+
const tipSha = fetchedTip.sha;
|
|
2548
|
+
// What the base still lacks. Each answer is read from the base's own tree,
|
|
2549
|
+
// so a record or a payload a records advance already landed is not carried
|
|
2550
|
+
// twice, which is the whole cause of the conflict this repairs.
|
|
2551
|
+
const policyArg = repoPath(root, policyPath);
|
|
2552
|
+
const payloadArg = repoPath(root, payloadFile);
|
|
2553
|
+
const carried = [];
|
|
2554
|
+
const reasons = [];
|
|
2555
|
+
const basePolicy = showBlob(root, base.sha, policyArg);
|
|
2556
|
+
const livePolicy = ((() => {
|
|
2557
|
+
try {
|
|
2558
|
+
return readFileSync(policyPath);
|
|
2559
|
+
}
|
|
2560
|
+
catch {
|
|
2561
|
+
return null;
|
|
2562
|
+
}
|
|
2563
|
+
})());
|
|
2564
|
+
if (livePolicy === null) {
|
|
2565
|
+
return stop("failed", `${policyPath} could not be read, so the rebuild has no policy bytes to carry`);
|
|
2566
|
+
}
|
|
2567
|
+
if (basePolicy === null || !basePolicy.equals(livePolicy)) {
|
|
2568
|
+
carried.push(policyArg);
|
|
2569
|
+
reasons.push(`the policy bytes (${baseLabel} does not carry them)`);
|
|
2570
|
+
}
|
|
2571
|
+
if (showBlob(root, base.sha, payloadArg) === null) {
|
|
2572
|
+
carried.push(payloadArg);
|
|
2573
|
+
reasons.push("the attested policy text");
|
|
2574
|
+
}
|
|
2575
|
+
const carriage = decideLogCarriage(root, base, logPath);
|
|
2576
|
+
if (carriage.carry) {
|
|
2577
|
+
carried.push(repoPath(root, logPath));
|
|
2578
|
+
reasons.push("the log, which nothing else is publishing");
|
|
2579
|
+
}
|
|
2580
|
+
else {
|
|
2581
|
+
reasons.push(`NOT the log: ${carriage.reason ?? "it is published elsewhere"}`);
|
|
2582
|
+
}
|
|
2583
|
+
const pins = pinsChangeIn(root, policyPath, base.sha);
|
|
2584
|
+
if (pins !== null) {
|
|
2585
|
+
carried.push(pins.arg);
|
|
2586
|
+
reasons.push(pins.arg);
|
|
2587
|
+
}
|
|
2588
|
+
if (carried.length === 0) {
|
|
2589
|
+
return stop("landed", `the amendment at seq ${seq} is already on ${baseLabel} in full: the policy, its attested text and its records are all there, so ${branch} has nothing left to carry. Close the pull request`, [`gh pr close ${branch} --delete-branch`]);
|
|
2590
|
+
}
|
|
2591
|
+
// Only now that a rebuild is actually owed does the branch's SHAPE matter,
|
|
2592
|
+
// and it has to be the one shape this ceremony makes: one commit, on a parent
|
|
2593
|
+
// the base already contains. `rev-list --count base..tip` counts the commits
|
|
2594
|
+
// the tip has that the base does not, so anything above one is work this verb
|
|
2595
|
+
// did not put there and a force-push would lose it. Zero is not checked here,
|
|
2596
|
+
// because a branch the base already contains has nothing left to carry and
|
|
2597
|
+
// left through the `landed` exit above.
|
|
2598
|
+
const countRun = git(["rev-list", "--count", `${base.sha}..${tipSha}`], root);
|
|
2599
|
+
const count = countRun.ok ? Number(countRun.stdout.trim()) : Number.NaN;
|
|
2600
|
+
if (!Number.isInteger(count) || count > 1) {
|
|
2601
|
+
return stop("failed", `${branch} on origin carries ${Number.isInteger(count) ? String(count) : "an unknown number of"} commits that ${baseLabel} does not. The amendment ceremony builds exactly one, so this branch holds work this verb did not put there and it will not be rebuilt over. Rebase or merge it by hand`, [`git log --oneline ${base.sha.slice(0, 12)}..${tipSha.slice(0, 12)}`]);
|
|
2602
|
+
}
|
|
2603
|
+
const subject = git(["log", "-1", "--format=%s", tipSha], root);
|
|
2604
|
+
const message = subject.ok && subject.stdout.trim().length > 0
|
|
2605
|
+
? subject.stdout.trim()
|
|
2606
|
+
: `Policy: amend ${policyArg} (attested seq ${seq})`;
|
|
2607
|
+
const rebuild = `git push origin +<rebuilt>:refs/heads/${branch}`;
|
|
2608
|
+
const built = commitOnBase(root, { base: base.sha, paths: carried, message });
|
|
2609
|
+
if (!built.ok) {
|
|
2610
|
+
commands.push({ command: rebuild, ok: null });
|
|
2611
|
+
return stop("failed", `${branch} could not be rebuilt on ${baseLabel}: ${built.message}`);
|
|
2612
|
+
}
|
|
2613
|
+
if (built.unchanged) {
|
|
2614
|
+
return stop("landed", `the amendment at seq ${seq} is already on ${baseLabel}: a commit carrying ${carried.join(", ")} over ${baseLabel} is the same tree. Close the pull request`, [`gh pr close ${branch} --delete-branch`]);
|
|
2615
|
+
}
|
|
2616
|
+
const commitSha = built.sha;
|
|
2617
|
+
const carriedNote = `rebuilt ${commitSha.slice(0, 12)} on ${baseLabel} ${base.sha.slice(0, 12)} carrying ${reasons.join(", ")}`;
|
|
2618
|
+
const push = `git push origin +${commitSha.slice(0, 12)}:refs/heads/${branch}`;
|
|
2619
|
+
if (!act) {
|
|
2620
|
+
commands.push({ command: push, ok: null });
|
|
2621
|
+
commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: null });
|
|
2622
|
+
return {
|
|
2623
|
+
branch,
|
|
2624
|
+
state: "owed",
|
|
2625
|
+
base: base.sha,
|
|
2626
|
+
carried,
|
|
2627
|
+
commit: commitSha,
|
|
2628
|
+
pushed: false,
|
|
2629
|
+
autoMerge: "not-attempted",
|
|
2630
|
+
commands,
|
|
2631
|
+
message: `${carriedNote}; nothing was pushed, so the pull request is unchanged. Run the commands above to finish it`,
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
// Force, and only here: the branch carries exactly the one commit this
|
|
2635
|
+
// ceremony put on it (checked above), and the whole repair is replacing that
|
|
2636
|
+
// commit with the same amendment parented on a base that has moved.
|
|
2637
|
+
const pushed = git(["push", "origin", `+${commitSha}:refs/heads/${branch}`], root);
|
|
2638
|
+
commands.push({ command: push, ok: pushed.ok });
|
|
2639
|
+
if (!pushed.ok) {
|
|
2640
|
+
commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: null });
|
|
2641
|
+
return {
|
|
2642
|
+
branch,
|
|
2643
|
+
state: "failed",
|
|
2644
|
+
base: base.sha,
|
|
2645
|
+
carried,
|
|
2646
|
+
commit: commitSha,
|
|
2647
|
+
pushed: false,
|
|
2648
|
+
autoMerge: "not-attempted",
|
|
2649
|
+
commands,
|
|
2650
|
+
message: `${carriedNote}, and \`${push}\` was REJECTED: ${pushFailureText(pushed)}. The commit exists locally; push it and re-arm the merge`,
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
let autoMerge = "not-attempted";
|
|
2654
|
+
if (ghAvailable(root)) {
|
|
2655
|
+
const armed = gh(["pr", "merge", branch, "--merge", "--auto"], root);
|
|
2656
|
+
commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: armed.ok });
|
|
2657
|
+
autoMerge = armed.ok ? "armed" : "refused";
|
|
2658
|
+
}
|
|
2659
|
+
else {
|
|
2660
|
+
commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: null });
|
|
2661
|
+
}
|
|
2662
|
+
return {
|
|
2663
|
+
branch,
|
|
2664
|
+
state: "repaired",
|
|
2665
|
+
base: base.sha,
|
|
2666
|
+
carried,
|
|
2667
|
+
commit: commitSha,
|
|
2668
|
+
pushed: true,
|
|
2669
|
+
autoMerge,
|
|
2670
|
+
commands,
|
|
2671
|
+
message: `${carriedNote}, and force-updated ${branch} on origin, so the pull request is mergeable again${autoMerge === "armed" ? " and armed" : ""}`,
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2324
2674
|
function loadSummary(load) {
|
|
2325
2675
|
return load.ok
|
|
2326
2676
|
? { ok: true, code: null, message: null }
|
|
@@ -2350,6 +2700,9 @@ function emitReport(streams, report) {
|
|
|
2350
2700
|
// APRV-109, additive and always present: `null` says the attestation was
|
|
2351
2701
|
// performed at this terminal, an object says it was collected as a tap.
|
|
2352
2702
|
proposal: report.proposal ?? null,
|
|
2703
|
+
// APRV-420, additive and always present: `null` says this run had an
|
|
2704
|
+
// amendment to make, or found no standing branch to repair.
|
|
2705
|
+
repair: report.repair ?? null,
|
|
2353
2706
|
})}\n`);
|
|
2354
2707
|
}
|
|
2355
2708
|
//# sourceMappingURL=amend.js.map
|