toolroll 0.5.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 +21 -0
- package/README.md +1113 -0
- package/THIRD_PARTY_NOTICES.md +449 -0
- package/dist/accent-colors.d.ts +48 -0
- package/dist/accent-colors.js +122 -0
- package/dist/action-ledger.d.ts +26 -0
- package/dist/action-ledger.js +75 -0
- package/dist/agent-fence.d.ts +42 -0
- package/dist/agent-fence.js +183 -0
- package/dist/agentconfig.d.ts +216 -0
- package/dist/agentconfig.js +454 -0
- package/dist/api-tokens.d.ts +16 -0
- package/dist/api-tokens.js +27 -0
- package/dist/approval-policy.d.ts +69 -0
- package/dist/approval-policy.js +137 -0
- package/dist/approval-rules-ui.d.ts +25 -0
- package/dist/approval-rules-ui.js +39 -0
- package/dist/assignment-adapters.d.ts +180 -0
- package/dist/assignment-adapters.js +239 -0
- package/dist/assignment-brief.d.ts +71 -0
- package/dist/assignment-brief.js +129 -0
- package/dist/assignment-delivery.d.ts +56 -0
- package/dist/assignment-delivery.js +160 -0
- package/dist/assignment-presentation.d.ts +26 -0
- package/dist/assignment-presentation.js +80 -0
- package/dist/assignment-status.d.ts +62 -0
- package/dist/assignment-status.js +152 -0
- package/dist/assignment-ui.d.ts +71 -0
- package/dist/assignment-ui.js +103 -0
- package/dist/assignment.d.ts +222 -0
- package/dist/assignment.js +399 -0
- package/dist/attest.d.ts +56 -0
- package/dist/attest.js +153 -0
- package/dist/backend.d.ts +97 -0
- package/dist/backend.js +166 -0
- package/dist/backup-ui.d.ts +22 -0
- package/dist/backup-ui.js +59 -0
- package/dist/backup.d.ts +84 -0
- package/dist/backup.js +421 -0
- package/dist/beads.d.ts +34 -0
- package/dist/beads.js +135 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +22 -0
- package/dist/board.d.ts +150 -0
- package/dist/board.js +210 -0
- package/dist/boot-identity.d.ts +63 -0
- package/dist/boot-identity.js +99 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +2295 -0
- package/dist/browser/workspace.css +4 -0
- package/dist/browser/workspace.js +225 -0
- package/dist/browser-crew.d.ts +11 -0
- package/dist/browser-crew.js +50 -0
- package/dist/browser-shell.d.ts +8 -0
- package/dist/browser-shell.js +60 -0
- package/dist/browser-workspace.d.ts +780 -0
- package/dist/browser-workspace.js +47 -0
- package/dist/budget-alerts.d.ts +19 -0
- package/dist/budget-alerts.js +46 -0
- package/dist/builder.d.ts +370 -0
- package/dist/builder.js +3262 -0
- package/dist/capscan.d.ts +35 -0
- package/dist/capscan.js +113 -0
- package/dist/chat-acceptance.d.ts +71 -0
- package/dist/chat-acceptance.js +120 -0
- package/dist/chat-actions.d.ts +265 -0
- package/dist/chat-actions.js +1273 -0
- package/dist/chat-channel.d.ts +109 -0
- package/dist/chat-channel.js +652 -0
- package/dist/chat-continuity.d.ts +32 -0
- package/dist/chat-continuity.js +317 -0
- package/dist/chat-controls.d.ts +113 -0
- package/dist/chat-controls.js +53 -0
- package/dist/chat-delivery-state.d.ts +159 -0
- package/dist/chat-delivery-state.js +309 -0
- package/dist/chat-delivery.d.ts +38 -0
- package/dist/chat-delivery.js +693 -0
- package/dist/chat-display.d.ts +1 -0
- package/dist/chat-display.js +18 -0
- package/dist/chat-evidence.d.ts +123 -0
- package/dist/chat-evidence.js +207 -0
- package/dist/chat-flow.d.ts +44 -0
- package/dist/chat-flow.js +215 -0
- package/dist/chat-inbox.d.ts +57 -0
- package/dist/chat-inbox.js +123 -0
- package/dist/chat-polish.d.ts +16 -0
- package/dist/chat-polish.js +135 -0
- package/dist/chat-review.d.ts +46 -0
- package/dist/chat-review.js +98 -0
- package/dist/chat-rooms.d.ts +123 -0
- package/dist/chat-rooms.js +216 -0
- package/dist/chat-task-actions.d.ts +44 -0
- package/dist/chat-task-actions.js +79 -0
- package/dist/check-progress.d.ts +51 -0
- package/dist/check-progress.js +290 -0
- package/dist/child-database.d.ts +12 -0
- package/dist/child-database.js +30 -0
- package/dist/claim.d.ts +688 -0
- package/dist/claim.js +1740 -0
- package/dist/cli.d.ts +137 -0
- package/dist/cli.js +1461 -0
- package/dist/codex-limits.d.ts +15 -0
- package/dist/codex-limits.js +145 -0
- package/dist/coding-context.d.ts +55 -0
- package/dist/coding-context.js +92 -0
- package/dist/coding-handoff.d.ts +57 -0
- package/dist/coding-handoff.js +277 -0
- package/dist/coding-provider.d.ts +72 -0
- package/dist/coding-provider.js +424 -0
- package/dist/coding-shipping-ui.d.ts +4 -0
- package/dist/coding-shipping-ui.js +12 -0
- package/dist/coding-types.d.ts +62 -0
- package/dist/coding-types.js +1 -0
- package/dist/coding-ui.d.ts +17 -0
- package/dist/coding-ui.js +364 -0
- package/dist/coding-update.d.ts +15 -0
- package/dist/coding-update.js +149 -0
- package/dist/coding-workspace.d.ts +124 -0
- package/dist/coding-workspace.js +768 -0
- package/dist/container-state.d.ts +5 -0
- package/dist/container-state.js +62 -0
- package/dist/containment.d.ts +197 -0
- package/dist/containment.js +559 -0
- package/dist/contest.d.ts +318 -0
- package/dist/contest.js +753 -0
- package/dist/control-setup.d.ts +35 -0
- package/dist/control-setup.js +40 -0
- package/dist/control-ui.d.ts +29 -0
- package/dist/control-ui.js +53 -0
- package/dist/controller-service.d.ts +1 -0
- package/dist/controller-service.js +19 -0
- package/dist/controller-supervisor.d.ts +30 -0
- package/dist/controller-supervisor.js +104 -0
- package/dist/converse.d.ts +304 -0
- package/dist/converse.js +849 -0
- package/dist/coordinator-proposals.d.ts +29 -0
- package/dist/coordinator-proposals.js +137 -0
- package/dist/coordinator.d.ts +186 -0
- package/dist/coordinator.js +447 -0
- package/dist/credentials-ui.d.ts +25 -0
- package/dist/credentials-ui.js +48 -0
- package/dist/daemon.d.ts +209 -0
- package/dist/daemon.js +604 -0
- package/dist/decision.d.ts +120 -0
- package/dist/decision.js +388 -0
- package/dist/demo.d.ts +55 -0
- package/dist/demo.js +992 -0
- package/dist/desktop-access.d.ts +28 -0
- package/dist/desktop-access.js +88 -0
- package/dist/desktop-bundle.d.ts +16 -0
- package/dist/desktop-bundle.js +74 -0
- package/dist/desktop-host.d.ts +54 -0
- package/dist/desktop-host.js +508 -0
- package/dist/desktop-update-gate.d.ts +12 -0
- package/dist/desktop-update-gate.js +91 -0
- package/dist/desktop-update-recovery.d.ts +14 -0
- package/dist/desktop-update-recovery.js +243 -0
- package/dist/desktop-update.d.ts +110 -0
- package/dist/desktop-update.js +740 -0
- package/dist/discord-api.d.ts +20 -0
- package/dist/discord-api.js +138 -0
- package/dist/discord-chat.d.ts +16 -0
- package/dist/discord-chat.js +381 -0
- package/dist/discord-settings.d.ts +7 -0
- package/dist/discord-settings.js +75 -0
- package/dist/discord.d.ts +10 -0
- package/dist/discord.js +169 -0
- package/dist/discover.d.ts +75 -0
- package/dist/discover.js +150 -0
- package/dist/dispatch.d.ts +100 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispose.d.ts +161 -0
- package/dist/dispose.js +803 -0
- package/dist/email-settings.d.ts +34 -0
- package/dist/email-settings.js +50 -0
- package/dist/envelope.d.ts +47 -0
- package/dist/envelope.js +105 -0
- package/dist/evidence-pack.d.ts +185 -0
- package/dist/evidence-pack.js +258 -0
- package/dist/evidence.d.ts +345 -0
- package/dist/evidence.js +782 -0
- package/dist/exec.d.ts +285 -0
- package/dist/exec.js +1841 -0
- package/dist/exhaustion.d.ts +85 -0
- package/dist/exhaustion.js +141 -0
- package/dist/export-ui.d.ts +4 -0
- package/dist/export-ui.js +17 -0
- package/dist/export.d.ts +48 -0
- package/dist/export.js +305 -0
- package/dist/flow-actions.d.ts +77 -0
- package/dist/flow-actions.js +203 -0
- package/dist/flow-code.d.ts +50 -0
- package/dist/flow-code.js +159 -0
- package/dist/flow-draft.d.ts +38 -0
- package/dist/flow-draft.js +80 -0
- package/dist/flow-engine.d.ts +66 -0
- package/dist/flow-engine.js +416 -0
- package/dist/flow-insights.d.ts +95 -0
- package/dist/flow-insights.js +149 -0
- package/dist/flow-live.d.ts +24 -0
- package/dist/flow-live.js +150 -0
- package/dist/flow-people.d.ts +24 -0
- package/dist/flow-people.js +91 -0
- package/dist/flow-replies.d.ts +48 -0
- package/dist/flow-replies.js +113 -0
- package/dist/flow-scripts.d.ts +38 -0
- package/dist/flow-scripts.js +73 -0
- package/dist/flow-secrets.d.ts +13 -0
- package/dist/flow-secrets.js +45 -0
- package/dist/flow-sort.d.ts +82 -0
- package/dist/flow-sort.js +153 -0
- package/dist/flow-steps.d.ts +40 -0
- package/dist/flow-steps.js +408 -0
- package/dist/flow-triggers.d.ts +244 -0
- package/dist/flow-triggers.js +959 -0
- package/dist/flows-ui.d.ts +20 -0
- package/dist/flows-ui.js +175 -0
- package/dist/flows.d.ts +229 -0
- package/dist/flows.js +968 -0
- package/dist/fonts.d.ts +19 -0
- package/dist/fonts.js +19 -0
- package/dist/gaps.d.ts +35 -0
- package/dist/gaps.js +101 -0
- package/dist/gate-failure.d.ts +40 -0
- package/dist/gate-failure.js +66 -0
- package/dist/git.d.ts +54 -0
- package/dist/git.js +94 -0
- package/dist/google-mail.d.ts +58 -0
- package/dist/google-mail.js +151 -0
- package/dist/grant.d.ts +136 -0
- package/dist/grant.js +238 -0
- package/dist/graph.d.ts +164 -0
- package/dist/graph.js +383 -0
- package/dist/guides.d.ts +22 -0
- package/dist/guides.js +363 -0
- package/dist/held.d.ts +150 -0
- package/dist/held.js +799 -0
- package/dist/invoke.d.ts +112 -0
- package/dist/invoke.js +780 -0
- package/dist/issues.d.ts +41 -0
- package/dist/issues.js +131 -0
- package/dist/job-object-helper.ps1 +252 -0
- package/dist/jsonl-discriminants.d.ts +11 -0
- package/dist/jsonl-discriminants.js +93 -0
- package/dist/keys.d.ts +104 -0
- package/dist/keys.js +229 -0
- package/dist/kits-ui.d.ts +17 -0
- package/dist/kits-ui.js +46 -0
- package/dist/kits.d.ts +86 -0
- package/dist/kits.js +215 -0
- package/dist/knowledge-cli.d.ts +101 -0
- package/dist/knowledge-cli.js +92 -0
- package/dist/knowledge-ui.d.ts +24 -0
- package/dist/knowledge-ui.js +40 -0
- package/dist/lead-context.d.ts +8 -0
- package/dist/lead-context.js +56 -0
- package/dist/lead-follow.d.ts +22 -0
- package/dist/lead-follow.js +167 -0
- package/dist/lead-status.d.ts +75 -0
- package/dist/lead-status.js +285 -0
- package/dist/ledger-chain.d.ts +46 -0
- package/dist/ledger-chain.js +187 -0
- package/dist/ledger-csv.d.ts +4 -0
- package/dist/ledger-csv.js +11 -0
- package/dist/ledger-view.d.ts +24 -0
- package/dist/ledger-view.js +58 -0
- package/dist/limits-ui.d.ts +26 -0
- package/dist/limits-ui.js +67 -0
- package/dist/link.d.ts +72 -0
- package/dist/link.js +217 -0
- package/dist/live.d.ts +96 -0
- package/dist/live.js +366 -0
- package/dist/liveness.d.ts +30 -0
- package/dist/liveness.js +42 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +25 -0
- package/dist/mailbox.d.ts +57 -0
- package/dist/mailbox.js +150 -0
- package/dist/maintenance.d.ts +11 -0
- package/dist/maintenance.js +35 -0
- package/dist/mate-cli.d.ts +52 -0
- package/dist/mate-cli.js +345 -0
- package/dist/mate-contract.d.ts +10 -0
- package/dist/mate-contract.js +30 -0
- package/dist/mate-doors.d.ts +69 -0
- package/dist/mate-doors.js +548 -0
- package/dist/mate-progress.d.ts +29 -0
- package/dist/mate-progress.js +121 -0
- package/dist/mate-tools.d.ts +94 -0
- package/dist/mate-tools.js +1878 -0
- package/dist/mate.d.ts +92 -0
- package/dist/mate.js +435 -0
- package/dist/mcp-connect.d.ts +103 -0
- package/dist/mcp-connect.js +252 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +651 -0
- package/dist/memory-cli.d.ts +466 -0
- package/dist/memory-cli.js +111 -0
- package/dist/memory-pass.d.ts +160 -0
- package/dist/memory-pass.js +409 -0
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +56 -0
- package/dist/mobile-viewport.d.ts +3 -0
- package/dist/mobile-viewport.js +37 -0
- package/dist/model-catalog.d.ts +118 -0
- package/dist/model-catalog.js +376 -0
- package/dist/models-cli.d.ts +102 -0
- package/dist/models-cli.js +66 -0
- package/dist/models-ui.d.ts +34 -0
- package/dist/models-ui.js +68 -0
- package/dist/modes.d.ts +80 -0
- package/dist/modes.js +182 -0
- package/dist/monitoring-settings.d.ts +44 -0
- package/dist/monitoring-settings.js +173 -0
- package/dist/monitoring-ui.d.ts +20 -0
- package/dist/monitoring-ui.js +51 -0
- package/dist/monitoring.d.ts +34 -0
- package/dist/monitoring.js +221 -0
- package/dist/names.d.ts +67 -0
- package/dist/names.js +102 -0
- package/dist/observations.d.ts +40 -0
- package/dist/observations.js +211 -0
- package/dist/oidc.d.ts +71 -0
- package/dist/oidc.js +182 -0
- package/dist/onboard.d.ts +108 -0
- package/dist/onboard.js +325 -0
- package/dist/openrouter-models.d.ts +30 -0
- package/dist/openrouter-models.js +120 -0
- package/dist/operate.d.ts +118 -0
- package/dist/operate.js +11321 -0
- package/dist/peek-cli.d.ts +97 -0
- package/dist/peek-cli.js +215 -0
- package/dist/peek.d.ts +134 -0
- package/dist/peek.js +578 -0
- package/dist/phase-routing.d.ts +307 -0
- package/dist/phase-routing.js +658 -0
- package/dist/plan-auto.d.ts +14 -0
- package/dist/plan-auto.js +92 -0
- package/dist/plan.d.ts +186 -0
- package/dist/plan.js +401 -0
- package/dist/planner-source.d.ts +251 -0
- package/dist/planner-source.js +460 -0
- package/dist/planner.d.ts +75 -0
- package/dist/planner.js +992 -0
- package/dist/policy-ui.d.ts +20 -0
- package/dist/policy-ui.js +41 -0
- package/dist/policy.d.ts +111 -0
- package/dist/policy.js +231 -0
- package/dist/prepared-evidence.d.ts +16 -0
- package/dist/prepared-evidence.js +96 -0
- package/dist/pricing.d.ts +45 -0
- package/dist/pricing.js +77 -0
- package/dist/principal.d.ts +42 -0
- package/dist/principal.js +82 -0
- package/dist/probe.d.ts +46 -0
- package/dist/probe.js +79 -0
- package/dist/process-custody.d.ts +17 -0
- package/dist/process-custody.js +91 -0
- package/dist/process-liveness.d.ts +10 -0
- package/dist/process-liveness.js +81 -0
- package/dist/process-recovery-anchor.d.ts +75 -0
- package/dist/process-recovery-anchor.js +188 -0
- package/dist/process-recovery-coalition.d.ts +25 -0
- package/dist/process-recovery-coalition.js +105 -0
- package/dist/process-recovery-eligibility.d.ts +178 -0
- package/dist/process-recovery-eligibility.js +170 -0
- package/dist/process-recovery-native.d.ts +172 -0
- package/dist/process-recovery-native.js +592 -0
- package/dist/process-recovery-provenance.d.ts +163 -0
- package/dist/process-recovery-provenance.js +292 -0
- package/dist/process-recovery-services.d.ts +56 -0
- package/dist/process-recovery-services.js +191 -0
- package/dist/process-recovery-settlement.d.ts +32 -0
- package/dist/process-recovery-settlement.js +132 -0
- package/dist/process-recovery.d.ts +41 -0
- package/dist/process-recovery.js +91 -0
- package/dist/process-tree.d.ts +41 -0
- package/dist/process-tree.js +264 -0
- package/dist/project-access.d.ts +14 -0
- package/dist/project-access.js +34 -0
- package/dist/project-cli.d.ts +18 -0
- package/dist/project-cli.js +104 -0
- package/dist/project-delete-ui.d.ts +22 -0
- package/dist/project-delete-ui.js +48 -0
- package/dist/project-delete.d.ts +55 -0
- package/dist/project-delete.js +413 -0
- package/dist/project-knowledge.d.ts +111 -0
- package/dist/project-knowledge.js +241 -0
- package/dist/project-learning.d.ts +100 -0
- package/dist/project-learning.js +438 -0
- package/dist/project-memory.d.ts +81 -0
- package/dist/project-memory.js +264 -0
- package/dist/project-skills.d.ts +152 -0
- package/dist/project-skills.js +660 -0
- package/dist/project-tools.d.ts +219 -0
- package/dist/project-tools.js +796 -0
- package/dist/project.d.ts +61 -0
- package/dist/project.js +125 -0
- package/dist/prompt.d.ts +22 -0
- package/dist/prompt.js +72 -0
- package/dist/proof.d.ts +513 -0
- package/dist/proof.js +1140 -0
- package/dist/proposal.d.ts +82 -0
- package/dist/proposal.js +210 -0
- package/dist/provider-connection.d.ts +22 -0
- package/dist/provider-connection.js +98 -0
- package/dist/provider-limits.d.ts +35 -0
- package/dist/provider-limits.js +95 -0
- package/dist/provider.d.ts +279 -0
- package/dist/provider.js +935 -0
- package/dist/publish.d.ts +107 -0
- package/dist/publish.js +650 -0
- package/dist/pulls.d.ts +118 -0
- package/dist/pulls.js +240 -0
- package/dist/push.d.ts +95 -0
- package/dist/push.js +353 -0
- package/dist/quality.d.ts +8 -0
- package/dist/quality.js +6 -0
- package/dist/recipe-ui.d.ts +18 -0
- package/dist/recipe-ui.js +152 -0
- package/dist/recipes.d.ts +81 -0
- package/dist/recipes.js +332 -0
- package/dist/remote.d.ts +67 -0
- package/dist/remote.js +122 -0
- package/dist/render.d.ts +64 -0
- package/dist/render.js +587 -0
- package/dist/report-summary.d.ts +13 -0
- package/dist/report-summary.js +15 -0
- package/dist/repos.d.ts +65 -0
- package/dist/repos.js +277 -0
- package/dist/repository-context-ui.d.ts +2 -0
- package/dist/repository-context-ui.js +10 -0
- package/dist/repository-context.d.ts +67 -0
- package/dist/repository-context.js +376 -0
- package/dist/restart-certification.d.ts +134 -0
- package/dist/restart-certification.js +226 -0
- package/dist/result-actions.d.ts +28 -0
- package/dist/result-actions.js +270 -0
- package/dist/result-completion.d.ts +18 -0
- package/dist/result-completion.js +66 -0
- package/dist/result-review.d.ts +213 -0
- package/dist/result-review.js +430 -0
- package/dist/retention-ui.d.ts +12 -0
- package/dist/retention-ui.js +39 -0
- package/dist/retention.d.ts +72 -0
- package/dist/retention.js +288 -0
- package/dist/review-context.d.ts +243 -0
- package/dist/review-context.js +1043 -0
- package/dist/review-evidence.d.ts +33 -0
- package/dist/review-evidence.js +56 -0
- package/dist/reviewer.d.ts +97 -0
- package/dist/reviewer.js +202 -0
- package/dist/routine.d.ts +228 -0
- package/dist/routine.js +764 -0
- package/dist/runner.d.ts +266 -0
- package/dist/runner.js +399 -0
- package/dist/scan.d.ts +32 -0
- package/dist/scan.js +92 -0
- package/dist/scope.d.ts +695 -0
- package/dist/scope.js +1176 -0
- package/dist/scout-report.d.ts +42 -0
- package/dist/scout-report.js +108 -0
- package/dist/scout.d.ts +69 -0
- package/dist/scout.js +351 -0
- package/dist/serve.d.ts +313 -0
- package/dist/serve.js +21776 -0
- package/dist/session-brief.d.ts +8 -0
- package/dist/session-brief.js +24 -0
- package/dist/session-cli.d.ts +13 -0
- package/dist/session-cli.js +284 -0
- package/dist/session-contract.d.ts +183 -0
- package/dist/session-contract.js +122 -0
- package/dist/session-http.d.ts +19 -0
- package/dist/session-http.js +154 -0
- package/dist/session-server.d.ts +12 -0
- package/dist/session-server.js +37 -0
- package/dist/session-service.d.ts +22 -0
- package/dist/session-service.js +157 -0
- package/dist/setup-guide.d.ts +39 -0
- package/dist/setup-guide.js +93 -0
- package/dist/sign-in-guard.d.ts +45 -0
- package/dist/sign-in-guard.js +74 -0
- package/dist/skills-ui.d.ts +11 -0
- package/dist/skills-ui.js +58 -0
- package/dist/skills.d.ts +97 -0
- package/dist/skills.js +276 -0
- package/dist/slack-api.d.ts +55 -0
- package/dist/slack-api.js +229 -0
- package/dist/slack-chat.d.ts +28 -0
- package/dist/slack-chat.js +413 -0
- package/dist/slack-settings.d.ts +7 -0
- package/dist/slack-settings.js +75 -0
- package/dist/slack-state.d.ts +13 -0
- package/dist/slack-state.js +9 -0
- package/dist/slack.d.ts +13 -0
- package/dist/slack.js +167 -0
- package/dist/spend-ui.d.ts +28 -0
- package/dist/spend-ui.js +95 -0
- package/dist/spend.d.ts +142 -0
- package/dist/spend.js +313 -0
- package/dist/sqlite-runtime.d.ts +3 -0
- package/dist/sqlite-runtime.js +6 -0
- package/dist/sso-settings.d.ts +33 -0
- package/dist/sso-settings.js +103 -0
- package/dist/sso-ui.d.ts +22 -0
- package/dist/sso-ui.js +39 -0
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +84 -0
- package/dist/store.d.ts +7754 -0
- package/dist/store.js +23279 -0
- package/dist/structured-output.d.ts +58 -0
- package/dist/structured-output.js +103 -0
- package/dist/style-asset.d.ts +7 -0
- package/dist/style-asset.js +41 -0
- package/dist/subscription-chat.d.ts +46 -0
- package/dist/subscription-chat.js +275 -0
- package/dist/summary.d.ts +33 -0
- package/dist/summary.js +71 -0
- package/dist/supervisor.mjs +297 -0
- package/dist/surface.d.ts +63 -0
- package/dist/surface.js +352 -0
- package/dist/sync.d.ts +94 -0
- package/dist/sync.js +279 -0
- package/dist/task-composer.d.ts +13 -0
- package/dist/task-composer.js +52 -0
- package/dist/task-control.d.ts +165 -0
- package/dist/task-control.js +214 -0
- package/dist/task-outcome-cli.d.ts +10 -0
- package/dist/task-outcome-cli.js +92 -0
- package/dist/task-text.d.ts +30 -0
- package/dist/task-text.js +41 -0
- package/dist/team-cli.d.ts +15 -0
- package/dist/team-cli.js +618 -0
- package/dist/team-contract.d.ts +92 -0
- package/dist/team-contract.js +1 -0
- package/dist/team-http.d.ts +17 -0
- package/dist/team-http.js +144 -0
- package/dist/team-leads.d.ts +81 -0
- package/dist/team-leads.js +565 -0
- package/dist/team-runtime.d.ts +27 -0
- package/dist/team-runtime.js +264 -0
- package/dist/team-ui.d.ts +3 -0
- package/dist/team-ui.js +9 -0
- package/dist/team-updates.d.ts +8 -0
- package/dist/team-updates.js +57 -0
- package/dist/teammate-admin.d.ts +53 -0
- package/dist/teammate-admin.js +111 -0
- package/dist/teammate-desk.d.ts +60 -0
- package/dist/teammate-desk.js +154 -0
- package/dist/teammate-memory.d.ts +64 -0
- package/dist/teammate-memory.js +196 -0
- package/dist/teammate-question.d.ts +59 -0
- package/dist/teammate-question.js +175 -0
- package/dist/teammate-tools.d.ts +108 -0
- package/dist/teammate-tools.js +271 -0
- package/dist/teammate-week.d.ts +68 -0
- package/dist/teammate-week.js +132 -0
- package/dist/teammate-work.d.ts +76 -0
- package/dist/teammate-work.js +280 -0
- package/dist/teammates-ui.d.ts +20 -0
- package/dist/teammates-ui.js +166 -0
- package/dist/teammates.d.ts +180 -0
- package/dist/teammates.js +253 -0
- package/dist/teams-api.d.ts +33 -0
- package/dist/teams-api.js +204 -0
- package/dist/teams-chat.d.ts +26 -0
- package/dist/teams-chat.js +215 -0
- package/dist/teams-settings.d.ts +12 -0
- package/dist/teams-settings.js +57 -0
- package/dist/teams.d.ts +21 -0
- package/dist/teams.js +124 -0
- package/dist/telegram-flow.d.ts +49 -0
- package/dist/telegram-flow.js +110 -0
- package/dist/telegram-mate.d.ts +137 -0
- package/dist/telegram-mate.js +707 -0
- package/dist/telegram-progress.d.ts +14 -0
- package/dist/telegram-progress.js +129 -0
- package/dist/telegram-settings.d.ts +9 -0
- package/dist/telegram-settings.js +36 -0
- package/dist/telegram-status.d.ts +75 -0
- package/dist/telegram-status.js +265 -0
- package/dist/telegram-team.d.ts +85 -0
- package/dist/telegram-team.js +359 -0
- package/dist/telegram.d.ts +230 -0
- package/dist/telegram.js +1931 -0
- package/dist/templates.d.ts +65 -0
- package/dist/templates.js +118 -0
- package/dist/tool-launcher.d.ts +1 -0
- package/dist/tool-launcher.js +101 -0
- package/dist/tools-ui.d.ts +30 -0
- package/dist/tools-ui.js +40 -0
- package/dist/transitions-recipes.d.ts +1 -0
- package/dist/transitions-recipes.js +203 -0
- package/dist/tree-proof.d.ts +34 -0
- package/dist/tree-proof.js +58 -0
- package/dist/verification-evidence.d.ts +44 -0
- package/dist/verification-evidence.js +238 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +10 -0
- package/dist/webhooks.d.ts +91 -0
- package/dist/webhooks.js +283 -0
- package/dist/work-index.d.ts +68 -0
- package/dist/work-index.js +384 -0
- package/dist/work-summary.d.ts +57 -0
- package/dist/work-summary.js +116 -0
- package/dist/workspace-motion.d.ts +4 -0
- package/dist/workspace-motion.js +133 -0
- package/dist/workspace-revision.d.ts +19 -0
- package/dist/workspace-revision.js +92 -0
- package/dist/workspace-ui.d.ts +204 -0
- package/dist/workspace-ui.js +411 -0
- package/dist/worktree-notices.d.ts +13 -0
- package/dist/worktree-notices.js +17 -0
- package/dist/worktree.d.ts +249 -0
- package/dist/worktree.js +720 -0
- package/package.json +122 -0
- package/scripts/canary-assertions.mjs +42 -0
- package/scripts/crash-canary.mjs +253 -0
- package/scripts/fixtures/crash-process.mjs +74 -0
- package/scripts/fixtures/pilot-scenarios.mjs +85 -0
- package/scripts/fixtures/restart-service.mjs +27 -0
- package/scripts/launchd-certification.mjs +143 -0
- package/scripts/pilot.mjs +121 -0
- package/scripts/proof-preflight.mjs +140 -0
- package/scripts/provider-canary.mjs +379 -0
- package/scripts/recovery-canary.mjs +135 -0
- package/scripts/restart-certification.mjs +98 -0
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
// Assignment direct assessments reuse custody checks; gaps remain explicit.
|
|
2
|
+
import { readObservationEvidence, OBSERVATION_CAPTURE, OBSERVATION_FILE } from "./observations.js";
|
|
3
|
+
import { isVerificationReceipt } from "./verification-evidence.js";
|
|
4
|
+
/**
|
|
5
|
+
* Inherited review context (v51, contract handoff task 3): the bounded,
|
|
6
|
+
* sealed source-and-ancestry inventory a REVISION's reviewer is shown
|
|
7
|
+
* beside the revision's own patch.
|
|
8
|
+
*
|
|
9
|
+
* A small revision to a larger feature carries the whole feature's signed
|
|
10
|
+
* rubric, but its terminal diff shows only what the revision touched — so
|
|
11
|
+
* an honest reviewer could only say `cannot-tell` about every criterion
|
|
12
|
+
* the earlier build implemented (run 1518 said exactly that three times,
|
|
13
|
+
* although run 1514 had upheld those same criteria on the base). This
|
|
14
|
+
* module closes that gap WITHOUT letting a reviewer read a checkout:
|
|
15
|
+
*
|
|
16
|
+
* - Complete items are files read from the git object database BY EXACT
|
|
17
|
+
* COMMIT (the revision's sealed head) and literal path — `ls-tree` for
|
|
18
|
+
* identity and size, `cat-file blob` for bytes — with lazy fetch,
|
|
19
|
+
* replace refs, filters, textconv, and diff drivers all out of the
|
|
20
|
+
* picture. The working tree is never read: a checkout is mutable, a
|
|
21
|
+
* blob at a commit is not.
|
|
22
|
+
* - Which paths are relevant comes from VERIFIED ancestor artifacts: the
|
|
23
|
+
* source run's sealed proof (its changed paths and the changed-path
|
|
24
|
+
* evidence it cited per criterion) and its sealed diff-stat. Every item
|
|
25
|
+
* binds to the source run, the commit, the path, the blob id, and a
|
|
26
|
+
* SHA-256 of the stored bytes.
|
|
27
|
+
* - Storage ceilings are independent of prompt delivery. A pathological
|
|
28
|
+
* oversized file remains a NAMED
|
|
29
|
+
* GAP. Schema 2 may also supply bounded partial ancestor patch sections
|
|
30
|
+
* with exact artifact ranges and all intervening sealed changes. Tree
|
|
31
|
+
* identity is proved independently of full-file capture. Binary,
|
|
32
|
+
* non-UTF-8, missing, redacted and over-budget context stays a gap.
|
|
33
|
+
* - An earlier reviewer's judgement on the source run is CONTEXT ONLY.
|
|
34
|
+
* It is exposed as `priorReview` with its own eligibility proof — same
|
|
35
|
+
* criterion text, verified ancestry, relevant code byte-identical
|
|
36
|
+
* between the source head and this head, the source review's own
|
|
37
|
+
* bindings intact — and never as an automatic judgement for this run.
|
|
38
|
+
*
|
|
39
|
+
* `coverage` is the per-criterion projection every surface reads: `patch`
|
|
40
|
+
* (judged from this revision's own diff), `context` (inherited, partly or wholly outside
|
|
41
|
+
* the patch, every relevant path sealed here), or `gap` (inherited but
|
|
42
|
+
* some relevant context could not be supplied — the reasons are listed).
|
|
43
|
+
*/
|
|
44
|
+
import { createHash } from "node:crypto";
|
|
45
|
+
import { hasForbiddenControls } from "./decision.js";
|
|
46
|
+
import { EVIDENCE_CAPS, readVerifiedArtifact, redactSecretLines, scanForSecrets, storeEvidence } from "./evidence.js";
|
|
47
|
+
import { parseProof } from "./proof.js";
|
|
48
|
+
/** Explicit bounds. Content bytes are UTF-8 of the stored (possibly
|
|
49
|
+
* redacted) text; the aggregate is the sum over every stored item. */
|
|
50
|
+
export const LEGACY_REVIEW_CONTEXT_LIMITS = {
|
|
51
|
+
/** One complete file or partial patch chain. Larger files stay gaps. */
|
|
52
|
+
itemBytes: 48 * 1024,
|
|
53
|
+
/** Every item's bytes together. Whatever would cross it is a named gap. */
|
|
54
|
+
aggregateBytes: 192 * 1024,
|
|
55
|
+
/** Items stored, in deterministic relevance order. */
|
|
56
|
+
items: 24,
|
|
57
|
+
/** Candidate paths considered from the source proof and diff-stat. */
|
|
58
|
+
candidatePaths: 200,
|
|
59
|
+
/** Prior review rows carried as context. */
|
|
60
|
+
priorReviews: 48,
|
|
61
|
+
ancestors: 16,
|
|
62
|
+
};
|
|
63
|
+
/** Storage ceilings, independent of delivery. Ordinary large files remain whole.
|
|
64
|
+
* The encoded artifact allows worst-case JSON escaping plus metadata. */
|
|
65
|
+
export const REVIEW_CONTEXT_LIMITS = {
|
|
66
|
+
...LEGACY_REVIEW_CONTEXT_LIMITS,
|
|
67
|
+
itemBytes: 8 * 1024 * 1024,
|
|
68
|
+
aggregateBytes: 32 * 1024 * 1024,
|
|
69
|
+
items: 200,
|
|
70
|
+
};
|
|
71
|
+
/** Only this compact projection enters prompts. Full bytes remain sealed in
|
|
72
|
+
* the artifact and are materialized under generated, flat scratch names. */
|
|
73
|
+
export function reviewContextFileName(id) {
|
|
74
|
+
if (!ITEM_ID.test(id))
|
|
75
|
+
throw new Error("invalid context item id");
|
|
76
|
+
return `REVIEW-CONTEXT-${id}.txt`;
|
|
77
|
+
}
|
|
78
|
+
export function reviewContextManifest(inventory) {
|
|
79
|
+
return JSON.stringify({
|
|
80
|
+
...inventory,
|
|
81
|
+
...(inventory.handoff === undefined ? {} : { handoff: { artifact: inventory.handoff.artifact, sha256: inventory.handoff.sha256, file: "REVIEW-BUILDER-NOTES.json" } }),
|
|
82
|
+
...(inventory.observations === undefined ? {} : { observations: { artifact: inventory.observations.artifact, sha256: inventory.observations.sha256, file: OBSERVATION_FILE } }),
|
|
83
|
+
format: "review-context-manifest-v1",
|
|
84
|
+
items: inventory.items.map(({ content: _content, ...item }) => ({
|
|
85
|
+
...item, file: reviewContextFileName(item.id),
|
|
86
|
+
})),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const CONTEXT_INPUT_KINDS = new Set(["terminal-diff", "diff-stat", "proof", "check-log", "screenshot", "review-context"]);
|
|
90
|
+
/** A shortened check log is the exact bounded log the reviewer saw.
|
|
91
|
+
* Completeness of patches, proofs and other structured inputs is still
|
|
92
|
+
* required. Failed capture is never evidence, even if its bytes hash. */
|
|
93
|
+
function usableReviewInput(root, artifact) {
|
|
94
|
+
return artifact.captureStatus !== "failed" && (!artifact.truncated || artifact.kind === "check-log") && readVerifiedArtifact(root, artifact).ok;
|
|
95
|
+
}
|
|
96
|
+
/** A terminal reviewer can have one correction child. Follow its recorded
|
|
97
|
+
* ancestry, never another task's judgement, with a fixed traversal bound. */
|
|
98
|
+
function historicalReviewerLineage(store, reviewerId) {
|
|
99
|
+
const rows = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
let id = reviewerId;
|
|
102
|
+
for (let depth = 0; id !== null && depth < 4 && !seen.has(id); depth++) {
|
|
103
|
+
seen.add(id);
|
|
104
|
+
const run = store.getRun(id);
|
|
105
|
+
if (run === null)
|
|
106
|
+
break;
|
|
107
|
+
rows.push({ id: run.id, taskRef: run.taskRef, role: run.role, parentRun: run.parentRun, outcome: run.outcome });
|
|
108
|
+
if (run.role !== "reviewer")
|
|
109
|
+
break;
|
|
110
|
+
id = run.parentRun;
|
|
111
|
+
}
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
/** Fingerprint every input the ancestor review depended on, including
|
|
115
|
+
* absent inputs and later additions. No transcript or checkout is copied. */
|
|
116
|
+
export function reviewContextBindingOf(store, runId, candidate = false) {
|
|
117
|
+
const run = store.getRun(runId);
|
|
118
|
+
const taskId = run === null ? null : store.externalIdFor(run.taskRef);
|
|
119
|
+
const scope = taskId === null ? null : store.getScope(taskId);
|
|
120
|
+
const ref = taskId === null ? null : store.lookupRef(taskId);
|
|
121
|
+
const state = {
|
|
122
|
+
task: taskId, head: run?.headRevision ?? null, base: run?.baseRevision ?? null, scopeDigest: run?.scopeDigest ?? null,
|
|
123
|
+
currentScope: scope?.digest ?? null, termsProblem: scope?.termsProblem ?? null,
|
|
124
|
+
revisionOf: ref?.revisionOf ?? null, revisionBrief: ref?.revisionBriefArtifact ?? null,
|
|
125
|
+
artifacts: store.artifactsFor(runId).filter(one => (CONTEXT_INPUT_KINDS.has(one.kind) && (!candidate || one.kind !== "review-context")) || isVerificationReceipt(one) || one.capture === OBSERVATION_CAPTURE),
|
|
126
|
+
reviews: candidate ? [] : store.criterionReviewsFor(runId),
|
|
127
|
+
reviewerLineages: candidate ? [] : [...new Set(store.criterionReviewsFor(runId).map(one => one.reviewerRun))].map(id => historicalReviewerLineage(store, id)),
|
|
128
|
+
};
|
|
129
|
+
return { run: runId, sha256: createHash("sha256").update(JSON.stringify(state)).digest("hex"), ...(candidate ? { candidate: true } : {}) };
|
|
130
|
+
}
|
|
131
|
+
/** Re-prove sealed context before spending and inside final ingestion. */
|
|
132
|
+
export function reviewContextCustodyProblem(store, root, inventory) {
|
|
133
|
+
try {
|
|
134
|
+
const current = readObservationEvidence(store, root, inventory.run);
|
|
135
|
+
if (JSON.stringify(current) !== JSON.stringify(inventory.observations ?? null))
|
|
136
|
+
return "the focused observation inventory changed";
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return "the focused observations no longer verify";
|
|
140
|
+
}
|
|
141
|
+
if (inventory.handoff !== undefined) {
|
|
142
|
+
const found = store.artifactsFor(inventory.run).filter(one => one.kind === "handoff");
|
|
143
|
+
const artifact = found[0];
|
|
144
|
+
if (found.length !== 1 || artifact?.id !== inventory.handoff.artifact || artifact.sha256 !== inventory.handoff.sha256)
|
|
145
|
+
return "the builder notes binding changed";
|
|
146
|
+
const read = readVerifiedArtifact(root, artifact);
|
|
147
|
+
if (!read.ok || read.content.toString("utf8") !== inventory.handoff.content)
|
|
148
|
+
return "the captured builder notes no longer verify";
|
|
149
|
+
}
|
|
150
|
+
const run = store.getRun(inventory.run);
|
|
151
|
+
const source = run === null ? null : store.revisionSourceOf(run.taskRef) ?? { sourceRun: run.id, sourceTask: store.externalIdFor(run.taskRef) };
|
|
152
|
+
if (run === null || run.headRevision !== inventory.head || run.baseRevision !== inventory.base || source?.sourceRun !== inventory.source.run || source.sourceTask !== inventory.source.task)
|
|
153
|
+
return "the review context's run, head, base or revision ancestry changed";
|
|
154
|
+
if (!inventory.bindings?.length || inventory.bindings.length > REVIEW_CONTEXT_LIMITS.ancestors || !inventory.bindings.some(one => one.run === inventory.source.run))
|
|
155
|
+
return "the review context has no complete bounded source binding";
|
|
156
|
+
for (const binding of inventory.bindings) {
|
|
157
|
+
if (reviewContextBindingOf(store, binding.run, binding.candidate === true).sha256 !== binding.sha256)
|
|
158
|
+
return `ancestor run #${binding.run}'s scope, ancestry, evidence inventory or review context changed`;
|
|
159
|
+
for (const artifact of store.artifactsFor(binding.run).filter(one => CONTEXT_INPUT_KINDS.has(one.kind) || isVerificationReceipt(one))) {
|
|
160
|
+
try {
|
|
161
|
+
if (!readVerifiedArtifact(root, artifact).ok)
|
|
162
|
+
return `ancestor run #${binding.run}'s ${artifact.kind} bytes no longer verify`;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return `ancestor run #${binding.run}'s ${artifact.kind} cannot be read`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const item of inventory.items) {
|
|
170
|
+
let offset = 0;
|
|
171
|
+
for (const segment of item.patch?.segments ?? []) {
|
|
172
|
+
const source = store.getRun(segment.run);
|
|
173
|
+
const artifact = store.getArtifact(segment.artifact);
|
|
174
|
+
if (source?.baseRevision !== segment.base || source.headRevision !== segment.head || artifact?.run !== segment.run || artifact.kind !== "terminal-diff" || artifact.sha256 !== segment.sha256)
|
|
175
|
+
return "an ancestor patch's source binding changed";
|
|
176
|
+
try {
|
|
177
|
+
const read = readVerifiedArtifact(root, artifact);
|
|
178
|
+
const content = Buffer.from(item.content, "utf8");
|
|
179
|
+
// Redaction elsewhere does not invalidate this exact clean section.
|
|
180
|
+
// Reselect by path: matching arbitrary bytes alone could admit a
|
|
181
|
+
// fragment that hides a redaction, or another file's section.
|
|
182
|
+
const section = read.ok ? patchSection(read.content, item.path) : null;
|
|
183
|
+
if (!usableReviewInput(root, artifact) || section === null || section.offset !== segment.offset || section.content.length !== segment.bytes || !section.content.equals(content.subarray(offset, offset + segment.bytes)))
|
|
184
|
+
return "an ancestor patch's exact source bytes no longer verify";
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return "an ancestor patch cannot be read";
|
|
188
|
+
}
|
|
189
|
+
offset += segment.bytes;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const SHA1 = /^[0-9a-f]{40}$/;
|
|
195
|
+
const ITEM_ID = /^ctx-[1-9][0-9]{0,3}$/;
|
|
196
|
+
/** A candidate path the object reader will accept on argv: repository-
|
|
197
|
+
* relative, no `..` or `.` segments, no leading dash, no control bytes. */
|
|
198
|
+
export function safeContextPath(path) {
|
|
199
|
+
if (path.length === 0 || path.length > 300 || path.startsWith("-") || path.startsWith("/") || path.includes("\\"))
|
|
200
|
+
return false;
|
|
201
|
+
// eslint-disable-next-line no-control-regex
|
|
202
|
+
if (/[\u0000-\u001f\u007f]/.test(path))
|
|
203
|
+
return false;
|
|
204
|
+
return path.split("/").every(segment => segment.length > 0 && segment !== "." && segment !== "..");
|
|
205
|
+
}
|
|
206
|
+
export function serializeReviewContext(inventory) {
|
|
207
|
+
return JSON.stringify(inventory, null, 1);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The strict reader every consumer uses (the reviewer pass, the surfaces,
|
|
211
|
+
* the tests): shape, bounds, unique ids, and every item's SHA-256
|
|
212
|
+
* recomputed from its content. What this refuses, nothing shows.
|
|
213
|
+
*/
|
|
214
|
+
export function parseReviewContext(raw) {
|
|
215
|
+
let parsed;
|
|
216
|
+
try {
|
|
217
|
+
parsed = JSON.parse(raw);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return { ok: false, problem: "not JSON" };
|
|
221
|
+
}
|
|
222
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
223
|
+
return { ok: false, problem: "not an object" };
|
|
224
|
+
const r = parsed;
|
|
225
|
+
const str = (value, max = 2000) => typeof value === "string" && value.length <= max && !hasForbiddenControls(value);
|
|
226
|
+
const strList = (value, max = 64) => Array.isArray(value) && value.length <= max && value.every(one => str(one, 300));
|
|
227
|
+
const optionalStr = (value, max = 2000) => value === null || str(value, max);
|
|
228
|
+
if (r["schema"] !== 1 && r["schema"] !== 2 && r["schema"] !== 3)
|
|
229
|
+
return { ok: false, problem: "unsupported review context schema" };
|
|
230
|
+
const contentLimits = r["schema"] === 3 ? REVIEW_CONTEXT_LIMITS : LEGACY_REVIEW_CONTEXT_LIMITS;
|
|
231
|
+
if (r["schema"] === 1 && r["identities"] !== undefined)
|
|
232
|
+
return { ok: false, problem: "identities require schema 2" };
|
|
233
|
+
if (!Number.isSafeInteger(r["run"]) || Number(r["run"]) <= 0)
|
|
234
|
+
return { ok: false, problem: "run must be a positive integer" };
|
|
235
|
+
if (!str(r["head"], 40) || !SHA1.test(r["head"]))
|
|
236
|
+
return { ok: false, problem: "head must be a 40-hex commit" };
|
|
237
|
+
if (!optionalStr(r["base"], 40))
|
|
238
|
+
return { ok: false, problem: "base must be a string or null" };
|
|
239
|
+
const bindings = r["bindings"];
|
|
240
|
+
const handoff = r["handoff"];
|
|
241
|
+
const observations = r["observations"];
|
|
242
|
+
if (observations !== undefined && (!observations || !Number.isSafeInteger(observations.artifact) || observations.artifact <= 0 ||
|
|
243
|
+
typeof observations.content !== "string" || Buffer.byteLength(observations.content) > 64 * 1024 ||
|
|
244
|
+
typeof observations.sha256 !== "string" || createHash("sha256").update(observations.content).digest("hex") !== observations.sha256))
|
|
245
|
+
return { ok: false, problem: "focused observations are malformed or do not match their hash" };
|
|
246
|
+
if (handoff !== undefined && (handoff === null || !Number.isSafeInteger(handoff.artifact) || handoff.artifact <= 0 ||
|
|
247
|
+
typeof handoff.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(handoff.sha256) || typeof handoff.content !== "string" ||
|
|
248
|
+
Buffer.byteLength(handoff.content) > 64 * 1024 || createHash("sha256").update(handoff.content).digest("hex") !== handoff.sha256))
|
|
249
|
+
return { ok: false, problem: "builder notes are malformed or do not match their hash" };
|
|
250
|
+
if (bindings !== undefined && (!Array.isArray(bindings) || bindings.length > REVIEW_CONTEXT_LIMITS.ancestors || !bindings.every(one => one !== null && typeof one === "object" && Number.isSafeInteger(one.run) && one.run > 0 && typeof one.sha256 === "string" && /^[0-9a-f]{64}$/.test(one.sha256) && (one.candidate === undefined || one.candidate === true)) || new Set(bindings.map(one => one.run)).size !== bindings.length))
|
|
251
|
+
return { ok: false, problem: "source bindings are malformed" };
|
|
252
|
+
const source = r["source"];
|
|
253
|
+
if (source === null || typeof source !== "object" || Array.isArray(source))
|
|
254
|
+
return { ok: false, problem: "source must be an object" };
|
|
255
|
+
const so = source;
|
|
256
|
+
if (!str(so["task"], 200) || !Number.isSafeInteger(so["run"]) || !optionalStr(so["head"], 40) || !optionalStr(so["scopeDigest"], 200) || typeof so["verified"] !== "boolean" || !strList(so["problems"], 16)) {
|
|
257
|
+
return { ok: false, problem: "source is malformed" };
|
|
258
|
+
}
|
|
259
|
+
const ancestry = r["ancestry"];
|
|
260
|
+
if (ancestry === null || typeof ancestry !== "object" || Array.isArray(ancestry))
|
|
261
|
+
return { ok: false, problem: "ancestry must be an object" };
|
|
262
|
+
const an = ancestry;
|
|
263
|
+
if (typeof an["verified"] !== "boolean" || !str(an["detail"], 500))
|
|
264
|
+
return { ok: false, problem: "ancestry is malformed" };
|
|
265
|
+
const limits = r["limits"];
|
|
266
|
+
if (limits === null || typeof limits !== "object" || Array.isArray(limits))
|
|
267
|
+
return { ok: false, problem: "limits must be an object" };
|
|
268
|
+
const li = limits;
|
|
269
|
+
if (![li["itemBytes"], li["aggregateBytes"], li["items"]].every(one => Number.isSafeInteger(one) && Number(one) > 0))
|
|
270
|
+
return { ok: false, problem: "limits are malformed" };
|
|
271
|
+
const identities = [];
|
|
272
|
+
if (r["schema"] !== 1) {
|
|
273
|
+
if (!Array.isArray(r["identities"]) || r["identities"].length > REVIEW_CONTEXT_LIMITS.candidatePaths)
|
|
274
|
+
return { ok: false, problem: "identities must be a bounded array" };
|
|
275
|
+
for (const one of r["identities"]) {
|
|
276
|
+
if (one === null || typeof one !== "object" || Array.isArray(one))
|
|
277
|
+
return { ok: false, problem: "identity must be an object" };
|
|
278
|
+
const it = one;
|
|
279
|
+
if (!str(it.path, 300) || !safeContextPath(it.path) || identities.some(other => other.path === it.path) || it.commit !== r["head"] || !str(it.blob, 40) || !SHA1.test(it.blob) || !["100644", "100755"].includes(it.mode) || !Number.isSafeInteger(it.bytes) || it.bytes < 0 || !Number.isSafeInteger(it.sourceRun) || it.sourceRun !== so["run"] || (it.sourceBlob !== null && (!str(it.sourceBlob, 40) || !SHA1.test(it.sourceBlob))) || (it.sourceMode !== null && !["100644", "100755"].includes(it.sourceMode)) || (it.unchangedSinceSource !== null && typeof it.unchangedSinceSource !== "boolean"))
|
|
280
|
+
return { ok: false, problem: "identity is malformed" };
|
|
281
|
+
if ((it.sourceBlob === null) !== (it.sourceMode === null) || (it.unchangedSinceSource !== null && (!an["verified"] || it.unchangedSinceSource !== (it.blob === it.sourceBlob && it.mode === it.sourceMode))))
|
|
282
|
+
return { ok: false, problem: "identity claims unproved equality" };
|
|
283
|
+
identities.push({ path: it.path, commit: it.commit, blob: it.blob, mode: it.mode, bytes: it.bytes, sourceRun: it.sourceRun, sourceBlob: it.sourceBlob, sourceMode: it.sourceMode, unchangedSinceSource: it.unchangedSinceSource });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const items = [];
|
|
287
|
+
if (!Array.isArray(r["items"]) || r["items"].length > contentLimits.items)
|
|
288
|
+
return { ok: false, problem: `items must be an array of at most ${contentLimits.items}` };
|
|
289
|
+
const ids = new Set();
|
|
290
|
+
const paths = new Set();
|
|
291
|
+
let aggregate = 0;
|
|
292
|
+
for (const [index, one] of r["items"].entries()) {
|
|
293
|
+
if (one === null || typeof one !== "object" || Array.isArray(one))
|
|
294
|
+
return { ok: false, problem: `item ${index}: not an object` };
|
|
295
|
+
const it = one;
|
|
296
|
+
if (!str(it["id"], 12) || !ITEM_ID.test(it["id"]) || ids.has(it["id"]))
|
|
297
|
+
return { ok: false, problem: `item ${index}: id must be a unique ctx-<n> token` };
|
|
298
|
+
if (!str(it["path"], 300) || !safeContextPath(it["path"]) || paths.has(it["path"]))
|
|
299
|
+
return { ok: false, problem: `item ${index}: path is not a safe repository path` };
|
|
300
|
+
if (!str(it["commit"], 40) || !SHA1.test(it["commit"]) || it["commit"] !== r["head"])
|
|
301
|
+
return { ok: false, problem: `item ${index}: commit must be a 40-hex commit` };
|
|
302
|
+
if (!str(it["blob"], 40) || !SHA1.test(it["blob"]))
|
|
303
|
+
return { ok: false, problem: `item ${index}: blob must be a 40-hex object id` };
|
|
304
|
+
if (typeof it["content"] !== "string" || it["content"].includes("\u0000"))
|
|
305
|
+
return { ok: false, problem: `item ${index}: content must be a NUL-free string` };
|
|
306
|
+
const bytes = Buffer.byteLength(it["content"], "utf8");
|
|
307
|
+
if (bytes > contentLimits.itemBytes || it["bytes"] !== bytes)
|
|
308
|
+
return { ok: false, problem: `item ${index}: bytes must equal the content's UTF-8 length and fit the item limit` };
|
|
309
|
+
aggregate += bytes;
|
|
310
|
+
if (aggregate > contentLimits.aggregateBytes)
|
|
311
|
+
return { ok: false, problem: "items exceed the aggregate content limit" };
|
|
312
|
+
const sha256 = createHash("sha256").update(it["content"], "utf8").digest("hex");
|
|
313
|
+
if (it["sha256"] !== sha256)
|
|
314
|
+
return { ok: false, problem: `item ${index}: sha256 does not match its content` };
|
|
315
|
+
if (!Number.isSafeInteger(it["sourceRun"]) || Number(it["sourceRun"]) <= 0)
|
|
316
|
+
return { ok: false, problem: `item ${index}: sourceRun must be a positive integer` };
|
|
317
|
+
if (!strList(it["criteria"], 12))
|
|
318
|
+
return { ok: false, problem: `item ${index}: criteria must be a list of ids` };
|
|
319
|
+
if (!Array.isArray(it["why"]) || it["why"].length === 0 || it["why"].length > 2 || !it["why"].every(w => w === "source-criterion-evidence" || w === "source-changed-path" || w === "candidate-changed-path")) {
|
|
320
|
+
return { ok: false, problem: `item ${index}: why must name how the path became relevant` };
|
|
321
|
+
}
|
|
322
|
+
if (it["unchangedSinceSource"] !== null && typeof it["unchangedSinceSource"] !== "boolean")
|
|
323
|
+
return { ok: false, problem: `item ${index}: unchangedSinceSource must be boolean or null` };
|
|
324
|
+
if (typeof it["redacted"] !== "boolean")
|
|
325
|
+
return { ok: false, problem: `item ${index}: redacted must be boolean` };
|
|
326
|
+
let patch;
|
|
327
|
+
if (it["patch"] !== undefined) {
|
|
328
|
+
const p = it["patch"];
|
|
329
|
+
if (r["schema"] === 1 || p === null || typeof p !== "object" || p.coverage !== "partial" || !Array.isArray(p.segments) || p.segments.length === 0 || p.segments.length > REVIEW_CONTEXT_LIMITS.ancestors + 1 || it["redacted"] !== false)
|
|
330
|
+
return { ok: false, problem: `item ${index}: unsupported partial patch` };
|
|
331
|
+
let total = 0;
|
|
332
|
+
const runs = new Set();
|
|
333
|
+
const lineage = [...(bindings ?? []).map(one => one.run), Number(r["run"])];
|
|
334
|
+
let lastPosition = -1;
|
|
335
|
+
for (const s of p.segments) {
|
|
336
|
+
if (s === null || typeof s !== "object" || !Number.isSafeInteger(s.run) || s.run <= 0 || runs.has(s.run) || (s.run !== r["run"] && !bindings?.some(one => one.run === s.run)) || !str(s.base, 40) || !SHA1.test(s.base) || !str(s.head, 40) || !SHA1.test(s.head) || !Number.isSafeInteger(s.artifact) || s.artifact <= 0 || !str(s.sha256, 64) || !/^[0-9a-f]{64}$/.test(s.sha256) || !Number.isSafeInteger(s.offset) || s.offset < 0 || !Number.isSafeInteger(s.bytes) || s.bytes <= 0 || s.offset + s.bytes > EVIDENCE_CAPS["terminal-diff"])
|
|
337
|
+
return { ok: false, problem: `item ${index}: malformed patch segment` };
|
|
338
|
+
total += s.bytes;
|
|
339
|
+
const position = lineage.indexOf(s.run);
|
|
340
|
+
if (position <= lastPosition)
|
|
341
|
+
return { ok: false, problem: `item ${index}: patch segments are out of ancestry order` };
|
|
342
|
+
lastPosition = position;
|
|
343
|
+
runs.add(s.run);
|
|
344
|
+
}
|
|
345
|
+
if (total !== bytes)
|
|
346
|
+
return { ok: false, problem: `item ${index}: patch ranges do not cover its content` };
|
|
347
|
+
patch = { coverage: "partial", segments: p.segments.map(s => ({ run: s.run, base: s.base, head: s.head, artifact: s.artifact, sha256: s.sha256, offset: s.offset, bytes: s.bytes })) };
|
|
348
|
+
}
|
|
349
|
+
else if (it["redacted"] === false && createHash("sha1").update(`blob ${bytes}\0`).update(it["content"], "utf8").digest("hex") !== it["blob"])
|
|
350
|
+
return { ok: false, problem: `item ${index}: git blob identity does not match its content` };
|
|
351
|
+
if (r["schema"] !== 1 && !identities.some(one => one.path === it["path"] && one.blob === it["blob"] && one.sourceRun === it["sourceRun"] && one.unchangedSinceSource === it["unchangedSinceSource"]))
|
|
352
|
+
return { ok: false, problem: `item ${index}: no matching tree identity` };
|
|
353
|
+
paths.add(it["path"]);
|
|
354
|
+
ids.add(it["id"]);
|
|
355
|
+
items.push({
|
|
356
|
+
id: it["id"],
|
|
357
|
+
path: it["path"],
|
|
358
|
+
commit: it["commit"],
|
|
359
|
+
blob: it["blob"],
|
|
360
|
+
sha256,
|
|
361
|
+
bytes,
|
|
362
|
+
content: it["content"],
|
|
363
|
+
sourceRun: Number(it["sourceRun"]),
|
|
364
|
+
criteria: [...it["criteria"]],
|
|
365
|
+
why: [...it["why"]],
|
|
366
|
+
unchangedSinceSource: it["unchangedSinceSource"],
|
|
367
|
+
redacted: it["redacted"],
|
|
368
|
+
...(patch === undefined ? {} : { patch }),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const gaps = [];
|
|
372
|
+
if (!Array.isArray(r["gaps"]) || r["gaps"].length > 400)
|
|
373
|
+
return { ok: false, problem: "gaps must be a bounded array" };
|
|
374
|
+
const GAP_REASONS = new Set([
|
|
375
|
+
"source-missing", "source-unverified", "source-proof-missing", "source-scope-changed", "criterion-changed", "stale-ancestry",
|
|
376
|
+
"missing-at-head", "not-a-file", "binary", "over-limit", "aggregate-limit", "item-cap", "secret-redacted", "capture-failed",
|
|
377
|
+
]);
|
|
378
|
+
for (const [index, one] of r["gaps"].entries()) {
|
|
379
|
+
if (one === null || typeof one !== "object" || Array.isArray(one))
|
|
380
|
+
return { ok: false, problem: `gap ${index}: not an object` };
|
|
381
|
+
const g = one;
|
|
382
|
+
if (!str(g["reason"], 40) || !GAP_REASONS.has(g["reason"]) || !optionalStr(g["path"], 300) || !strList(g["criteria"], 12) || !str(g["detail"], 500)) {
|
|
383
|
+
return { ok: false, problem: `gap ${index}: malformed` };
|
|
384
|
+
}
|
|
385
|
+
gaps.push({ reason: g["reason"], path: g["path"], criteria: [...g["criteria"]], detail: g["detail"] });
|
|
386
|
+
}
|
|
387
|
+
const priorReview = [];
|
|
388
|
+
if (!Array.isArray(r["priorReview"]) || r["priorReview"].length > REVIEW_CONTEXT_LIMITS.priorReviews)
|
|
389
|
+
return { ok: false, problem: "priorReview must be a bounded array" };
|
|
390
|
+
for (const [index, one] of r["priorReview"].entries()) {
|
|
391
|
+
if (one === null || typeof one !== "object" || Array.isArray(one))
|
|
392
|
+
return { ok: false, problem: `priorReview ${index}: not an object` };
|
|
393
|
+
const p = one;
|
|
394
|
+
if (!str(p["criterionId"], 64) || !Number.isSafeInteger(p["sourceRun"]) || !Number.isSafeInteger(p["reviewerRun"]) || !str(p["author"], 200) ||
|
|
395
|
+
(p["judgement"] !== "upholds" && p["judgement"] !== "contradicts" && p["judgement"] !== "cannot-tell") || !str(p["note"], 500) ||
|
|
396
|
+
typeof p["statementMatches"] !== "boolean" || typeof p["ancestryVerified"] !== "boolean" || !strList(p["relevantPaths"], 200) ||
|
|
397
|
+
typeof p["relevantUnchanged"] !== "boolean" || typeof p["bindingsVerified"] !== "boolean" ||
|
|
398
|
+
(p["support"] !== "eligible" && p["support"] !== "invalid") || !strList(p["reasons"], 16)) {
|
|
399
|
+
return { ok: false, problem: `priorReview ${index}: malformed` };
|
|
400
|
+
}
|
|
401
|
+
if (p["support"] === "eligible" && !(p["statementMatches"] && p["ancestryVerified"] && p["relevantUnchanged"] && p["bindingsVerified"])) {
|
|
402
|
+
return { ok: false, problem: `priorReview ${index}: claims eligibility without every proof` };
|
|
403
|
+
}
|
|
404
|
+
if (r["schema"] !== 1 && p["relevantUnchanged"] && (p["relevantPaths"].length === 0 || !p["relevantPaths"].every(path => identities.some(one => one.path === path && one.unchangedSinceSource === true))))
|
|
405
|
+
return { ok: false, problem: `priorReview ${index}: unchanged paths lack tree identity` };
|
|
406
|
+
priorReview.push({
|
|
407
|
+
criterionId: p["criterionId"], sourceRun: Number(p["sourceRun"]), reviewerRun: Number(p["reviewerRun"]), author: p["author"],
|
|
408
|
+
judgement: p["judgement"], note: p["note"], statementMatches: p["statementMatches"], ancestryVerified: p["ancestryVerified"],
|
|
409
|
+
relevantPaths: [...p["relevantPaths"]], relevantUnchanged: p["relevantUnchanged"], bindingsVerified: p["bindingsVerified"],
|
|
410
|
+
support: p["support"], reasons: [...p["reasons"]],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const coverage = [];
|
|
414
|
+
if (!Array.isArray(r["coverage"]) || r["coverage"].length > 12)
|
|
415
|
+
return { ok: false, problem: "coverage must be a bounded array" };
|
|
416
|
+
const coverageIds = new Set();
|
|
417
|
+
for (const [index, one] of r["coverage"].entries()) {
|
|
418
|
+
if (one === null || typeof one !== "object" || Array.isArray(one))
|
|
419
|
+
return { ok: false, problem: `coverage ${index}: not an object` };
|
|
420
|
+
const c = one;
|
|
421
|
+
if (!str(c["id"], 64) || coverageIds.has(c["id"]) || (c["state"] !== "patch" && c["state"] !== "context" && c["state"] !== "gap") ||
|
|
422
|
+
typeof c["inherited"] !== "boolean" || !strList(c["paths"], 200) || !strList(c["items"], r["schema"] === 3 ? contentLimits.items : 64) || !strList(c["gaps"], r["schema"] === 3 ? 400 : 64) ||
|
|
423
|
+
(c["priorSupport"] !== "eligible" && c["priorSupport"] !== "invalid" && c["priorSupport"] !== "none")) {
|
|
424
|
+
return { ok: false, problem: `coverage ${index}: malformed` };
|
|
425
|
+
}
|
|
426
|
+
if (c["items"].some(id => !ids.has(id) || !items.find(item => item.id === id)?.criteria.includes(String(c["id"]))))
|
|
427
|
+
return { ok: false, problem: `coverage ${index}: names an item that is not in the inventory` };
|
|
428
|
+
if (items.some(item => (item.patch !== undefined || item.redacted) && c["items"].includes(item.id)) && (c["state"] !== "gap" || c["gaps"].length === 0))
|
|
429
|
+
return { ok: false, problem: `coverage ${index}: partial patches require an explicit gap; redacted items do too` };
|
|
430
|
+
coverageIds.add(c["id"]);
|
|
431
|
+
coverage.push({
|
|
432
|
+
id: c["id"], state: c["state"], inherited: c["inherited"], paths: [...c["paths"]],
|
|
433
|
+
items: [...c["items"]], gaps: [...c["gaps"]], priorSupport: c["priorSupport"],
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
ok: true,
|
|
438
|
+
inventory: {
|
|
439
|
+
schema: r["schema"],
|
|
440
|
+
...(handoff === undefined ? {} : { handoff }),
|
|
441
|
+
...(observations === undefined ? {} : { observations }),
|
|
442
|
+
...(r["schema"] !== 1 ? { identities } : {}),
|
|
443
|
+
...(bindings === undefined ? {} : { bindings: bindings }),
|
|
444
|
+
run: Number(r["run"]),
|
|
445
|
+
head: r["head"],
|
|
446
|
+
base: r["base"],
|
|
447
|
+
source: { task: so["task"], run: Number(so["run"]), head: so["head"], scopeDigest: so["scopeDigest"], verified: so["verified"], problems: [...so["problems"]] },
|
|
448
|
+
ancestry: { verified: an["verified"], detail: an["detail"] },
|
|
449
|
+
items,
|
|
450
|
+
gaps,
|
|
451
|
+
priorReview,
|
|
452
|
+
coverage,
|
|
453
|
+
limits: { itemBytes: Number(li["itemBytes"]), aggregateBytes: Number(li["aggregateBytes"]), items: Number(li["items"]) },
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/** What the reviewer's parser needs from an inventory: the provenance
|
|
458
|
+
* tokens a note may cite, and the criteria whose judgement must cite one —
|
|
459
|
+
* every criterion NOT judged from the patch alone. */
|
|
460
|
+
export function reviewContextRules(inventory) {
|
|
461
|
+
return {
|
|
462
|
+
itemIds: new Set(inventory.items.map(one => one.id)),
|
|
463
|
+
provenanceRequired: new Set(inventory.coverage.filter(one => one.state !== "patch").map(one => one.id)),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/** Whether a reviewer's note cites supplied provenance: a `ctx-<n>` item
|
|
467
|
+
* id from the inventory, a path the patch names, or a sealed file name. */
|
|
468
|
+
export function citesSuppliedProvenance(note, supplied) {
|
|
469
|
+
for (const token of note.match(/ctx-[0-9]{1,4}/g) ?? [])
|
|
470
|
+
if (supplied.itemIds.has(token))
|
|
471
|
+
return true;
|
|
472
|
+
for (const path of supplied.patchPaths)
|
|
473
|
+
if (path.length > 0 && note.includes(path))
|
|
474
|
+
return true;
|
|
475
|
+
for (const file of supplied.sealedFiles)
|
|
476
|
+
if (note.includes(file))
|
|
477
|
+
return true;
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
const GIT_READ = ["--literal-pathspecs", "--no-lazy-fetch", "--no-replace-objects", "--no-optional-locks"];
|
|
481
|
+
async function treeEntries(git, cwd, revision, paths) {
|
|
482
|
+
if (paths.length === 0)
|
|
483
|
+
return new Map();
|
|
484
|
+
const listed = await git("git", [...GIT_READ, "ls-tree", "-z", "-l", revision, "--", ...paths], { cwd, maxBuffer: 4 * 1024 * 1024 });
|
|
485
|
+
if (listed.code !== 0)
|
|
486
|
+
return null;
|
|
487
|
+
const entries = new Map();
|
|
488
|
+
for (const record of listed.stdout.split("\u0000")) {
|
|
489
|
+
if (record === "")
|
|
490
|
+
continue;
|
|
491
|
+
const tab = record.indexOf("\t");
|
|
492
|
+
if (tab < 0)
|
|
493
|
+
continue;
|
|
494
|
+
const [mode, type, object, sizeText] = record.slice(0, tab).trim().split(/\s+/);
|
|
495
|
+
const path = record.slice(tab + 1);
|
|
496
|
+
if (mode === undefined || type === undefined || object === undefined || sizeText === undefined)
|
|
497
|
+
continue;
|
|
498
|
+
entries.set(path, { mode, type, object, size: sizeText === "-" ? null : Number(sizeText) });
|
|
499
|
+
}
|
|
500
|
+
return entries;
|
|
501
|
+
}
|
|
502
|
+
function regular(entry) {
|
|
503
|
+
return entry !== undefined && entry.type === "blob" && ["100644", "100755"].includes(entry.mode) && SHA1.test(entry.object) && Number.isSafeInteger(entry.size) && entry.size >= 0;
|
|
504
|
+
}
|
|
505
|
+
function sameEntry(a, b) {
|
|
506
|
+
return a === undefined && b === undefined || regular(a) && regular(b) && a.object === b.object && a.mode === b.mode;
|
|
507
|
+
}
|
|
508
|
+
/** The native redactor replaces an entire diff line. Quoted marker text
|
|
509
|
+
* in code is not a redaction: unchanged/added/deleted lines have a prefix. */
|
|
510
|
+
function hasRedactedLine(text) {
|
|
511
|
+
return /^\[redacted: [^\r\n]+ detected on this line\]\r?$/m.test(text);
|
|
512
|
+
}
|
|
513
|
+
/** Select a whole plain, same-path text diff section. Quoted paths,
|
|
514
|
+
* renames, binary patches and redactions WITHIN it are unsupported.
|
|
515
|
+
* Other sections may be redacted; their hidden bytes are never inferred.
|
|
516
|
+
* Offsets refer to the original verified bytes, not regenerated output. */
|
|
517
|
+
function patchSection(content, path) {
|
|
518
|
+
if (!/^[A-Za-z0-9_./-]+$/.test(path))
|
|
519
|
+
return null;
|
|
520
|
+
const text = content.toString("utf8");
|
|
521
|
+
if (!Buffer.from(text, "utf8").equals(content) || text.includes("\0"))
|
|
522
|
+
return null;
|
|
523
|
+
const header = `diff --git a/${path} b/${path}\n`;
|
|
524
|
+
const sections = [...text.matchAll(/^diff --git .*\n/gm)];
|
|
525
|
+
const matches = sections.filter(one => one[0] === header);
|
|
526
|
+
if (matches.length !== 1)
|
|
527
|
+
return null;
|
|
528
|
+
const start = matches[0].index;
|
|
529
|
+
const end = sections.find(one => one.index > start)?.index ?? text.length;
|
|
530
|
+
const section = text.slice(start, end);
|
|
531
|
+
if (/^(?:Binary files |GIT binary patch|rename |copy |similarity index )/m.test(section) || scanForSecrets(section).length > 0 || hasRedactedLine(section))
|
|
532
|
+
return null;
|
|
533
|
+
const offset = Buffer.byteLength(text.slice(0, start), "utf8");
|
|
534
|
+
return { offset, content: content.subarray(offset, offset + Buffer.byteLength(section, "utf8")) };
|
|
535
|
+
}
|
|
536
|
+
/** Account for EVERY sealed interval through this revision. If a run's
|
|
537
|
+
* base omits an intervening change to this path, refuse the entire chain.
|
|
538
|
+
* Equal blobs/modes bridge intervals without needing full-file capture. */
|
|
539
|
+
async function ancestorPatch(store, git, args, inventory, path) {
|
|
540
|
+
const fail = (detail, reason = "source-unverified") => ({ ok: false, reason, detail });
|
|
541
|
+
if (!inventory.source.verified || !inventory.ancestry.verified || inventory.gaps.some(one => one.path === null && one.reason !== "criterion-changed"))
|
|
542
|
+
return fail("verified source ancestry is unavailable for a partial patch");
|
|
543
|
+
const runs = [...(inventory.bindings ?? []).map(one => one.run), args.runId];
|
|
544
|
+
const segments = [];
|
|
545
|
+
const contents = [];
|
|
546
|
+
let previous;
|
|
547
|
+
let previousHead = null;
|
|
548
|
+
let total = 0;
|
|
549
|
+
for (const runId of runs) {
|
|
550
|
+
const run = store.getRun(runId);
|
|
551
|
+
const base = runId === args.runId ? args.base : run?.baseRevision;
|
|
552
|
+
const head = runId === args.runId ? args.head : run?.headRevision;
|
|
553
|
+
if (!base || !head || !SHA1.test(base) || !SHA1.test(head))
|
|
554
|
+
return fail(`run #${runId} has no exact patch endpoints`);
|
|
555
|
+
for (const from of new Set([base, previousHead ?? base])) {
|
|
556
|
+
const ancestor = await git("git", [...GIT_READ, "merge-base", "--is-ancestor", from, head], { cwd: args.worktree });
|
|
557
|
+
if (ancestor.code !== 0)
|
|
558
|
+
return fail(`run #${runId}'s patch ancestry could not be verified`);
|
|
559
|
+
}
|
|
560
|
+
const before = await treeEntries(git, args.worktree, base, [path]);
|
|
561
|
+
const after = await treeEntries(git, args.worktree, head, [path]);
|
|
562
|
+
if (before === null || after === null)
|
|
563
|
+
return fail(`run #${runId}'s patch trees could not be read`, "capture-failed");
|
|
564
|
+
const a = before.get(path);
|
|
565
|
+
const b = after.get(path);
|
|
566
|
+
if ((a !== undefined && !regular(a)) || (b !== undefined && !regular(b)))
|
|
567
|
+
return fail(`run #${runId}'s patch crosses a non-file`, "not-a-file");
|
|
568
|
+
if (previousHead !== null && !sameEntry(previous, a))
|
|
569
|
+
return fail(`uncovered changes between ${previousHead} and run #${runId}'s base; no ancestor patch supplied`);
|
|
570
|
+
previous = b;
|
|
571
|
+
previousHead = head;
|
|
572
|
+
if (sameEntry(a, b))
|
|
573
|
+
continue;
|
|
574
|
+
const artifacts = store.artifactsFor(runId).filter(one => one.kind === "terminal-diff");
|
|
575
|
+
const artifact = artifacts[0];
|
|
576
|
+
if (artifacts.length !== 1 || artifact === undefined)
|
|
577
|
+
return fail(`run #${runId}'s terminal patch is missing or ambiguous`);
|
|
578
|
+
if (artifact.truncated)
|
|
579
|
+
return fail(`run #${runId}'s terminal patch exceeds its capture budget`, "over-limit");
|
|
580
|
+
if (artifact.captureStatus === "failed")
|
|
581
|
+
return fail(`run #${runId}'s terminal patch capture failed`, "capture-failed");
|
|
582
|
+
let read;
|
|
583
|
+
try {
|
|
584
|
+
read = readVerifiedArtifact(args.root, artifact);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return fail(`run #${runId}'s terminal patch cannot be read`, "capture-failed");
|
|
588
|
+
}
|
|
589
|
+
if (!read.ok)
|
|
590
|
+
return fail(`run #${runId}'s terminal patch bytes do not verify`);
|
|
591
|
+
const section = patchSection(read.content, path);
|
|
592
|
+
if (section === null) {
|
|
593
|
+
const text = read.content.toString("utf8");
|
|
594
|
+
if (scanForSecrets(text).length > 0 || hasRedactedLine(text))
|
|
595
|
+
return fail(`run #${runId}'s terminal patch contains secret-shaped or redacted lines`, "secret-redacted");
|
|
596
|
+
const binary = text.includes(`Binary files a/${path} and b/${path} differ`) || text.includes(`Binary files /dev/null and b/${path} differ`);
|
|
597
|
+
return fail(`run #${runId}'s path patch is ${binary ? "binary" : "missing or an unsupported shape"}`, binary ? "binary" : "source-unverified");
|
|
598
|
+
}
|
|
599
|
+
// Git's index header binds the section to both exact tree blobs.
|
|
600
|
+
const index = /^index ([0-9a-f]{7,40})\.\.([0-9a-f]{7,40})(?: [0-7]{6})?$/m.exec(section.content.toString("utf8"));
|
|
601
|
+
if (index === null || !(a?.object ?? "0".repeat(40)).startsWith(index[1]) || !(b?.object ?? "0".repeat(40)).startsWith(index[2]))
|
|
602
|
+
return fail(`run #${runId}'s path patch does not bind both tree blobs`);
|
|
603
|
+
total += section.content.length;
|
|
604
|
+
if (total > LEGACY_REVIEW_CONTEXT_LIMITS.itemBytes)
|
|
605
|
+
return fail("the ancestor patch chain exceeds the item budget; no segments were omitted", "over-limit");
|
|
606
|
+
segments.push({ run: runId, base, head, artifact: artifact.id, sha256: artifact.sha256, offset: section.offset, bytes: section.content.length });
|
|
607
|
+
contents.push(section.content);
|
|
608
|
+
}
|
|
609
|
+
if (segments.length === 0)
|
|
610
|
+
return fail("no changed source section is available for this path");
|
|
611
|
+
return { ok: true, content: Buffer.concat(contents).toString("utf8"), segments };
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Capture complete criterion-relevant files for every rubric-bearing build.
|
|
615
|
+
* Ordinary builds bind their own candidate; revisions also bind their ancestry.
|
|
616
|
+
* Every capture stores a parseable inventory; problems become gaps in it, never a
|
|
617
|
+
* missing or truncated file.
|
|
618
|
+
*/
|
|
619
|
+
export async function captureReviewContext(store, git, args) {
|
|
620
|
+
const lineage = store.revisionSourceOf(args.taskRef) ?? { sourceRun: args.runId, sourceTask: store.externalIdFor(args.taskRef) };
|
|
621
|
+
const inventory = await deriveReviewContext(store, git, args, lineage);
|
|
622
|
+
let encoded = serializeReviewContext(inventory);
|
|
623
|
+
if (Buffer.byteLength(encoded, "utf8") > EVIDENCE_CAPS["review-context"]) {
|
|
624
|
+
// Unreachable under the content limits, but a structured artifact is
|
|
625
|
+
// never byte-truncated: shed every item and say so.
|
|
626
|
+
const stripped = {
|
|
627
|
+
...inventory,
|
|
628
|
+
items: [],
|
|
629
|
+
gaps: [...inventory.gaps, { reason: "over-limit", path: null, criteria: inventory.coverage.map(one => one.id), detail: "the encoded inventory exceeded its evidence cap — every item was shed" }],
|
|
630
|
+
coverage: inventory.coverage.map(one => ({ ...one, state: one.state === "patch" ? "patch" : "gap", items: [], gaps: [...one.gaps, "the encoded inventory exceeded its evidence cap"] })),
|
|
631
|
+
priorReview: inventory.priorReview.map(one => ({ ...one, support: "invalid", relevantUnchanged: false, reasons: [...one.reasons, "the inventory was shed"] })),
|
|
632
|
+
};
|
|
633
|
+
encoded = serializeReviewContext(stripped);
|
|
634
|
+
const artifactId = storeEvidence(store, args.root, args.runId, "review-context", "review-context.json", Buffer.from(encoded, "utf8"), `machine-captured review context at ${args.head} (shed, exit 0)`, args.now(), { captureStatus: "ok" });
|
|
635
|
+
return { inventory: stripped, artifactId };
|
|
636
|
+
}
|
|
637
|
+
const artifactId = storeEvidence(store, args.root, args.runId, "review-context", "review-context.json", Buffer.from(encoded, "utf8"), `machine-captured review context at ${args.head} (exit 0)`, args.now(), {
|
|
638
|
+
captureStatus: "ok",
|
|
639
|
+
redacted: inventory.items.some(one => one.redacted),
|
|
640
|
+
});
|
|
641
|
+
return { inventory, artifactId };
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* The derivation, separated from storage so a test can hold the inventory
|
|
645
|
+
* against a real repository without an evidence root.
|
|
646
|
+
*/
|
|
647
|
+
export async function deriveReviewContext(store, git, args, lineage) {
|
|
648
|
+
const gaps = [];
|
|
649
|
+
const allIds = args.rubric.map(one => one.id);
|
|
650
|
+
const sourceRun = store.getRun(lineage.sourceRun);
|
|
651
|
+
const sourceProblems = [];
|
|
652
|
+
const inventory = {
|
|
653
|
+
schema: 3,
|
|
654
|
+
identities: [],
|
|
655
|
+
bindings: [reviewContextBindingOf(store, lineage.sourceRun, lineage.sourceRun === args.runId)],
|
|
656
|
+
run: args.runId,
|
|
657
|
+
head: args.head,
|
|
658
|
+
base: args.base,
|
|
659
|
+
source: { task: lineage.sourceTask, run: lineage.sourceRun, head: sourceRun?.headRevision ?? null, scopeDigest: sourceRun?.scopeDigest ?? null, verified: false, problems: sourceProblems },
|
|
660
|
+
ancestry: { verified: false, detail: "not checked" },
|
|
661
|
+
items: [],
|
|
662
|
+
gaps,
|
|
663
|
+
priorReview: [],
|
|
664
|
+
coverage: [],
|
|
665
|
+
limits: { itemBytes: REVIEW_CONTEXT_LIMITS.itemBytes, aggregateBytes: REVIEW_CONTEXT_LIMITS.aggregateBytes, items: REVIEW_CONTEXT_LIMITS.items },
|
|
666
|
+
};
|
|
667
|
+
const finish = (coverage) => ({ ...inventory, coverage });
|
|
668
|
+
const gapAll = (reason, detail) => {
|
|
669
|
+
gaps.push({ reason, path: null, criteria: allIds, detail });
|
|
670
|
+
};
|
|
671
|
+
try {
|
|
672
|
+
const observations = readObservationEvidence(store, args.root, args.runId);
|
|
673
|
+
if (observations)
|
|
674
|
+
inventory.observations = observations;
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
gapAll("capture-failed", "the focused observations could not be verified");
|
|
678
|
+
}
|
|
679
|
+
const handoffs = store.artifactsFor(args.runId).filter(one => one.kind === "handoff");
|
|
680
|
+
if (handoffs.length === 1) {
|
|
681
|
+
const artifact = handoffs[0];
|
|
682
|
+
const read = readVerifiedArtifact(args.root, artifact);
|
|
683
|
+
if (read.ok && !artifact.truncated && artifact.captureStatus !== "failed")
|
|
684
|
+
inventory.handoff = { artifact: artifact.id, sha256: artifact.sha256, content: read.content.toString("utf8") };
|
|
685
|
+
else
|
|
686
|
+
gapAll("capture-failed", "the captured builder notes could not be verified");
|
|
687
|
+
}
|
|
688
|
+
else if (handoffs.length > 1)
|
|
689
|
+
gapAll("source-unverified", "multiple builder notes make the handoff ambiguous");
|
|
690
|
+
const patchOnly = (id, inherited, extra = []) => ({
|
|
691
|
+
id, state: "patch", inherited, paths: [], items: [], gaps: extra, priorSupport: "none",
|
|
692
|
+
});
|
|
693
|
+
// 1. The source run and its VERIFIED artifacts.
|
|
694
|
+
if (sourceRun === null) {
|
|
695
|
+
gapAll("source-missing", `source run #${lineage.sourceRun} does not exist — no inherited context can be supplied`);
|
|
696
|
+
return finish(allIds.map(id => ({ ...patchOnly(id, false), state: "gap", gaps: [`source run #${lineage.sourceRun} does not exist`] })));
|
|
697
|
+
}
|
|
698
|
+
const sourceHead = sourceRun.headRevision !== null && SHA1.test(sourceRun.headRevision) ? sourceRun.headRevision : null;
|
|
699
|
+
if (sourceHead === null)
|
|
700
|
+
sourceProblems.push("the source run has no sealed 40-hex head");
|
|
701
|
+
const sourceArtifacts = store.artifactsFor(sourceRun.id);
|
|
702
|
+
const verifiedOne = (kind, label) => {
|
|
703
|
+
const found = sourceArtifacts.filter(one => one.kind === kind);
|
|
704
|
+
if (found.length !== 1) {
|
|
705
|
+
sourceProblems.push(found.length === 0 ? `the source run has no sealed ${label}` : `the source run carries ${found.length} ${label} artifacts — ambiguous`);
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const artifact = found[0];
|
|
709
|
+
if (artifact.captureStatus === "failed" || artifact.truncated) {
|
|
710
|
+
sourceProblems.push(`the source run's ${label} is ${artifact.truncated ? "truncated" : "a failed capture"}`);
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
let verified;
|
|
714
|
+
try {
|
|
715
|
+
verified = readVerifiedArtifact(args.root, artifact);
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
verified = { ok: false, problem: "the file could not be read" };
|
|
719
|
+
}
|
|
720
|
+
if (!verified.ok) {
|
|
721
|
+
sourceProblems.push(`the source run's ${label} no longer verifies (${verified.problem})`);
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
return verified.content;
|
|
725
|
+
};
|
|
726
|
+
const hasProof = sourceArtifacts.some(one => one.kind === "proof");
|
|
727
|
+
const proofBytes = hasProof ? verifiedOne("proof", "proof") : null;
|
|
728
|
+
let sourceProof = null;
|
|
729
|
+
if (proofBytes !== null) {
|
|
730
|
+
const parsed = parseProof(proofBytes.toString("utf8"));
|
|
731
|
+
if (parsed.ok)
|
|
732
|
+
sourceProof = parsed.proof;
|
|
733
|
+
else
|
|
734
|
+
sourceProblems.push("the source run's proof is not a proof this build can read");
|
|
735
|
+
}
|
|
736
|
+
const statBytes = verifiedOne("diff-stat", "diff-stat");
|
|
737
|
+
let sourceDiffPaths = null;
|
|
738
|
+
if (statBytes !== null) {
|
|
739
|
+
try {
|
|
740
|
+
const stat = JSON.parse(statBytes.toString("utf8"));
|
|
741
|
+
if (stat?.filesTruncated === true)
|
|
742
|
+
sourceProblems.push("the source run's diff-stat file list is truncated");
|
|
743
|
+
else if (stat?.schema !== 1 || stat.base !== sourceRun.baseRevision || stat.head !== sourceHead || stat.filesTruncated !== false || !Array.isArray(stat.files) || stat.files.length > 400 || stat.fileCount !== stat.files.length || !stat.files.every(one => one !== null && typeof one.path === "string" && safeContextPath(one.path)) || new Set(stat.files.map(one => one.path)).size !== stat.files.length)
|
|
744
|
+
sourceProblems.push("the source run's diff-stat shape, endpoints or path inventory is unsupported");
|
|
745
|
+
else
|
|
746
|
+
sourceDiffPaths = stat.files.map(one => one.path);
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
sourceProblems.push("the source run's diff-stat is not JSON");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const sourceDiffOk = verifiedOne("terminal-diff", "terminal diff") !== null;
|
|
753
|
+
inventory.source.verified = sourceProblems.length === 0 && sourceDiffPaths !== null && sourceDiffOk && sourceHead !== null;
|
|
754
|
+
// 2. The source rubric: only the exact scope the source run built under
|
|
755
|
+
// (its digest still live on the source task) can say what a criterion
|
|
756
|
+
// meant then. A moved scope is a gap for every criterion.
|
|
757
|
+
const sourceTaskId = store.externalIdFor(sourceRun.taskRef);
|
|
758
|
+
const sourceScope = sourceTaskId === null ? null : store.getScope(sourceTaskId);
|
|
759
|
+
const sourceScopeVerified = sourceRun.scopeDigest !== null && sourceScope !== null && sourceScope.digest === sourceRun.scopeDigest;
|
|
760
|
+
if (!sourceScopeVerified) {
|
|
761
|
+
gapAll("source-scope-changed", sourceRun.scopeDigest === null ? "the source run built under no signed scope — no criterion can be matched to it" : "the source task's scope no longer carries the digest the source run built under — its criteria cannot be matched");
|
|
762
|
+
}
|
|
763
|
+
const sourceCriteria = new Map((sourceScopeVerified ? sourceScope.acceptance : []).map(one => [one.id, one]));
|
|
764
|
+
// 3. Ancestry: is the source head an ancestor of this head?
|
|
765
|
+
if (sourceHead !== null) {
|
|
766
|
+
const check = await git("git", [...GIT_READ, "merge-base", "--is-ancestor", sourceHead, args.head], { cwd: args.worktree });
|
|
767
|
+
if (check.code === 0)
|
|
768
|
+
inventory.ancestry = { verified: true, detail: `${sourceHead} is an ancestor of ${args.head}` };
|
|
769
|
+
else if (check.code === 1) {
|
|
770
|
+
inventory.ancestry = { verified: false, detail: `${sourceHead} is not an ancestor of ${args.head} — the source work is not in this history` };
|
|
771
|
+
gapAll("stale-ancestry", inventory.ancestry.detail);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
inventory.ancestry = { verified: false, detail: `ancestry could not be checked (git exit ${check.code})` };
|
|
775
|
+
gapAll("capture-failed", inventory.ancestry.detail);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
inventory.ancestry = { verified: false, detail: "the source run has no sealed head to check ancestry against" };
|
|
780
|
+
gapAll("source-unverified", inventory.ancestry.detail);
|
|
781
|
+
}
|
|
782
|
+
if (hasProof && sourceProof === null)
|
|
783
|
+
gapAll("source-proof-missing", sourceProblems.find(one => one.includes("proof")) ?? "the source run's proof is unavailable");
|
|
784
|
+
else if (!inventory.source.verified)
|
|
785
|
+
gapAll("source-unverified", sourceProblems.join("; "));
|
|
786
|
+
// 4. Relevance: which paths matter to which criterion, from the source
|
|
787
|
+
// proof only — criterion-cited changed paths first, then every changed
|
|
788
|
+
// path of the source build for criteria that cited none.
|
|
789
|
+
// A revision of a revision keeps the parent's verified path inventory.
|
|
790
|
+
// Capture those paths anew at THIS head; prior bytes are never substituted.
|
|
791
|
+
let parentContext = null;
|
|
792
|
+
if (sourceArtifacts.some(one => one.kind === "review-context")) {
|
|
793
|
+
const bytes = verifiedOne("review-context", "review context");
|
|
794
|
+
const parsed = bytes === null ? null : parseReviewContext(bytes.toString("utf8"));
|
|
795
|
+
if (parsed?.ok) {
|
|
796
|
+
const problem = reviewContextCustodyProblem(store, args.root, parsed.inventory);
|
|
797
|
+
const combined = new Map([...(parsed.inventory.bindings ?? []), ...inventory.bindings].map(one => [one.run, one]));
|
|
798
|
+
if (problem === null && combined.size <= REVIEW_CONTEXT_LIMITS.ancestors) {
|
|
799
|
+
parentContext = parsed.inventory;
|
|
800
|
+
inventory.bindings = [...combined.values()];
|
|
801
|
+
}
|
|
802
|
+
else
|
|
803
|
+
gapAll("source-unverified", problem ?? "the inherited context exceeds the ancestor bound");
|
|
804
|
+
}
|
|
805
|
+
else
|
|
806
|
+
gapAll("source-unverified", "the source revision's inherited context cannot be verified");
|
|
807
|
+
}
|
|
808
|
+
else if (store.revisionSourceOf(sourceRun.taskRef) !== null) {
|
|
809
|
+
gapAll("source-unverified", "the source revision has no inherited context inventory");
|
|
810
|
+
}
|
|
811
|
+
const generalPaths = [...new Set([...(sourceProof?.changed ?? []), ...(sourceDiffPaths ?? []), ...(parentContext?.coverage.flatMap(one => one.paths) ?? [])])].filter(safeContextPath).sort();
|
|
812
|
+
// A first no-change result has no patch to select files from. Only the
|
|
813
|
+
// signed scope may widen its context; read exact committed paths/directories,
|
|
814
|
+
// under the same existing size/item bounds. Unsupported patterns stay gaps.
|
|
815
|
+
if (generalPaths.length === 0 && sourceScopeVerified) {
|
|
816
|
+
const paths = sourceScope.touches.filter(path => safeContextPath(path.replace(/\/$/, "")) && !/[?*\[\]]/.test(path));
|
|
817
|
+
if (paths.length > 0) {
|
|
818
|
+
const listed = await git("git", [...GIT_READ, "ls-tree", "-r", "-z", "--name-only", args.head, "--", ...paths], { cwd: args.worktree, maxBuffer: 4 * 1024 * 1024 });
|
|
819
|
+
if (listed.code === 0)
|
|
820
|
+
generalPaths.push(...[...new Set(listed.stdout.split("\0").filter(safeContextPath))].sort());
|
|
821
|
+
else
|
|
822
|
+
gapAll("capture-failed", "the committed source paths in the approved scope could not be listed");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const specificFor = new Map();
|
|
826
|
+
for (const criterion of sourceProof?.criteria ?? []) {
|
|
827
|
+
const cited = [...new Set(criterion.evidence.filter(one => one.kind === "changed-path").map(one => one.ref))].filter(safeContextPath).sort();
|
|
828
|
+
const inherited = parentContext?.coverage.find(one => one.id === criterion.id)?.paths ?? [];
|
|
829
|
+
const combined = [...new Set([...cited, ...inherited])].filter(safeContextPath).sort();
|
|
830
|
+
if (combined.length > 0)
|
|
831
|
+
specificFor.set(criterion.id, combined);
|
|
832
|
+
}
|
|
833
|
+
const plans = args.rubric.map(criterion => {
|
|
834
|
+
const inSource = sourceCriteria.get(criterion.id);
|
|
835
|
+
const knownInSource = inSource !== undefined;
|
|
836
|
+
const inherited = knownInSource && inSource.statement.trim() === criterion.statement.trim() && [...inSource.evidence].sort().join(",") === [...criterion.evidence].sort().join(",");
|
|
837
|
+
if (knownInSource && !inherited) {
|
|
838
|
+
gaps.push({ reason: "criterion-changed", path: null, criteria: [criterion.id], detail: `criterion "${criterion.id}" reads differently now than in the source run's signed rubric — its earlier judgement cannot support it` });
|
|
839
|
+
}
|
|
840
|
+
const paths = knownInSource ? (specificFor.get(criterion.id) ?? generalPaths) : [];
|
|
841
|
+
return { id: criterion.id, knownInSource, inherited, paths, changedText: knownInSource && !inherited };
|
|
842
|
+
});
|
|
843
|
+
const relevance = new Map();
|
|
844
|
+
for (const plan of plans) {
|
|
845
|
+
for (const path of plan.paths) {
|
|
846
|
+
const entry = relevance.get(path) ?? { criteria: new Set(), why: new Set() };
|
|
847
|
+
entry.criteria.add(plan.id);
|
|
848
|
+
entry.why.add(specificFor.get(plan.id)?.includes(path) ? "source-criterion-evidence" : "source-changed-path");
|
|
849
|
+
relevance.set(path, entry);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
// A proof's path citations are relevance hints, not permission to omit other
|
|
853
|
+
// changed files. Deliver new revision files and tests in full as supplemental
|
|
854
|
+
// context without inventing criterion ownership or inheriting a judgement.
|
|
855
|
+
for (const path of args.patchPaths) {
|
|
856
|
+
if (safeContextPath(path) && !relevance.has(path))
|
|
857
|
+
relevance.set(path, { criteria: new Set(), why: new Set(["candidate-changed-path"]) });
|
|
858
|
+
}
|
|
859
|
+
const candidates = [...relevance.keys()].sort((a, b) => {
|
|
860
|
+
const aSpecific = relevance.get(a).why.has("source-criterion-evidence") ? 0 : 1;
|
|
861
|
+
const bSpecific = relevance.get(b).why.has("source-criterion-evidence") ? 0 : 1;
|
|
862
|
+
return aSpecific !== bSpecific ? aSpecific - bSpecific : a.localeCompare(b);
|
|
863
|
+
});
|
|
864
|
+
if (candidates.length > REVIEW_CONTEXT_LIMITS.candidatePaths) {
|
|
865
|
+
for (const path of candidates.slice(REVIEW_CONTEXT_LIMITS.candidatePaths)) {
|
|
866
|
+
gaps.push({ reason: "item-cap", path, criteria: [...relevance.get(path).criteria].sort(), detail: `${path}: beyond the ${REVIEW_CONTEXT_LIMITS.candidatePaths} candidate-path bound` });
|
|
867
|
+
}
|
|
868
|
+
candidates.length = REVIEW_CONTEXT_LIMITS.candidatePaths;
|
|
869
|
+
}
|
|
870
|
+
// 5. Read every candidate at the EXACT head — identity and size first,
|
|
871
|
+
// bytes only for what the bounds admit — and compare identity at the
|
|
872
|
+
// source head when ancestry verified.
|
|
873
|
+
const itemsByPath = new Map();
|
|
874
|
+
if (candidates.length > 0) {
|
|
875
|
+
const atHead = await treeEntries(git, args.worktree, args.head, candidates);
|
|
876
|
+
const atSource = inventory.ancestry.verified && sourceHead !== null ? await treeEntries(git, args.worktree, sourceHead, candidates) : null;
|
|
877
|
+
if (atHead === null) {
|
|
878
|
+
gapAll("capture-failed", `the tree at ${args.head} could not be listed`);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
let aggregate = 0;
|
|
882
|
+
for (const path of candidates) {
|
|
883
|
+
const criteria = [...relevance.get(path).criteria].sort();
|
|
884
|
+
const why = [...relevance.get(path).why].sort();
|
|
885
|
+
const entry = atHead.get(path);
|
|
886
|
+
const gap = (reason, detail) => { gaps.push({ reason, path, criteria, detail: `${path}: ${detail}` }); };
|
|
887
|
+
if (entry === undefined) {
|
|
888
|
+
gap("missing-at-head", `no object at ${args.head}`);
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (!regular(entry)) {
|
|
892
|
+
gap("not-a-file", `a ${entry.mode === "120000" ? "symlink" : entry.type} at ${args.head}, not a regular file`);
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const sourceEntry = atSource?.get(path);
|
|
896
|
+
const unchangedSinceSource = atSource === null ? null : sameEntry(sourceEntry, entry);
|
|
897
|
+
inventory.identities.push({ path, commit: args.head, blob: entry.object, mode: entry.mode, bytes: entry.size, sourceRun: sourceRun.id, sourceBlob: regular(sourceEntry) ? sourceEntry.object : null, sourceMode: regular(sourceEntry) ? sourceEntry.mode : null, unchangedSinceSource });
|
|
898
|
+
const oversized = entry.size > REVIEW_CONTEXT_LIMITS.itemBytes;
|
|
899
|
+
if (oversized)
|
|
900
|
+
gap("over-limit", `${entry.size} bytes at ${args.head} exceeds the ${REVIEW_CONTEXT_LIMITS.itemBytes}-byte item limit; full-file context is missing`);
|
|
901
|
+
if (itemsByPath.size >= REVIEW_CONTEXT_LIMITS.items) {
|
|
902
|
+
gap("item-cap", `beyond the ${REVIEW_CONTEXT_LIMITS.items}-item bound`);
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (oversized) {
|
|
906
|
+
const patch = await ancestorPatch(store, git, args, inventory, path);
|
|
907
|
+
if (!patch.ok) {
|
|
908
|
+
gap(patch.reason, patch.detail);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const bytes = Buffer.byteLength(patch.content, "utf8");
|
|
912
|
+
if (aggregate + bytes > REVIEW_CONTEXT_LIMITS.aggregateBytes) {
|
|
913
|
+
gap("aggregate-limit", `the partial ancestor patch would cross the ${REVIEW_CONTEXT_LIMITS.aggregateBytes}-byte aggregate limit`);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
aggregate += bytes;
|
|
917
|
+
itemsByPath.set(path, { id: `ctx-${itemsByPath.size + 1}`, path, commit: args.head, blob: entry.object, sha256: createHash("sha256").update(patch.content, "utf8").digest("hex"), bytes, content: patch.content, sourceRun: sourceRun.id, criteria, why, unchangedSinceSource, redacted: false, patch: { coverage: "partial", segments: patch.segments } });
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (aggregate + entry.size > REVIEW_CONTEXT_LIMITS.aggregateBytes) {
|
|
921
|
+
gap("aggregate-limit", `${entry.size} bytes would cross the ${REVIEW_CONTEXT_LIMITS.aggregateBytes}-byte aggregate limit`);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const read = await git("git", [...GIT_READ, "cat-file", "blob", entry.object], { cwd: args.worktree, maxBuffer: REVIEW_CONTEXT_LIMITS.itemBytes + 4096 });
|
|
925
|
+
if (read.code !== 0) {
|
|
926
|
+
gap("capture-failed", `blob ${entry.object} could not be read (git exit ${read.code})`);
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (read.stdout.includes("\u0000") || Buffer.byteLength(read.stdout, "utf8") !== entry.size) {
|
|
930
|
+
gap("binary", "not UTF-8 text");
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
// Re-encoding must reproduce the named blob. A literal U+FFFD is
|
|
934
|
+
// valid text; a replacement introduced by decoding fails this hash.
|
|
935
|
+
const blobHash = createHash("sha1").update(`blob ${entry.size}\0`).update(read.stdout, "utf8").digest("hex");
|
|
936
|
+
if (blobHash !== entry.object) {
|
|
937
|
+
gap("capture-failed", "the returned bytes do not match the named Git blob");
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const hits = scanForSecrets(read.stdout);
|
|
941
|
+
const content = hits.length > 0 ? redactSecretLines(read.stdout, hits) : read.stdout;
|
|
942
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
943
|
+
if (bytes > REVIEW_CONTEXT_LIMITS.itemBytes || aggregate + bytes > REVIEW_CONTEXT_LIMITS.aggregateBytes) {
|
|
944
|
+
gap("over-limit", "redaction did not fit the bounds");
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (hits.length > 0)
|
|
948
|
+
gap("secret-redacted", `${hits.length} secret-shaped line(s) redacted before storage — the redacted lines cannot support any criterion`);
|
|
949
|
+
aggregate += bytes;
|
|
950
|
+
itemsByPath.set(path, {
|
|
951
|
+
id: `ctx-${itemsByPath.size + 1}`,
|
|
952
|
+
path,
|
|
953
|
+
commit: args.head,
|
|
954
|
+
blob: entry.object,
|
|
955
|
+
sha256: createHash("sha256").update(content, "utf8").digest("hex"),
|
|
956
|
+
bytes,
|
|
957
|
+
content,
|
|
958
|
+
sourceRun: sourceRun.id,
|
|
959
|
+
criteria,
|
|
960
|
+
why,
|
|
961
|
+
unchangedSinceSource,
|
|
962
|
+
redacted: hits.length > 0,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
inventory.items = [...itemsByPath.values()];
|
|
968
|
+
// 6. Prior review provenance — context, proved per criterion, never a verdict.
|
|
969
|
+
const sourceDiff = sourceArtifacts.find(one => one.kind === "terminal-diff") ?? null;
|
|
970
|
+
const sourceProofArtifact = sourceArtifacts.find(one => one.kind === "proof") ?? null;
|
|
971
|
+
const planOf = new Map(plans.map(one => [one.id, one]));
|
|
972
|
+
for (const row of store.criterionReviewsFor(sourceRun.id).slice(0, REVIEW_CONTEXT_LIMITS.priorReviews)) {
|
|
973
|
+
const plan = planOf.get(row.criterionId);
|
|
974
|
+
if (plan === undefined || !plan.knownInSource)
|
|
975
|
+
continue;
|
|
976
|
+
const reasons = [];
|
|
977
|
+
if (!plan.inherited)
|
|
978
|
+
reasons.push("the criterion's signed text changed since the source review");
|
|
979
|
+
if (!inventory.ancestry.verified)
|
|
980
|
+
reasons.push("the source head is not verified ancestry of this head");
|
|
981
|
+
if (!inventory.source.verified)
|
|
982
|
+
reasons.push("the source run's sealed artifacts do not all verify");
|
|
983
|
+
const relevantUnchanged = plan.paths.length > 0 && plan.paths.every(path => inventory.identities.some(one => one.path === path && one.unchangedSinceSource === true) && !itemsByPath.get(path)?.redacted && !gaps.some(one => one.path === path && ["binary", "secret-redacted", "capture-failed", "source-unverified"].includes(one.reason)));
|
|
984
|
+
if (!relevantUnchanged)
|
|
985
|
+
reasons.push(plan.paths.length === 0 ? "no relevant source paths are known for this criterion" : "relevant code is not byte-identical between the source head and this head, or could not be sealed");
|
|
986
|
+
const singularMatches = (kind, bound) => {
|
|
987
|
+
const entries = sourceArtifacts.filter(one => one.kind === kind);
|
|
988
|
+
return bound === null ? entries.length === 0 : entries.length === 1 && entries[0].id === bound.artifact && entries[0].sha256 === bound.sha256;
|
|
989
|
+
};
|
|
990
|
+
const shots = sourceArtifacts.filter(one => one.kind === "screenshot");
|
|
991
|
+
const screenshotsMatch = shots.length === row.screenshots.length && shots.every(one => row.screenshots.some(shot => shot.artifact === one.id && shot.sha256 === one.sha256 && shot.path === one.capture));
|
|
992
|
+
const reviewerLineage = historicalReviewerLineage(store, row.reviewerRun);
|
|
993
|
+
const reviewerVerified = reviewerLineage.length >= 2 && reviewerLineage.at(-1)?.id === sourceRun.id && reviewerLineage.slice(0, -1).every(one => one.role === "reviewer" && one.taskRef === sourceRun.taskRef && one.outcome === "no-change");
|
|
994
|
+
const allBytesVerify = sourceArtifacts.filter(one => CONTEXT_INPUT_KINDS.has(one.kind)).every(one => {
|
|
995
|
+
try {
|
|
996
|
+
return usableReviewInput(args.root, one);
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
const bindingsVerified = allBytesVerify && reviewerVerified &&
|
|
1003
|
+
singularMatches("check-log", row.checkLog) && singularMatches("review-context", row.context) && screenshotsMatch &&
|
|
1004
|
+
row.headSha === sourceHead &&
|
|
1005
|
+
row.scopeDigest === sourceRun.scopeDigest &&
|
|
1006
|
+
sourceDiff !== null && row.artifact === sourceDiff.id && row.artifactSha === sourceDiff.sha256 &&
|
|
1007
|
+
(row.proof === null ? sourceProofArtifact === null : sourceProofArtifact !== null && row.proof.artifact === sourceProofArtifact.id && row.proof.sha256 === sourceProofArtifact.sha256);
|
|
1008
|
+
if (!bindingsVerified)
|
|
1009
|
+
reasons.push("the source review's own evidence bindings no longer match the source run's sealed artifacts");
|
|
1010
|
+
inventory.priorReview.push({
|
|
1011
|
+
criterionId: row.criterionId,
|
|
1012
|
+
sourceRun: sourceRun.id,
|
|
1013
|
+
reviewerRun: row.reviewerRun,
|
|
1014
|
+
author: row.author,
|
|
1015
|
+
judgement: row.judgement,
|
|
1016
|
+
note: row.note,
|
|
1017
|
+
statementMatches: plan.inherited,
|
|
1018
|
+
ancestryVerified: inventory.ancestry.verified,
|
|
1019
|
+
relevantPaths: plan.paths,
|
|
1020
|
+
relevantUnchanged,
|
|
1021
|
+
bindingsVerified,
|
|
1022
|
+
support: reasons.length === 0 ? "eligible" : "invalid",
|
|
1023
|
+
reasons,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
// 7. Coverage per criterion — the projection every surface reads.
|
|
1027
|
+
const coverage = plans.map((plan) => {
|
|
1028
|
+
const prior = inventory.priorReview.filter(one => one.criterionId === plan.id);
|
|
1029
|
+
const priorSupport = prior.length === 0 ? "none" : prior.some(one => one.support === "eligible") ? "eligible" : "invalid";
|
|
1030
|
+
const relevantGaps = gaps.filter(one => one.criteria.includes(plan.id)).map(one => one.detail);
|
|
1031
|
+
if (!plan.knownInSource)
|
|
1032
|
+
return { ...patchOnly(plan.id, false, relevantGaps), ...(sourceScopeVerified ? {} : { state: "gap" }), priorSupport };
|
|
1033
|
+
const items = plan.paths.flatMap(path => (itemsByPath.has(path) ? [itemsByPath.get(path).id] : []));
|
|
1034
|
+
// "Judged from the patch" needs KNOWN relevance: only a verified source
|
|
1035
|
+
// proof can say the patch touched what this criterion is about.
|
|
1036
|
+
const touched = inventory.source.verified && plan.paths.some(path => args.patchPaths.has(path));
|
|
1037
|
+
const complete = plan.paths.length > 0 && plan.paths.every(path => itemsByPath.has(path) && !itemsByPath.get(path).redacted && !itemsByPath.get(path).patch) && inventory.source.verified && inventory.ancestry.verified;
|
|
1038
|
+
const state = complete && relevantGaps.length === 0 ? (touched && plan.paths.every(path => args.patchPaths.has(path)) ? "patch" : "context") : "gap";
|
|
1039
|
+
const gapsFor = state === "gap" && relevantGaps.length === 0 ? [plan.paths.length === 0 ? "no relevant source paths are known" : "inherited context is incomplete"] : relevantGaps;
|
|
1040
|
+
return { id: plan.id, state, inherited: plan.inherited && lineage.sourceRun !== args.runId, paths: plan.paths, items, gaps: gapsFor, priorSupport };
|
|
1041
|
+
});
|
|
1042
|
+
return finish(coverage);
|
|
1043
|
+
}
|