approval-md 0.0.1 → 0.2.0
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/LICENSE +176 -0
- package/NOTICE +5 -0
- package/README.md +940 -4
- package/SPEC.md +476 -34
- package/cli.js +29 -3
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +1200 -0
- package/dist/src/adapters/agentmail.js.map +1 -0
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/conformance.js +461 -0
- package/dist/src/adapters/conformance.js.map +1 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +1035 -0
- package/dist/src/adapters/contract.js.map +1 -0
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/email.js +749 -0
- package/dist/src/adapters/email.js.map +1 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/env-passphrase.js +132 -0
- package/dist/src/adapters/env-passphrase.js.map +1 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +77 -0
- package/dist/src/adapters/registry.js.map +1 -0
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/smtp.js +499 -0
- package/dist/src/adapters/smtp.js.map +1 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +161 -0
- package/dist/src/adapters/vault-provider.js.map +1 -0
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/batch.js +121 -0
- package/dist/src/channels/batch.js.map +1 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/cli.js +468 -0
- package/dist/src/channels/cli.js.map +1 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/conformance.js +445 -0
- package/dist/src/channels/conformance.js.map +1 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/contract.js +494 -0
- package/dist/src/channels/contract.js.map +1 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/payload-view.js +43 -0
- package/dist/src/channels/payload-view.js.map +1 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/render-queue.js +564 -0
- package/dist/src/channels/render-queue.js.map +1 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/tagging.js +723 -0
- package/dist/src/channels/tagging.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/telegram.js +3190 -0
- package/dist/src/channels/telegram.js.map +1 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/channels/web.js +903 -0
- package/dist/src/channels/web.js.map +1 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +288 -0
- package/dist/src/cli/adapter.js.map +1 -0
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/amend.js +2171 -0
- package/dist/src/cli/amend.js.map +1 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/args.js +86 -0
- package/dist/src/cli/args.js.map +1 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/attest.js +307 -0
- package/dist/src/cli/attest.js.map +1 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit-card.js +201 -0
- package/dist/src/cli/audit-card.js.map +1 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/audit.js +460 -0
- package/dist/src/cli/audit.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-telegram.js +2063 -0
- package/dist/src/cli/channel-telegram.js.map +1 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel-web.js +357 -0
- package/dist/src/cli/channel-web.js.map +1 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/channel.js +438 -0
- package/dist/src/cli/channel.js.map +1 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/checkpoint-tap.js +238 -0
- package/dist/src/cli/checkpoint-tap.js.map +1 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/coverage.js +343 -0
- package/dist/src/cli/coverage.js.map +1 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/daemon.js +631 -0
- package/dist/src/cli/daemon.js.map +1 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +2762 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/env.js +302 -0
- package/dist/src/cli/env.js.map +1 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/execute.js +1682 -0
- package/dist/src/cli/execute.js.map +1 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/exit-codes.js +82 -0
- package/dist/src/cli/exit-codes.js.map +1 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/feedback.js +205 -0
- package/dist/src/cli/feedback.js.map +1 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate-window.js +294 -0
- package/dist/src/cli/gate-window.js.map +1 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/gate.js +557 -0
- package/dist/src/cli/gate.js.map +1 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/git-scope.js +295 -0
- package/dist/src/cli/git-scope.js.map +1 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-attach.js +107 -0
- package/dist/src/cli/gloss-attach.js.map +1 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex-child.js +149 -0
- package/dist/src/cli/gloss-codex-child.js.map +1 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-codex.js +255 -0
- package/dist/src/cli/gloss-codex.js.map +1 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss-options.js +79 -0
- package/dist/src/cli/gloss-options.js.map +1 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/gloss.js +362 -0
- package/dist/src/cli/gloss.js.map +1 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +2339 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +2849 -0
- package/dist/src/cli/hook.js.map +1 -0
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/import.js +175 -0
- package/dist/src/cli/import.js.map +1 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +336 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/instructions.js +262 -0
- package/dist/src/cli/instructions.js.map +1 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/journal.js +238 -0
- package/dist/src/cli/journal.js.map +1 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +840 -0
- package/dist/src/cli/log-advance.js.map +1 -0
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-anchor.js +387 -0
- package/dist/src/cli/log-anchor.js.map +1 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-checkpoint.js +128 -0
- package/dist/src/cli/log-checkpoint.js.map +1 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-sync.js +849 -0
- package/dist/src/cli/log-sync.js.map +1 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +360 -0
- package/dist/src/cli/log-verbs.js.map +1 -0
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/long-help.js +148 -0
- package/dist/src/cli/long-help.js.map +1 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +1206 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/mcp.js +306 -0
- package/dist/src/cli/mcp.js.map +1 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/paths.js +79 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/payload.js +253 -0
- package/dist/src/cli/payload.js.map +1 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/policy.js +229 -0
- package/dist/src/cli/policy.js.map +1 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +1175 -0
- package/dist/src/cli/preflight.js.map +1 -0
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/progress.js +112 -0
- package/dist/src/cli/progress.js.map +1 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/prompt.js +312 -0
- package/dist/src/cli/prompt.js.map +1 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/records.js +66 -0
- package/dist/src/cli/records.js.map +1 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/render.js +132 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/sandbox.js +150 -0
- package/dist/src/cli/sandbox.js.map +1 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/scaffold.js +137 -0
- package/dist/src/cli/scaffold.js.map +1 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +509 -0
- package/dist/src/cli/setup-adapter.js.map +1 -0
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-channel.js +635 -0
- package/dist/src/cli/setup-channel.js.map +1 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-checkpoint.js +196 -0
- package/dist/src/cli/setup-checkpoint.js.map +1 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-common.js +376 -0
- package/dist/src/cli/setup-common.js.map +1 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-flow.js +476 -0
- package/dist/src/cli/setup-flow.js.map +1 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup-service.js +308 -0
- package/dist/src/cli/setup-service.js.map +1 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/setup.js +473 -0
- package/dist/src/cli/setup.js.map +1 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/style.js +469 -0
- package/dist/src/cli/style.js.map +1 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/token.js +274 -0
- package/dist/src/cli/token.js.map +1 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +849 -0
- package/dist/src/cli/up.js.map +1 -0
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/usage.js +91 -0
- package/dist/src/cli/usage.js.map +1 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/values.js +189 -0
- package/dist/src/cli/values.js.map +1 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +362 -0
- package/dist/src/cli/vault.js.map +1 -0
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +2341 -0
- package/dist/src/cli/verb-registry.js.map +1 -0
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +52 -0
- package/dist/src/cli/wordmark.js.map +1 -0
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -0
- package/dist/src/core/advance-cycle.js +200 -0
- package/dist/src/core/advance-cycle.js.map +1 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/agents-md.js +747 -0
- package/dist/src/core/agents-md.js.map +1 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +589 -0
- package/dist/src/core/attest.js.map +1 -0
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/audit.js +882 -0
- package/dist/src/core/audit.js.map +1 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/budgets.js +449 -0
- package/dist/src/core/budgets.js.map +1 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/checkpoint.js +738 -0
- package/dist/src/core/checkpoint.js.map +1 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/child-env.js +86 -0
- package/dist/src/core/child-env.js.map +1 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/clock.js +43 -0
- package/dist/src/core/clock.js.map +1 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +2356 -0
- package/dist/src/core/command-class.js.map +1 -0
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/adapter.js +71 -0
- package/dist/src/core/coverage-sources/adapter.js.map +1 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/gh.js +136 -0
- package/dist/src/core/coverage-sources/gh.js.map +1 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage-sources/git.js +269 -0
- package/dist/src/core/coverage-sources/git.js.map +1 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/coverage.js +337 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/credential-spec.js +23 -0
- package/dist/src/core/credential-spec.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/dark-session.js +714 -0
- package/dist/src/core/dark-session.js.map +1 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/decision-refusal.js +265 -0
- package/dist/src/core/decision-refusal.js.map +1 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/env-file.js +837 -0
- package/dist/src/core/env-file.js.map +1 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +1271 -0
- package/dist/src/core/execute.js.map +1 -0
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/frontmatter.js +100 -0
- package/dist/src/core/frontmatter.js.map +1 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate-window.js +506 -0
- package/dist/src/core/gate-window.js.map +1 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +3002 -0
- package/dist/src/core/gate.js.map +1 -0
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/git-run.js +93 -0
- package/dist/src/core/git-run.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +211 -0
- package/dist/src/core/harness-version.js.map +1 -0
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/harness-wait.js +58 -0
- package/dist/src/core/harness-wait.js.map +1 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/head-retry.js +121 -0
- package/dist/src/core/head-retry.js.map +1 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/instance.js +319 -0
- package/dist/src/core/instance.js.map +1 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/intake-limits.js +350 -0
- package/dist/src/core/intake-limits.js.map +1 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/jcs.js +132 -0
- package/dist/src/core/jcs.js.map +1 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/journal.js +200 -0
- package/dist/src/core/journal.js.map +1 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/live-draw.js +703 -0
- package/dist/src/core/live-draw.js.map +1 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-reconcile.js +136 -0
- package/dist/src/core/log-reconcile.js.map +1 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/log.js +546 -0
- package/dist/src/core/log.js.map +1 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +487 -0
- package/dist/src/core/loop.js.map +1 -0
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/md-fence.js +74 -0
- package/dist/src/core/md-fence.js.map +1 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/money.js +195 -0
- package/dist/src/core/money.js.map +1 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-census.js +146 -0
- package/dist/src/core/payload-census.js.map +1 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload-store.js +340 -0
- package/dist/src/core/payload-store.js.map +1 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/payload.js +80 -0
- package/dist/src/core/payload.js.map +1 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +588 -0
- package/dist/src/core/policy-diff.js.map +1 -0
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-expectations.js +394 -0
- package/dist/src/core/policy-expectations.js.map +1 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +258 -0
- package/dist/src/core/policy-explain.js.map +1 -0
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +536 -0
- package/dist/src/core/policy-load.js.map +1 -0
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +478 -0
- package/dist/src/core/policy-match.js.map +1 -0
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/policy-proposal.js +458 -0
- package/dist/src/core/policy-proposal.js.map +1 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/prompt-layout.js +422 -0
- package/dist/src/core/prompt-layout.js.map +1 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +1566 -0
- package/dist/src/core/protected-path-guard.js.map +1 -0
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/registration.js +39 -0
- package/dist/src/core/registration.js.map +1 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/reindex.js +336 -0
- package/dist/src/core/reindex.js.map +1 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sampler.js +388 -0
- package/dist/src/core/sampler.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/sandbox.js +424 -0
- package/dist/src/core/sandbox.js.map +1 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/seal.js +290 -0
- package/dist/src/core/seal.js.map +1 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/state.js +1009 -0
- package/dist/src/core/state.js.map +1 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/task-file.js +464 -0
- package/dist/src/core/task-file.js.map +1 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/telegram-config.js +114 -0
- package/dist/src/core/telegram-config.js.map +1 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +561 -0
- package/dist/src/core/token.js.map +1 -0
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/values.js +153 -0
- package/dist/src/core/values.js.map +1 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/vault.js +612 -0
- package/dist/src/core/vault.js.map +1 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verified-snapshot.js +506 -0
- package/dist/src/core/verified-snapshot.js.map +1 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/verify.js +549 -0
- package/dist/src/core/verify.js.map +1 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/version.js +9 -0
- package/dist/src/core/version.js.map +1 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/core/wysiwys.js +728 -0
- package/dist/src/core/wysiwys.js.map +1 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance-child.js +78 -0
- package/dist/src/daemon/advance-child.js.map +1 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/advance.js +849 -0
- package/dist/src/daemon/advance.js.map +1 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/audit.js +90 -0
- package/dist/src/daemon/audit.js.map +1 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/daemon.js +1988 -0
- package/dist/src/daemon/daemon.js.map +1 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/dark-session.js +119 -0
- package/dist/src/daemon/dark-session.js.map +1 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw-child.js +132 -0
- package/dist/src/daemon/draw-child.js.map +1 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/draw.js +458 -0
- package/dist/src/daemon/draw.js.map +1 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +345 -0
- package/dist/src/daemon/git-evidence.js.map +1 -0
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/projection.js +233 -0
- package/dist/src/daemon/projection.js.map +1 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/daemon/prune.js +376 -0
- package/dist/src/daemon/prune.js.map +1 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/http.js +343 -0
- package/dist/src/mcp/http.js.map +1 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +602 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +5716 -0
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +53 -4
- package/schema/.gitkeep +0 -0
- package/schema/LICENSE +117 -0
- package/schema/codex-instance.schema.json +82 -0
- package/schema/envelope.schema.json +137 -0
- package/schema/event.schema.json +1811 -0
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
- package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
- package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
- package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
- package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
- package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
- package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
- package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
- package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
- package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
- package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
- package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
- package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
- package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
- package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
- package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
- package/schema/fixtures/envelope/valid/canonical.json +25 -0
- package/schema/fixtures/envelope/valid/minimal.json +7 -0
- package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
- package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
- package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
- package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
- package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
- package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
- package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
- package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
- package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
- package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
- package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
- package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
- package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
- package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
- package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
- package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
- package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
- package/schema/fixtures/event/invalid/missing-alg.json +14 -0
- package/schema/fixtures/event/invalid/missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
- package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
- package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
- package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
- package/schema/fixtures/event/invalid/short-hash.json +15 -0
- package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
- package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
- package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
- package/schema/fixtures/event/valid/approval-expired.json +15 -0
- package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
- package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
- package/schema/fixtures/event/valid/approval-granted.json +15 -0
- package/schema/fixtures/event/valid/approval-rejected.json +15 -0
- package/schema/fixtures/event/valid/approval-requested.json +19 -0
- package/schema/fixtures/event/valid/approval-revoked.json +15 -0
- package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
- package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
- package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
- package/schema/fixtures/event/valid/audit-sampled.json +14 -0
- package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
- package/schema/fixtures/event/valid/envelope-drift.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
- package/schema/fixtures/event/valid/execution-completed.json +15 -0
- package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-failed.json +15 -0
- package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
- package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
- package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
- package/schema/fixtures/event/valid/execution-started.json +14 -0
- package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
- package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
- package/schema/fixtures/event/valid/gate-closed.json +14 -0
- package/schema/fixtures/event/valid/gate-opened.json +16 -0
- package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
- package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
- package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
- package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
- package/schema/fixtures/event/valid/payload-pruned.json +17 -0
- package/schema/fixtures/event/valid/policy-declined.json +16 -0
- package/schema/fixtures/event/valid/policy-proposed.json +35 -0
- package/schema/fixtures/event/valid/policy-updated.json +14 -0
- package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
- package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
- package/schema/fixtures/event/valid/route-accepted.json +15 -0
- package/schema/fixtures/event/valid/route-proposed.json +16 -0
- package/schema/fixtures/event/valid/spec-example.json +15 -0
- package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
- package/schema/fixtures/event/valid/task-registered.json +14 -0
- package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
- package/schema/fixtures/hash/known-answer.json +74 -0
- package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
- package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
- package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
- package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
- package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/missing-version.json +8 -0
- package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
- package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
- package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
- package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
- package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
- package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
- package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
- package/schema/fixtures/policy/valid/canonical.json +47 -0
- package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
- package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
- package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
- package/schema/fixtures/policy/valid/global-budgets.json +19 -0
- package/schema/fixtures/policy/valid/human-only.json +9 -0
- package/schema/fixtures/policy/valid/minimal.json +6 -0
- package/schema/fixtures/policy/valid/protected-paths.json +10 -0
- package/schema/fixtures/policy/valid/record-namespace.json +13 -0
- package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
- package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
- package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
- package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
- package/schema/fixtures/policy/valid/wildcards.json +15 -0
- package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
- package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
- package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
- package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
- package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
- package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
- package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/valid/canonical.md +50 -0
- package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
- package/schema/fixtures/policy-md/valid/minimal.md +3 -0
- package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
- package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
- package/schema/fixtures/policy-md/valid/with-values.md +79 -0
- package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
- package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
- package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
- package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
- package/schema/fixtures/sample-record/valid/minimal.json +4 -0
- package/schema/fixtures/sample-record/valid/with-note.json +5 -0
- package/schema/fixtures/values/invalid/class-shaped.json +9 -0
- package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
- package/schema/fixtures/values/invalid/non-string-item.json +4 -0
- package/schema/fixtures/values/invalid/over-cap.json +26 -0
- package/schema/fixtures/values/invalid/unknown-key.json +5 -0
- package/schema/fixtures/values/invalid/version-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +7 -0
- package/schema/fixtures/values/valid/full.json +20 -0
- package/schema/fixtures/values/valid/minimal.json +1 -0
- package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
- package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
- package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
- package/schema/fixtures/values-md/valid/absent.md +50 -0
- package/schema/fixtures/values-md/valid/with-values.md +79 -0
- package/schema/policy.schema.json +501 -0
- package/schema/sample-record.schema.json +26 -0
- package/schema/values.schema.json +55 -0
- package/templates/codex/README.md +9 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `approval log sync` (APRV-125) — the pull half of the log ritual, as a verb.
|
|
3
|
+
*
|
|
4
|
+
* ## The ritual this replaces
|
|
5
|
+
*
|
|
6
|
+
* Bringing the committed log up to date used to be a hand-run sequence: stash
|
|
7
|
+
* the working `events.jsonl`, pull, pop the stash, hope. It was our own
|
|
8
|
+
* sanctioned runbook and it was dangerous three ways.
|
|
9
|
+
*
|
|
10
|
+
* 1. It rewound the working log through git while a daemon held it open for
|
|
11
|
+
* append. That is fork 2 of 2026-08-20 (APRV-104's notes): a rewound file
|
|
12
|
+
* under a live appender, and two chains where there was one.
|
|
13
|
+
* 2. It reached the approver's phone as `policy.edit` over a protected path —
|
|
14
|
+
* a true label that told the human nothing about what was happening.
|
|
15
|
+
* 3. `git stash pop` can conflict, and on the day it did, it left conflict
|
|
16
|
+
* markers inside the log file mid-ceremony.
|
|
17
|
+
*
|
|
18
|
+
* The precedent for turning it into a verb is `policy amend` after the seq 2
|
|
19
|
+
* incident: when a hand-ritual proves dangerous, it becomes deterministic code
|
|
20
|
+
* the gate can read.
|
|
21
|
+
*
|
|
22
|
+
* ## The sequence, and why each step is where it is
|
|
23
|
+
*
|
|
24
|
+
* Everything below runs inside ONE hold of the append lockfile. The lock is
|
|
25
|
+
* usually taken per-append; here it spans the whole operation, because an
|
|
26
|
+
* append landing between the snapshot and the restore is precisely the
|
|
27
|
+
* interleaving that forks a chain.
|
|
28
|
+
*
|
|
29
|
+
* 1. **Primary checkout only.** The committed log has one home. A worktree has
|
|
30
|
+
* no business synchronizing it, and `log-sync-not-primary` says so.
|
|
31
|
+
* 2. **Verify before touching anything.** The working chain is walked and its
|
|
32
|
+
* head recorded. Nothing is decided from a log that does not verify.
|
|
33
|
+
* 3. **Snapshot, not stash.** The working log is copied aside, atomically, to
|
|
34
|
+
* a file inside `.approval/`. `git stash` is never used, and the log never
|
|
35
|
+
* routes through git state mutation.
|
|
36
|
+
* 4. **Baseline.** The working LOG is set to the bytes git has at `HEAD`, so
|
|
37
|
+
* the path is clean and a fast-forward merge can move over it. This is a
|
|
38
|
+
* plain write of bytes we already hold, not a checkout: the snapshot from
|
|
39
|
+
* step 3 is the only copy that matters and it is already safe. The queue
|
|
40
|
+
* projection is deliberately NOT baselined here; see the section below.
|
|
41
|
+
* 5. **Fetch, then a fast-forward CHECK, then the projections are discarded,
|
|
42
|
+
* then the merge.** A non-fast-forward is named and refused rather than
|
|
43
|
+
* merged; a merge commit over a log is a merge of two hash chains, which is
|
|
44
|
+
* not a thing that exists.
|
|
45
|
+
* 6. **Payload reconciliation, between the check and the merge.** See the
|
|
46
|
+
* section below: untracked payload files the incoming tree also carries are
|
|
47
|
+
* proved byte-identical and then removed, so the fast-forward has a clean
|
|
48
|
+
* path to write them onto.
|
|
49
|
+
* 7. **Reconcile.** The committed chain must be a prefix of the snapshot, or
|
|
50
|
+
* equal to it, or an extension of it. Anything else is a fork:
|
|
51
|
+
* `log-diverged`, both heads, the first divergent seq, snapshot restored,
|
|
52
|
+
* nothing else touched. The comparison is `core/log-reconcile.ts`, shared
|
|
53
|
+
* with doctor's `log-drift` check.
|
|
54
|
+
* 8. **Projections are REBUILT, never copied back.** QUEUE.md is re-rendered
|
|
55
|
+
* from the reconciled log and the index is reindexed from it. The direction
|
|
56
|
+
* is load-bearing: a projection restored from before the pull would be a
|
|
57
|
+
* screenshot asserting something the log no longer says.
|
|
58
|
+
* 9. **Post-verify**, and only then is the snapshot removed.
|
|
59
|
+
*
|
|
60
|
+
* Any failure at any step restores the snapshot before returning. The working
|
|
61
|
+
* log is never left in a half state, and the snapshot outlives every failure
|
|
62
|
+
* path that could need it.
|
|
63
|
+
*
|
|
64
|
+
* ## Untracked payload files, and why the merge used to stop on them (APRV-225)
|
|
65
|
+
*
|
|
66
|
+
* On 2026-09-02, after a records advance to seq 11361 merged, `log sync` in the
|
|
67
|
+
* primary checkout refused `log-sync-git-failed`: `git merge --ff-only` will not
|
|
68
|
+
* write over an untracked working-tree file, and the advance had committed 33
|
|
69
|
+
* payload files that this checkout already held untracked. Every one of them was
|
|
70
|
+
* byte-identical to the incoming blob — they are content-addressed, so identical
|
|
71
|
+
* by construction — and the only way forward was a hand step: move them aside,
|
|
72
|
+
* sync, put them back.
|
|
73
|
+
*
|
|
74
|
+
* That hand step is now step 6, on the same reasoning as the whole verb: when a
|
|
75
|
+
* hand-ritual is provably safe, it becomes deterministic code, and when it is
|
|
76
|
+
* NOT provably safe it refuses rather than guessing. The proof is per file, and
|
|
77
|
+
* every part of it runs before a single byte is removed:
|
|
78
|
+
*
|
|
79
|
+
* - the local file is **self-addressed** — SHA-256 of its bytes is its own name
|
|
80
|
+
* (the store writes RFC 8785 canonical bytes, so this holds for anything
|
|
81
|
+
* `storePayload` wrote); the `{"$ref": …}` reference form is the store's
|
|
82
|
+
* documented exception and is carried on byte equality alone.
|
|
83
|
+
* - the local bytes **equal the incoming blob**, compared as bytes read through
|
|
84
|
+
* `git show`. Not git's blob id: that is SHA-1 over a header plus the content
|
|
85
|
+
* and has nothing to do with the SHA-256 content address the store uses, so
|
|
86
|
+
* comparing names to blob ids would be comparing two different hashes of two
|
|
87
|
+
* different things and calling the disagreement a fork.
|
|
88
|
+
*
|
|
89
|
+
* A payload that fails either test refuses `log-sync-payload-mismatch`, naming
|
|
90
|
+
* the file. A payload is the material evidence an approval bound to: two
|
|
91
|
+
* different versions of one is not a merge conflict to be resolved, it is a
|
|
92
|
+
* question about which bytes a human said yes to. Nothing is merged, nothing is
|
|
93
|
+
* appended, and the working tree is left as it was found.
|
|
94
|
+
*
|
|
95
|
+
* A local payload the incoming tree does **not** carry is not this verb's
|
|
96
|
+
* business and is not touched: it blocks nothing, and it is very likely a
|
|
97
|
+
* payload this checkout has recorded and not yet advanced.
|
|
98
|
+
*
|
|
99
|
+
* ## The queue projection is disposable, and discarded LATE (APRV-292)
|
|
100
|
+
*
|
|
101
|
+
* On 2026-09-07, after a records pull request that carried `.approval/QUEUE.md`
|
|
102
|
+
* as well as the log, `log sync` refused twice with git's "local changes to
|
|
103
|
+
* .approval/QUEUE.md would be overwritten", the second time immediately after a
|
|
104
|
+
* hand-run `git checkout -- .approval/QUEUE.md`. The hand step was not wrong,
|
|
105
|
+
* it was just early: the running daemon re-rendered the projection again before
|
|
106
|
+
* the fast-forward reached it.
|
|
107
|
+
*
|
|
108
|
+
* The two files are exposed to different writers, which is why they are handled
|
|
109
|
+
* differently. Nothing can append to the log while this verb runs — sync holds
|
|
110
|
+
* the append lock for the whole ceremony, and every appender takes it. Nothing
|
|
111
|
+
* at all holds the projection: `channels/render-queue.ts`'s `writeQueue` renders
|
|
112
|
+
* and renames `QUEUE.md` on every daemon tick, under no lock, by design (the
|
|
113
|
+
* TTL countdowns in it move even when the log does not, so the file is rewritten
|
|
114
|
+
* whether or not anything was appended). A
|
|
115
|
+
* projection baselined at step 4 can therefore be dirty again by the time `git
|
|
116
|
+
* merge --ff-only` looks at it, and a stale render is enough to stop a pull of
|
|
117
|
+
* the log.
|
|
118
|
+
*
|
|
119
|
+
* So the projection is not baselined with the log. It is **discarded** as the
|
|
120
|
+
* last statement before the merge, which is the narrowest window this verb can
|
|
121
|
+
* offer, and when the merge fails with a projection dirty again the discard and
|
|
122
|
+
* the merge are retried exactly once. What is discarded is a rendering of the
|
|
123
|
+
* log, rebuilt at step 8 from the reconciled log, so there is nothing here to
|
|
124
|
+
* weigh: a projection carries no truth of its own and losing the last render
|
|
125
|
+
* costs nothing but the render. A projection git neither has at `HEAD` nor
|
|
126
|
+
* carries in the incoming tree is left alone, because it can stop no merge —
|
|
127
|
+
* `.approval/index.sqlite` is gitignored, and clearing a cache that blocks
|
|
128
|
+
* nothing would only make the next reader rebuild it.
|
|
129
|
+
*
|
|
130
|
+
* The snapshot the queue still gets is for the REFUSAL path alone: a refusal
|
|
131
|
+
* leaves the whole working tree as it was found, log and projection together.
|
|
132
|
+
* No success path ever copies it back — see step 8.
|
|
133
|
+
*
|
|
134
|
+
* ## This verb appends no event
|
|
135
|
+
*
|
|
136
|
+
* The log records decisions with real-world consequence. A fast-forward pull of
|
|
137
|
+
* the file the log lives in is housekeeping on the container, not a decision
|
|
138
|
+
* about the world, and an event for it would be the log narrating its own
|
|
139
|
+
* filesystem. Nothing here calls `appendEvent`, and the tests pin that the head
|
|
140
|
+
* seq is unchanged by a successful sync (SPEC §10.1, amended APRV-125).
|
|
141
|
+
*/
|
|
142
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
143
|
+
import { createHash } from "node:crypto";
|
|
144
|
+
import { dirname, join } from "node:path";
|
|
145
|
+
import { writeQueue } from "../channels/render-queue.js";
|
|
146
|
+
import { withAppendLock } from "../core/log.js";
|
|
147
|
+
import { compareChains, describeHead, } from "../core/log-reconcile.js";
|
|
148
|
+
import { isPayloadHash } from "../core/payload.js";
|
|
149
|
+
import { payloadStoreDirFor } from "../core/payload-store.js";
|
|
150
|
+
import { reindex } from "../core/reindex.js";
|
|
151
|
+
import { verify } from "../core/verify.js";
|
|
152
|
+
import { currentBranch, failureText, git, outputLines, primaryCheckout, repoPath, showBlob, } from "./git-scope.js";
|
|
153
|
+
import { DEFAULT_INDEX_PATH, DEFAULT_LOG_PATH } from "./paths.js";
|
|
154
|
+
import { DEFAULT_QUEUE_PATH } from "./render.js";
|
|
155
|
+
/**
|
|
156
|
+
* The steps, named. They are a public shape because the failure-injection hook
|
|
157
|
+
* takes one, and because a refusal says which step it stopped at.
|
|
158
|
+
*/
|
|
159
|
+
export const LOG_SYNC_STEPS = [
|
|
160
|
+
"primary",
|
|
161
|
+
"verify",
|
|
162
|
+
"snapshot",
|
|
163
|
+
"baseline",
|
|
164
|
+
"fetch",
|
|
165
|
+
"ff-check",
|
|
166
|
+
"payloads",
|
|
167
|
+
"merge",
|
|
168
|
+
"reconcile",
|
|
169
|
+
"projections",
|
|
170
|
+
"post-verify",
|
|
171
|
+
];
|
|
172
|
+
/**
|
|
173
|
+
* Machine-readable refusal codes. Frozen public API, printed in the help.
|
|
174
|
+
*
|
|
175
|
+
* `log-diverged` is the one that matters and the one the runbook is written
|
|
176
|
+
* for: it is the only outcome here that means a human has to reconcile two
|
|
177
|
+
* chains by hand, because nothing in this codebase may do it for them.
|
|
178
|
+
*/
|
|
179
|
+
export const LOG_SYNC_REFUSAL_CODES = [
|
|
180
|
+
"log-sync-not-primary",
|
|
181
|
+
"log-sync-no-branch",
|
|
182
|
+
"log-sync-unverified",
|
|
183
|
+
"log-sync-not-fast-forward",
|
|
184
|
+
"log-sync-payload-mismatch",
|
|
185
|
+
"log-diverged",
|
|
186
|
+
"log-sync-locked",
|
|
187
|
+
"log-sync-git-failed",
|
|
188
|
+
"log-sync-projection-failed",
|
|
189
|
+
"log-sync-restore-failed",
|
|
190
|
+
"log-sync-io",
|
|
191
|
+
];
|
|
192
|
+
/** Where the snapshot of a file lives while the ritual runs. */
|
|
193
|
+
function snapshotPathFor(path) {
|
|
194
|
+
return `${path}.sync-snapshot`;
|
|
195
|
+
}
|
|
196
|
+
function detail(cause) {
|
|
197
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Place `bytes` at `path` atomically: write a sibling temp file, then rename.
|
|
201
|
+
*
|
|
202
|
+
* A rename within a directory is atomic on every filesystem this runtime
|
|
203
|
+
* supports, so a reader sees the whole previous file or the whole new one. A
|
|
204
|
+
* log half-overwritten by a crash mid-restore would be the exact damage this
|
|
205
|
+
* verb exists to prevent.
|
|
206
|
+
*/
|
|
207
|
+
function placeAtomically(path, bytes) {
|
|
208
|
+
const temp = `${path}.tmp-${String(process.pid)}-${String(Date.now())}`;
|
|
209
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
210
|
+
writeFileSync(temp, bytes, { flag: "wx" });
|
|
211
|
+
renameSync(temp, path);
|
|
212
|
+
}
|
|
213
|
+
/** The bytes at `path`, or `null` when there is no such file. */
|
|
214
|
+
function readIfPresent(path) {
|
|
215
|
+
try {
|
|
216
|
+
return readFileSync(path);
|
|
217
|
+
}
|
|
218
|
+
catch (cause) {
|
|
219
|
+
if (cause.code === "ENOENT")
|
|
220
|
+
return null;
|
|
221
|
+
throw cause;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* `approval log sync`, as a function.
|
|
226
|
+
*
|
|
227
|
+
* The CLI wrapper does argv, exit codes, and formatting; everything that could
|
|
228
|
+
* damage a log is here, in one sequence, with one restore path.
|
|
229
|
+
*/
|
|
230
|
+
export function logSync(options) {
|
|
231
|
+
const { cwd, hooks } = options;
|
|
232
|
+
const remote = options.remote ?? "origin";
|
|
233
|
+
/**
|
|
234
|
+
* Arrive at a step: run any injected interference for it, and answer whether
|
|
235
|
+
* a failure was injected before it. Both seams are test-only and both are
|
|
236
|
+
* inert unless a caller passed `hooks`.
|
|
237
|
+
*/
|
|
238
|
+
const enter = (step) => {
|
|
239
|
+
if (hooks?.interfere?.step === step)
|
|
240
|
+
hooks.interfere.run();
|
|
241
|
+
return hooks?.failBefore === step;
|
|
242
|
+
};
|
|
243
|
+
// ---- step 1: the primary checkout, before anything is read ----
|
|
244
|
+
if (enter("primary")) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
code: "log-sync-io",
|
|
248
|
+
message: "injected failure before the primary-checkout check",
|
|
249
|
+
step: "primary",
|
|
250
|
+
restored: true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const primary = primaryCheckout(cwd);
|
|
254
|
+
if (!primary.ok) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
code: "log-sync-not-primary",
|
|
258
|
+
message: `log sync runs in the PRIMARY checkout only: ${primary.reason}. The committed log has one home, and a worktree that synchronized it would be advancing a chain it is not the writer of. Run this in ${primary.primary ?? "the primary checkout"}.`,
|
|
259
|
+
step: "primary",
|
|
260
|
+
restored: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const root = primary.root;
|
|
264
|
+
const logPath = join(root, DEFAULT_LOG_PATH);
|
|
265
|
+
const queuePath = join(root, DEFAULT_QUEUE_PATH);
|
|
266
|
+
const indexPath = join(root, DEFAULT_INDEX_PATH);
|
|
267
|
+
const branch = options.branch ?? currentBranch(root);
|
|
268
|
+
if (branch === null) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
code: "log-sync-no-branch",
|
|
272
|
+
message: `HEAD in ${root} is detached, so there is no branch to fast-forward. Check out the branch the log lives on and run this again.`,
|
|
273
|
+
step: "primary",
|
|
274
|
+
restored: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// Everything from here runs under the append lock: no daemon, no hook, and no
|
|
278
|
+
// other CLI verb can append while the working log is moved aside.
|
|
279
|
+
const held = withAppendLock(logPath, () => syncUnderLock({ cwd, root, logPath, queuePath, indexPath, remote, branch, enter }));
|
|
280
|
+
if (held.ok)
|
|
281
|
+
return held.value;
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
code: held.error.code === "lock-timeout" ? "log-sync-locked" : "log-sync-io",
|
|
285
|
+
message: held.error.code === "lock-timeout"
|
|
286
|
+
? `${held.error.message}. Another writer holds the append lock; sync takes it for the WHOLE operation, because an append landing mid-sync is how a chain forks. Stop the daemon (or wait for the append to finish) and run this again.`
|
|
287
|
+
: held.error.message,
|
|
288
|
+
step: "primary",
|
|
289
|
+
restored: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The ceremony proper, with the lock already held.
|
|
294
|
+
*
|
|
295
|
+
* Written as one linear sequence on purpose. Splitting it into a step-runner
|
|
296
|
+
* would hide the ordering, and the ordering IS the safety property.
|
|
297
|
+
*/
|
|
298
|
+
function syncUnderLock(ctx) {
|
|
299
|
+
const { root, logPath, queuePath, indexPath, remote, branch, enter } = ctx;
|
|
300
|
+
// ---- step 2: verify before touching anything ----
|
|
301
|
+
if (enter("verify")) {
|
|
302
|
+
return refuseBefore("verify", "log-sync-io", "injected failure before the pre-verify");
|
|
303
|
+
}
|
|
304
|
+
const before = verify(logPath);
|
|
305
|
+
if (before.status !== "clean") {
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
code: "log-sync-unverified",
|
|
309
|
+
message: before.status === "torn-tail"
|
|
310
|
+
? `${logPath} ends without a newline: the final record is truncated, the signature of a crashed write. Sync touches nothing until a human has accounted for it; run \`approval log verify\`.`
|
|
311
|
+
: `${logPath} does not verify (${before.reason}${before.firstBadSeq === null ? "" : ` at seq ${String(before.firstBadSeq)}`}): ${before.message}. Nothing is synchronized from a log that does not verify.`,
|
|
312
|
+
step: "verify",
|
|
313
|
+
restored: true,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const headBefore = before.head;
|
|
317
|
+
// ---- step 3: the snapshot ----
|
|
318
|
+
if (enter("snapshot")) {
|
|
319
|
+
return refuseBefore("snapshot", "log-sync-io", "injected failure before the snapshot");
|
|
320
|
+
}
|
|
321
|
+
const guarded = [];
|
|
322
|
+
try {
|
|
323
|
+
for (const { path, disposable } of [
|
|
324
|
+
{ path: logPath, disposable: false },
|
|
325
|
+
{ path: queuePath, disposable: true },
|
|
326
|
+
]) {
|
|
327
|
+
const snapshot = snapshotPathFor(path);
|
|
328
|
+
rmSync(snapshot, { force: true });
|
|
329
|
+
const existed = existsSync(path);
|
|
330
|
+
// A plain copy rather than a rename: the original stays in place until
|
|
331
|
+
// the baseline step deliberately replaces it, so a crash between the two
|
|
332
|
+
// leaves the working file untouched rather than absent.
|
|
333
|
+
if (existed)
|
|
334
|
+
copyFileSync(path, snapshot);
|
|
335
|
+
guarded.push({ path, snapshot, existed, disposable });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (cause) {
|
|
339
|
+
restoreAll(guarded);
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
code: "log-sync-io",
|
|
343
|
+
message: `the snapshot could not be taken: ${detail(cause)}. Nothing was pulled and nothing was changed.`,
|
|
344
|
+
step: "snapshot",
|
|
345
|
+
restored: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Payload files this run removed so the fast-forward had a clean path, with
|
|
350
|
+
* the bytes they held. Every one of them was proved identical to the incoming
|
|
351
|
+
* blob before it was removed, so putting them back is putting back exactly
|
|
352
|
+
* what was found — and {@link abort} does, on every failure after step 6.
|
|
353
|
+
*/
|
|
354
|
+
const removedPayloads = [];
|
|
355
|
+
/**
|
|
356
|
+
* Put everything back and refuse. Every failure path below goes through here,
|
|
357
|
+
* which is what makes "the working log is never left in a half state" a
|
|
358
|
+
* property of the code rather than of the author's diligence.
|
|
359
|
+
*/
|
|
360
|
+
const abort = (step, code, message, extra = {}) => {
|
|
361
|
+
restorePayloads(removedPayloads);
|
|
362
|
+
const restored = restoreAll(guarded);
|
|
363
|
+
if (!restored) {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
code: "log-sync-restore-failed",
|
|
367
|
+
message: `${message} — AND THE SNAPSHOT COULD NOT BE PUT BACK. The working log may not be what you left; the snapshot is still at ${snapshotPathFor(logPath)} and nothing has removed it. Copy it back by hand and run \`approval log verify\` before anything appends.`,
|
|
368
|
+
step,
|
|
369
|
+
restored: false,
|
|
370
|
+
...extra,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return { ok: false, code, message, step, restored: true, ...extra };
|
|
374
|
+
};
|
|
375
|
+
// ---- step 4: the baseline, so a fast-forward can move over the log ----
|
|
376
|
+
//
|
|
377
|
+
// The LOG only. It can be baselined this early because nothing can dirty it
|
|
378
|
+
// between here and the merge: this verb holds the append lock, and appending
|
|
379
|
+
// is the only thing that writes it. The projections have no such protection
|
|
380
|
+
// and are discarded at the merge instead (APRV-292).
|
|
381
|
+
if (enter("baseline")) {
|
|
382
|
+
return abort("baseline", "log-sync-io", "injected failure before the baseline");
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
for (const entry of guarded) {
|
|
386
|
+
if (entry.disposable)
|
|
387
|
+
continue;
|
|
388
|
+
const blob = showBlob(root, "HEAD", repoPath(root, entry.path));
|
|
389
|
+
if (blob === null)
|
|
390
|
+
rmSync(entry.path, { force: true });
|
|
391
|
+
else
|
|
392
|
+
placeAtomically(entry.path, blob);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (cause) {
|
|
396
|
+
return abort("baseline", "log-sync-io", `the working log could not be set to its committed bytes: ${detail(cause)}. Nothing was pulled.`);
|
|
397
|
+
}
|
|
398
|
+
const commitBefore = revision(root, "HEAD");
|
|
399
|
+
// ---- step 5a: fetch ----
|
|
400
|
+
if (enter("fetch")) {
|
|
401
|
+
return abort("fetch", "log-sync-git-failed", "injected failure before the fetch");
|
|
402
|
+
}
|
|
403
|
+
const fetched = git(["fetch", remote, branch], root);
|
|
404
|
+
if (!fetched.ok) {
|
|
405
|
+
return abort("fetch", "log-sync-git-failed", `\`git fetch ${remote} ${branch}\` failed: ${failureText(fetched)}. Nothing was pulled and the working log is back as it was.`, { quote: outputLines(fetched.stderr, fetched.stdout) });
|
|
406
|
+
}
|
|
407
|
+
// ---- step 5b: the fast-forward CHECK, before the merge is attempted ----
|
|
408
|
+
if (enter("ff-check")) {
|
|
409
|
+
return abort("ff-check", "log-sync-git-failed", "injected failure before the fast-forward check");
|
|
410
|
+
}
|
|
411
|
+
const target = revision(root, "FETCH_HEAD");
|
|
412
|
+
const ancestor = git(["merge-base", "--is-ancestor", "HEAD", "FETCH_HEAD"], root);
|
|
413
|
+
if (!ancestor.ok) {
|
|
414
|
+
return abort("ff-check", "log-sync-not-fast-forward", `${remote}/${branch} (${short(target)}) is not a fast-forward of HEAD (${short(commitBefore)}): this checkout carries commits the remote does not. Sync only fast-forwards, because a merge commit over ${DEFAULT_LOG_PATH} is a merge of two hash chains, and chains do not merge. Land your commits first (\`approval log advance\`), then sync.`);
|
|
415
|
+
}
|
|
416
|
+
// ---- step 6: untracked payload files the incoming tree also carries ----
|
|
417
|
+
//
|
|
418
|
+
// Here and not earlier: the fast-forward is known to be possible, so removing
|
|
419
|
+
// a file the merge is about to write is a step towards a merge that WILL run.
|
|
420
|
+
// Here and not later: `git merge --ff-only` is what stops on these, so they
|
|
421
|
+
// have to be gone before it is asked.
|
|
422
|
+
if (enter("payloads")) {
|
|
423
|
+
return abort("payloads", "log-sync-io", "injected failure before the payload reconciliation");
|
|
424
|
+
}
|
|
425
|
+
const overlapping = overlappingPayloads(root, logPath, "FETCH_HEAD");
|
|
426
|
+
if (!overlapping.ok) {
|
|
427
|
+
return abort("payloads", "log-sync-git-failed", overlapping.message, { quote: overlapping.quote });
|
|
428
|
+
}
|
|
429
|
+
// Two passes, and the order is the safety property: every file is proved
|
|
430
|
+
// before any file is removed, so a mismatch in the last one cannot have
|
|
431
|
+
// deleted the first.
|
|
432
|
+
const confirmed = [];
|
|
433
|
+
for (const relative of overlapping.paths) {
|
|
434
|
+
const checked = checkPayload(root, relative, "FETCH_HEAD");
|
|
435
|
+
if (!checked.ok) {
|
|
436
|
+
return abort("payloads", "log-sync-payload-mismatch", checked.message);
|
|
437
|
+
}
|
|
438
|
+
confirmed.push({ relative, path: join(root, relative), bytes: checked.bytes });
|
|
439
|
+
}
|
|
440
|
+
for (const entry of confirmed) {
|
|
441
|
+
try {
|
|
442
|
+
rmSync(entry.path, { force: true });
|
|
443
|
+
removedPayloads.push({ path: entry.path, bytes: entry.bytes });
|
|
444
|
+
}
|
|
445
|
+
catch (cause) {
|
|
446
|
+
return abort("payloads", "log-sync-io", `the untracked payload ${entry.relative} could not be moved out of the fast-forward's way: ${detail(cause)}. Nothing was pulled.`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const payloadsReconciled = confirmed.length;
|
|
450
|
+
// ---- step 5c: the projections are discarded, and then the merge ----
|
|
451
|
+
//
|
|
452
|
+
// The discard is the last statement before the merge, and that placement is
|
|
453
|
+
// the fix APRV-292 is: a projection has no lock, so the daemon can re-render
|
|
454
|
+
// it at any instant, and every statement between the discard and the merge is
|
|
455
|
+
// a window in which a stale render stops a pull of the log.
|
|
456
|
+
if (enter("merge")) {
|
|
457
|
+
return abort("merge", "log-sync-git-failed", "injected failure before the merge");
|
|
458
|
+
}
|
|
459
|
+
const disposable = disposableProjections(root, [queuePath, indexPath]);
|
|
460
|
+
const discarded = discardProjections(root, disposable);
|
|
461
|
+
if (!discarded.ok)
|
|
462
|
+
return abort("merge", "log-sync-io", discarded.message);
|
|
463
|
+
let merged = git(["merge", "--ff-only", "FETCH_HEAD"], root);
|
|
464
|
+
let retried = false;
|
|
465
|
+
if (!merged.ok && dirtyProjections(root, disposable)) {
|
|
466
|
+
// The renderer beat the merge to the file. One retry, and only for this
|
|
467
|
+
// reason: any other failure is a real one, and repeating it would refuse
|
|
468
|
+
// twice as slowly.
|
|
469
|
+
retried = true;
|
|
470
|
+
const again = discardProjections(root, disposable);
|
|
471
|
+
if (!again.ok)
|
|
472
|
+
return abort("merge", "log-sync-io", again.message);
|
|
473
|
+
merged = git(["merge", "--ff-only", "FETCH_HEAD"], root);
|
|
474
|
+
}
|
|
475
|
+
if (!merged.ok) {
|
|
476
|
+
const racing = retried && dirtyProjections(root, disposable);
|
|
477
|
+
return abort("merge", "log-sync-git-failed", `\`git merge --ff-only FETCH_HEAD\` failed: ${failureText(merged)}. The working log is back as it was.${racing
|
|
478
|
+
? ` ${disposable.join(", ")} was dirty again on both attempts: something is rewriting the projection faster than the fast-forward can run. Stop the writer (usually the daemon) and run this again.`
|
|
479
|
+
: ""}`, { quote: outputLines(merged.stderr, merged.stdout) });
|
|
480
|
+
}
|
|
481
|
+
const commitAfter = revision(root, "HEAD");
|
|
482
|
+
// ---- step 6: reconcile ----
|
|
483
|
+
if (enter("reconcile")) {
|
|
484
|
+
return abort("reconcile", "log-sync-io", "injected failure before the reconcile");
|
|
485
|
+
}
|
|
486
|
+
const snapshotText = textOf(snapshotPathFor(logPath));
|
|
487
|
+
const committedText = textOf(logPath);
|
|
488
|
+
const compared = compareChains({ label: `the working log ${logPath}`, text: snapshotText }, { label: `the committed log at ${short(commitAfter)}`, text: committedText });
|
|
489
|
+
if (!compared.ok) {
|
|
490
|
+
return abort("reconcile", "log-sync-unverified", compared.message);
|
|
491
|
+
}
|
|
492
|
+
const drift = compared.drift;
|
|
493
|
+
if (drift.relation === "diverged") {
|
|
494
|
+
return abort("reconcile", "log-diverged", `the committed log and the working log are two different chains. They agree through seq ${String((drift.firstDivergentSeq ?? 1) - 1)} and part at seq ${String(drift.firstDivergentSeq)}: working head ${describeHead(drift.workingHead)}, committed head ${describeHead(drift.committedHead)}. Nothing was merged and nothing was re-chained — re-chaining is fabrication, and hash chains do not survive a merge. Your working log has been restored exactly as it was found; the pull DID advance the git checkout to ${short(commitAfter)}. A human has to decide which chain is the log.`, { drift });
|
|
495
|
+
}
|
|
496
|
+
// A prefix relationship in either direction is safe, and the two directions
|
|
497
|
+
// are handled differently for one reason: whichever chain is LONGER contains
|
|
498
|
+
// the other whole, so adopting it extends and never rewinds.
|
|
499
|
+
// ahead — the committed baseline is a prefix of ours: put the snapshot back.
|
|
500
|
+
// behind — the pull brought records we did not have: keep the pulled file.
|
|
501
|
+
// equal — nothing to do; the file on disk is already the chain.
|
|
502
|
+
let restored = false;
|
|
503
|
+
if (drift.relation === "ahead") {
|
|
504
|
+
const snapshot = readIfPresent(snapshotPathFor(logPath));
|
|
505
|
+
if (snapshot === null) {
|
|
506
|
+
return abort("reconcile", "log-sync-io", `the snapshot at ${snapshotPathFor(logPath)} disappeared before it could be restored.`);
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
placeAtomically(logPath, snapshot);
|
|
510
|
+
restored = true;
|
|
511
|
+
}
|
|
512
|
+
catch (cause) {
|
|
513
|
+
return abort("reconcile", "log-sync-io", `the snapshot could not be restored over ${logPath}: ${detail(cause)}.`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// ---- step 7: projections, REBUILT from the reconciled log ----
|
|
517
|
+
//
|
|
518
|
+
// Rebuilt, never copied back from the snapshot taken at step 3: that snapshot
|
|
519
|
+
// is the refusal path's, and a projection restored on a SUCCESS would be a
|
|
520
|
+
// screenshot asserting something the log no longer says.
|
|
521
|
+
if (enter("projections")) {
|
|
522
|
+
return abort("projections", "log-sync-projection-failed", "injected failure before the projection rebuild");
|
|
523
|
+
}
|
|
524
|
+
const rendered = writeQueue(logPath, queuePath, { policy: { dir: root } }, new Date().toISOString());
|
|
525
|
+
if (!rendered.ok) {
|
|
526
|
+
return abort("projections", "log-sync-projection-failed", `the queue projection could not be rebuilt from the reconciled log (${rendered.code}): ${rendered.message}`);
|
|
527
|
+
}
|
|
528
|
+
let index = "absent";
|
|
529
|
+
if (existsSync(indexPath)) {
|
|
530
|
+
const reindexed = reindex(logPath, indexPath, { force: true });
|
|
531
|
+
if (!reindexed.ok) {
|
|
532
|
+
return abort("projections", "log-sync-projection-failed", `the index could not be rebuilt from the reconciled log (${reindexed.error.code}): ${reindexed.error.message}`);
|
|
533
|
+
}
|
|
534
|
+
index = "rebuilt";
|
|
535
|
+
}
|
|
536
|
+
// ---- step 8: post-verify, and only then is the snapshot let go ----
|
|
537
|
+
if (enter("post-verify")) {
|
|
538
|
+
return abort("post-verify", "log-sync-unverified", "injected failure before the post-verify");
|
|
539
|
+
}
|
|
540
|
+
const after = verify(logPath);
|
|
541
|
+
if (after.status !== "clean") {
|
|
542
|
+
return abort("post-verify", "log-sync-unverified", `the reconciled log does not verify (${after.status === "torn-tail" ? "torn-tail" : after.reason}). The working log has been restored to what it was before the sync.`);
|
|
543
|
+
}
|
|
544
|
+
for (const entry of guarded)
|
|
545
|
+
rmSync(entry.snapshot, { force: true });
|
|
546
|
+
return {
|
|
547
|
+
ok: true,
|
|
548
|
+
report: {
|
|
549
|
+
root,
|
|
550
|
+
logPath,
|
|
551
|
+
remote,
|
|
552
|
+
branch,
|
|
553
|
+
commitBefore,
|
|
554
|
+
commitAfter,
|
|
555
|
+
pulled: countCommits(root, commitBefore, commitAfter),
|
|
556
|
+
headBefore,
|
|
557
|
+
headAfter: after.head,
|
|
558
|
+
relation: drift.relation,
|
|
559
|
+
ahead: drift.ahead,
|
|
560
|
+
behind: drift.behind,
|
|
561
|
+
restored,
|
|
562
|
+
payloadsReconciled,
|
|
563
|
+
queue: { path: rendered.path, bytes: rendered.bytes },
|
|
564
|
+
index,
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Put back every payload file this run removed.
|
|
570
|
+
*
|
|
571
|
+
* Deliberately quiet, and deliberately not part of `restoreAll`'s success
|
|
572
|
+
* answer: each of these was proved byte-identical to a blob the remote holds
|
|
573
|
+
* before it was removed, so the worst case of a failure here is a file that has
|
|
574
|
+
* to be checked out again, never a byte anyone can no longer obtain. The log's
|
|
575
|
+
* restore is the one that must be loud, and it stays the one that is.
|
|
576
|
+
*/
|
|
577
|
+
function restorePayloads(removed) {
|
|
578
|
+
for (const entry of removed) {
|
|
579
|
+
if (existsSync(entry.path))
|
|
580
|
+
continue;
|
|
581
|
+
try {
|
|
582
|
+
placeAtomically(entry.path, entry.bytes);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// See above: recoverable from the remote, and never worth masking the
|
|
586
|
+
// refusal that brought us here.
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// The projections in the fast-forward's way (APRV-292)
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
/**
|
|
594
|
+
* The projections a fast-forward could collide with, as repo-relative paths.
|
|
595
|
+
*
|
|
596
|
+
* A projection git neither has at `HEAD` nor carries in the incoming tree can
|
|
597
|
+
* stop no merge, so it is not in this list and nothing below touches it. That
|
|
598
|
+
* is the whole reason `.approval/index.sqlite` — gitignored, a cache, and
|
|
599
|
+
* potentially large — is passed in here rather than special-cased: whether it
|
|
600
|
+
* is disposable in the git sense is a question about the repository, and the
|
|
601
|
+
* repository is asked.
|
|
602
|
+
*/
|
|
603
|
+
function disposableProjections(root, paths) {
|
|
604
|
+
const found = [];
|
|
605
|
+
for (const path of paths) {
|
|
606
|
+
const relative = repoPath(root, path);
|
|
607
|
+
// A projection outside the repository is not something a fast-forward can
|
|
608
|
+
// collide with either.
|
|
609
|
+
if (relative.startsWith(".."))
|
|
610
|
+
continue;
|
|
611
|
+
if (!carries(root, "HEAD", relative) && !carries(root, "FETCH_HEAD", relative))
|
|
612
|
+
continue;
|
|
613
|
+
found.push(relative);
|
|
614
|
+
}
|
|
615
|
+
return found;
|
|
616
|
+
}
|
|
617
|
+
/** Does `rev` carry a blob at `relative`? `-e` asks and reads nothing. */
|
|
618
|
+
function carries(root, rev, relative) {
|
|
619
|
+
return git(["cat-file", "-e", `${rev}:${relative}`], root).ok;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Throw the working copy of every disposable projection away.
|
|
623
|
+
*
|
|
624
|
+
* "Throw away" is a projection's whole relationship to this verb. It is a
|
|
625
|
+
* rendering of the log, it is rebuilt from the reconciled log two steps later,
|
|
626
|
+
* and whatever the renderer wrote into it a millisecond ago says nothing the
|
|
627
|
+
* log does not. So there is nothing to weigh here and no proof to demand — the
|
|
628
|
+
* opposite of the payload reconciliation above, where every byte is evidence
|
|
629
|
+
* and every removal is proved first.
|
|
630
|
+
*
|
|
631
|
+
* The bytes written are the ones git has at `HEAD`, which leaves the path clean
|
|
632
|
+
* for the merge. A projection only the incoming tree carries has no `HEAD`
|
|
633
|
+
* bytes and is removed instead: an existing untracked file is the other thing
|
|
634
|
+
* `git merge --ff-only` refuses to write over.
|
|
635
|
+
*/
|
|
636
|
+
function discardProjections(root, relatives) {
|
|
637
|
+
for (const relative of relatives) {
|
|
638
|
+
const path = join(root, relative);
|
|
639
|
+
try {
|
|
640
|
+
const blob = showBlob(root, "HEAD", relative);
|
|
641
|
+
if (blob === null)
|
|
642
|
+
rmSync(path, { force: true });
|
|
643
|
+
else
|
|
644
|
+
placeAtomically(path, blob);
|
|
645
|
+
}
|
|
646
|
+
catch (cause) {
|
|
647
|
+
return {
|
|
648
|
+
ok: false,
|
|
649
|
+
message: `the projection ${relative} could not be moved out of the fast-forward's way: ${detail(cause)}. Nothing was pulled and the working log is back as it was.`,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return { ok: true };
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Is any disposable projection dirty in the working tree right now?
|
|
657
|
+
*
|
|
658
|
+
* Asked after a failed merge, to tell the one failure worth retrying (the
|
|
659
|
+
* renderer wrote the file again in the microseconds since the discard) from
|
|
660
|
+
* every other failure, which a retry would only repeat.
|
|
661
|
+
*/
|
|
662
|
+
function dirtyProjections(root, relatives) {
|
|
663
|
+
if (relatives.length === 0)
|
|
664
|
+
return false;
|
|
665
|
+
const status = git(["status", "--porcelain", "-z", "--", ...relatives], root);
|
|
666
|
+
return status.ok && pathLines(status.stdout).length > 0;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Repo-relative paths out of a NUL-separated git listing.
|
|
670
|
+
*
|
|
671
|
+
* `-z` on both listings, so the two sets are spelled the same way. Without it
|
|
672
|
+
* git quotes any path it considers unusual, and a quoted name on one side would
|
|
673
|
+
* simply fail to match its unquoted self on the other — dropping the file out of
|
|
674
|
+
* the intersection, which is to say handling it by not noticing it.
|
|
675
|
+
*/
|
|
676
|
+
function pathLines(stdout) {
|
|
677
|
+
return stdout.split("\0").filter((line) => line.length > 0);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Untracked, non-ignored payload files that `rev` also carries.
|
|
681
|
+
*
|
|
682
|
+
* The intersection is the whole point. An untracked payload the incoming tree
|
|
683
|
+
* does not have blocks nothing and is left alone; a tracked one is git's
|
|
684
|
+
* business and rides the fast-forward like any other file. What is left is
|
|
685
|
+
* exactly the set `git merge --ff-only` refuses to write over.
|
|
686
|
+
*
|
|
687
|
+
* `--exclude-standard` is on: an ignored file is not one git would refuse to
|
|
688
|
+
* overwrite, so including it here would make sync stricter than the merge it is
|
|
689
|
+
* clearing the way for.
|
|
690
|
+
*/
|
|
691
|
+
function overlappingPayloads(root, logPath, rev) {
|
|
692
|
+
const storeRelative = repoPath(root, payloadStoreDirFor(logPath));
|
|
693
|
+
// A store outside the repository is not something a fast-forward can collide
|
|
694
|
+
// with, so there is nothing here to reconcile.
|
|
695
|
+
if (storeRelative.startsWith(".."))
|
|
696
|
+
return { ok: true, paths: [] };
|
|
697
|
+
const untracked = git(["ls-files", "--others", "--exclude-standard", "-z", "--", storeRelative], root);
|
|
698
|
+
if (!untracked.ok) {
|
|
699
|
+
return {
|
|
700
|
+
ok: false,
|
|
701
|
+
message: `\`git ls-files --others\` over ${storeRelative} failed: ${failureText(untracked)}. Nothing was pulled and the working log is back as it was.`,
|
|
702
|
+
quote: outputLines(untracked.stderr, untracked.stdout),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const local = new Set(pathLines(untracked.stdout));
|
|
706
|
+
if (local.size === 0)
|
|
707
|
+
return { ok: true, paths: [] };
|
|
708
|
+
const incoming = git(["ls-tree", "-r", "--name-only", "-z", rev, "--", storeRelative], root);
|
|
709
|
+
if (!incoming.ok) {
|
|
710
|
+
return {
|
|
711
|
+
ok: false,
|
|
712
|
+
message: `\`git ls-tree ${rev}\` over ${storeRelative} failed: ${failureText(incoming)}. Nothing was pulled and the working log is back as it was.`,
|
|
713
|
+
quote: outputLines(incoming.stderr, incoming.stdout),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const carried = new Set(pathLines(incoming.stdout));
|
|
717
|
+
return { ok: true, paths: [...local].filter((path) => carried.has(path)).sort() };
|
|
718
|
+
}
|
|
719
|
+
/** SHA-256 of some bytes, lowercase hex: the store's content address. */
|
|
720
|
+
function sha256(bytes) {
|
|
721
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
722
|
+
}
|
|
723
|
+
/** Is this the store's `{"$ref": "<uri>"}` form, and nothing else? */
|
|
724
|
+
function isReferenceForm(bytes) {
|
|
725
|
+
let value;
|
|
726
|
+
try {
|
|
727
|
+
value = JSON.parse(bytes.toString("utf8"));
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
733
|
+
return false;
|
|
734
|
+
const keys = Object.keys(value);
|
|
735
|
+
return keys.length === 1 && keys[0] === "$ref" && typeof value.$ref === "string";
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Prove one untracked payload safe to remove, and answer the bytes it holds.
|
|
739
|
+
*
|
|
740
|
+
* Two independent facts, both required, neither inferred from the other:
|
|
741
|
+
* the file is self-addressed, and its bytes are the incoming bytes. The second
|
|
742
|
+
* is what makes removal safe; the first is what catches a local file that was
|
|
743
|
+
* already wrong before any of this started, so that sync is not the step that
|
|
744
|
+
* quietly discards it.
|
|
745
|
+
*/
|
|
746
|
+
function checkPayload(root, relative, rev) {
|
|
747
|
+
const path = join(root, relative);
|
|
748
|
+
const name = relative.slice(relative.lastIndexOf("/") + 1);
|
|
749
|
+
const stem = name.endsWith(".json") ? name.slice(0, -".json".length) : name;
|
|
750
|
+
const local = readIfPresent(path);
|
|
751
|
+
if (local === null) {
|
|
752
|
+
return {
|
|
753
|
+
ok: false,
|
|
754
|
+
message: `the untracked payload ${relative} disappeared while sync was reading it. Nothing was pulled and the working log is back as it was.`,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
if (!isPayloadHash(stem)) {
|
|
758
|
+
return {
|
|
759
|
+
ok: false,
|
|
760
|
+
message: `${relative} sits in the payload store and is not named by a payload hash (SHA-256, lowercase hex). The store is addressed by hash and nothing else, so sync will not clear this file out of a fast-forward's way. Move it somewhere that is not the payload store and run \`approval log sync\` again.`,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
const digest = sha256(local);
|
|
764
|
+
if (digest !== stem && !isReferenceForm(local)) {
|
|
765
|
+
return {
|
|
766
|
+
ok: false,
|
|
767
|
+
message: `the untracked payload ${relative} does not hash to its own name: its bytes are ${digest}. A payload file is the material evidence an approval bound to, and one that is not what it says it is has to be accounted for by a human before anything overwrites it. Nothing was pulled, nothing was appended, and the working tree is as you left it.`,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const incoming = showBlob(root, rev, relative);
|
|
771
|
+
if (incoming === null) {
|
|
772
|
+
return {
|
|
773
|
+
ok: false,
|
|
774
|
+
message: `the incoming commit lists ${relative} and git could not read its bytes. Nothing was pulled and the working log is back as it was.`,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (!incoming.equals(local)) {
|
|
778
|
+
return {
|
|
779
|
+
ok: false,
|
|
780
|
+
message: `the untracked payload ${relative} is not the payload the incoming commit carries: local bytes hash to ${digest}, incoming bytes to ${sha256(incoming)}. Two different versions of one content-addressed payload is a question about which bytes an approval bound to, not a merge conflict, so sync removes neither. Nothing was pulled, nothing was appended, and the working tree is as you left it.`,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
return { ok: true, bytes: local };
|
|
784
|
+
}
|
|
785
|
+
/** A refusal from before the snapshot exists: there is nothing to put back. */
|
|
786
|
+
function refuseBefore(step, code, message) {
|
|
787
|
+
return { ok: false, code, message, step, restored: true };
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Put every guarded file back exactly as it was found, and drop the snapshots.
|
|
791
|
+
*
|
|
792
|
+
* Returns false when any file could not be restored, which is the one outcome
|
|
793
|
+
* that must never be reported quietly: the caller turns it into
|
|
794
|
+
* `log-sync-restore-failed` and leaves the snapshot on disk.
|
|
795
|
+
*/
|
|
796
|
+
function restoreAll(guarded) {
|
|
797
|
+
let ok = true;
|
|
798
|
+
for (const entry of guarded) {
|
|
799
|
+
try {
|
|
800
|
+
if (entry.existed) {
|
|
801
|
+
const bytes = readIfPresent(entry.snapshot);
|
|
802
|
+
if (bytes === null) {
|
|
803
|
+
ok = false;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
placeAtomically(entry.path, bytes);
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
rmSync(entry.path, { force: true });
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
ok = false;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
rmSync(entry.snapshot, { force: true });
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
// The file is back; a leftover snapshot is untidy, never unsafe.
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return ok;
|
|
824
|
+
}
|
|
825
|
+
/** A file's text, or the empty string when it is not there. */
|
|
826
|
+
function textOf(path) {
|
|
827
|
+
const bytes = readIfPresent(path);
|
|
828
|
+
return bytes === null ? "" : bytes.toString("utf8");
|
|
829
|
+
}
|
|
830
|
+
/** `git rev-parse <rev>`, or the empty string. */
|
|
831
|
+
function revision(root, rev) {
|
|
832
|
+
const result = git(["rev-parse", rev], root);
|
|
833
|
+
return result.ok ? result.stdout.trim() : "";
|
|
834
|
+
}
|
|
835
|
+
/** How many commits `to` carries beyond `from`. */
|
|
836
|
+
function countCommits(root, from, to) {
|
|
837
|
+
if (from.length === 0 || to.length === 0 || from === to)
|
|
838
|
+
return 0;
|
|
839
|
+
const result = git(["rev-list", "--count", `${from}..${to}`], root);
|
|
840
|
+
if (!result.ok)
|
|
841
|
+
return 0;
|
|
842
|
+
const parsed = Number.parseInt(result.stdout.trim(), 10);
|
|
843
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
844
|
+
}
|
|
845
|
+
/** A commit, as a human reads one. */
|
|
846
|
+
export function short(commit) {
|
|
847
|
+
return commit.length === 0 ? "none" : commit.slice(0, 12);
|
|
848
|
+
}
|
|
849
|
+
//# sourceMappingURL=log-sync.js.map
|