audit-tools 0.45.0 → 0.48.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/README.md +51 -18
- package/dispatch/merge-results.mjs +3 -3
- package/dispatch/validate-result.mjs +3 -3
- package/dist/audit/cli/charterExtractionPrompt.d.ts.map +1 -1
- package/dist/audit/cli/charterExtractionPrompt.js +11 -1
- package/dist/audit/cli/charterExtractionPrompt.js.map +1 -1
- package/dist/audit/cli/cleanupCommand.d.ts +25 -0
- package/dist/audit/cli/cleanupCommand.d.ts.map +1 -1
- package/dist/audit/cli/cleanupCommand.js +76 -1
- package/dist/audit/cli/cleanupCommand.js.map +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.js +52 -76
- package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
- package/dist/audit/cli/dispatch/packetFilter.d.ts +1 -1
- package/dist/audit/cli/fanoutLanes.d.ts.map +1 -1
- package/dist/audit/cli/fanoutLanes.js +6 -1
- package/dist/audit/cli/fanoutLanes.js.map +1 -1
- package/dist/audit/cli/laneSubmissions.d.ts +13 -0
- package/dist/audit/cli/laneSubmissions.d.ts.map +1 -1
- package/dist/audit/cli/laneSubmissions.js +38 -27
- package/dist/audit/cli/laneSubmissions.js.map +1 -1
- package/dist/audit/cli/nextStepCommand.d.ts +12 -0
- package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
- package/dist/audit/cli/nextStepCommand.js +23 -12
- package/dist/audit/cli/nextStepCommand.js.map +1 -1
- package/dist/audit/cli/nextStepHelpers.d.ts +3 -2
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +65 -37
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/reviewRun.d.ts +1 -1
- package/dist/audit/cli/reviewRun.d.ts.map +1 -1
- package/dist/audit/cli/reviewRun.js +2 -9
- package/dist/audit/cli/reviewRun.js.map +1 -1
- package/dist/audit/cli/sampleRunCommand.d.ts +215 -0
- package/dist/audit/cli/sampleRunCommand.d.ts.map +1 -1
- package/dist/audit/cli/sampleRunCommand.js +60 -3
- package/dist/audit/cli/sampleRunCommand.js.map +1 -1
- package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
- package/dist/audit/cli/semanticReviewStep.js +9 -1
- package/dist/audit/cli/semanticReviewStep.js.map +1 -1
- package/dist/audit/cli/steps.d.ts +6 -6
- package/dist/audit/contracts/workerSchemas.d.ts +36 -26
- package/dist/audit/contracts/workerSchemas.d.ts.map +1 -1
- package/dist/audit/contracts/workerSchemas.js +11 -1
- package/dist/audit/contracts/workerSchemas.js.map +1 -1
- package/dist/audit/contracts/wrapperResponse.d.ts +52 -28
- package/dist/audit/contracts/wrapperResponse.d.ts.map +1 -1
- package/dist/audit/contracts/wrapperResponse.js +8 -5
- package/dist/audit/contracts/wrapperResponse.js.map +1 -1
- package/dist/audit/decompose/sources.d.ts.map +1 -1
- package/dist/audit/decompose/sources.js +21 -5
- package/dist/audit/decompose/sources.js.map +1 -1
- package/dist/audit/extractors/analyzers/registry.d.ts +8 -5
- package/dist/audit/extractors/analyzers/registry.d.ts.map +1 -1
- package/dist/audit/extractors/analyzers/registry.js +28 -7
- package/dist/audit/extractors/analyzers/registry.js.map +1 -1
- package/dist/audit/extractors/designAssessment.js +1 -1
- package/dist/audit/extractors/gitHistory.d.ts +1 -1
- package/dist/audit/extractors/gitHistory.js +1 -1
- package/dist/audit/extractors/graph.d.ts +1 -1
- package/dist/audit/extractors/graph.js +1 -1
- package/dist/audit/extractors/risk.d.ts +1 -1
- package/dist/audit/extractors/risk.js +1 -1
- package/dist/audit/io/artifacts.d.ts.map +1 -1
- package/dist/audit/io/artifacts.js +5 -5
- package/dist/audit/io/artifacts.js.map +1 -1
- package/dist/audit/orchestrator/acquisitionExecutor.d.ts +12 -7
- package/dist/audit/orchestrator/acquisitionExecutor.d.ts.map +1 -1
- package/dist/audit/orchestrator/acquisitionExecutor.js +1 -1
- package/dist/audit/orchestrator/acquisitionExecutor.js.map +1 -1
- package/dist/audit/orchestrator/advance.d.ts +14 -10
- package/dist/audit/orchestrator/advance.d.ts.map +1 -1
- package/dist/audit/orchestrator/advance.js +49 -24
- package/dist/audit/orchestrator/advance.js.map +1 -1
- package/dist/audit/orchestrator/advanceTypes.d.ts +14 -0
- package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
- package/dist/audit/orchestrator/autoFixExecutor.d.ts +20 -1
- package/dist/audit/orchestrator/autoFixExecutor.d.ts.map +1 -1
- package/dist/audit/orchestrator/autoFixExecutor.js +77 -15
- package/dist/audit/orchestrator/autoFixExecutor.js.map +1 -1
- package/dist/audit/orchestrator/dependencyMap.js +1 -1
- package/dist/audit/orchestrator/dependencyMap.js.map +1 -1
- package/dist/audit/orchestrator/designReviewPrompt.d.ts.map +1 -1
- package/dist/audit/orchestrator/designReviewPrompt.js +7 -0
- package/dist/audit/orchestrator/designReviewPrompt.js.map +1 -1
- package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.js +18 -4
- package/dist/audit/orchestrator/executorRunners.js.map +1 -1
- package/dist/audit/orchestrator/executors.d.ts +54 -0
- package/dist/audit/orchestrator/executors.d.ts.map +1 -1
- package/dist/audit/orchestrator/executors.js +352 -0
- package/dist/audit/orchestrator/executors.js.map +1 -1
- package/dist/audit/orchestrator/hostInputPause.d.ts +28 -13
- package/dist/audit/orchestrator/hostInputPause.d.ts.map +1 -1
- package/dist/audit/orchestrator/hostInputPause.js +17 -11
- package/dist/audit/orchestrator/hostInputPause.js.map +1 -1
- package/dist/audit/orchestrator/intentInterpreter.d.ts +15 -18
- package/dist/audit/orchestrator/intentInterpreter.d.ts.map +1 -1
- package/dist/audit/orchestrator/intentInterpreter.js +12 -26
- package/dist/audit/orchestrator/intentInterpreter.js.map +1 -1
- package/dist/audit/orchestrator/localCommands.d.ts +24 -1
- package/dist/audit/orchestrator/localCommands.d.ts.map +1 -1
- package/dist/audit/orchestrator/localCommands.js +44 -9
- package/dist/audit/orchestrator/localCommands.js.map +1 -1
- package/dist/audit/orchestrator/nextStep.d.ts +13 -0
- package/dist/audit/orchestrator/nextStep.d.ts.map +1 -1
- package/dist/audit/orchestrator/nextStep.js +13 -0
- package/dist/audit/orchestrator/nextStep.js.map +1 -1
- package/dist/audit/orchestrator/requeueCommand.d.ts +1 -1
- package/dist/audit/orchestrator/runtimeCommand.d.ts +32 -1
- package/dist/audit/orchestrator/runtimeCommand.d.ts.map +1 -1
- package/dist/audit/orchestrator/runtimeCommand.js +90 -3
- package/dist/audit/orchestrator/runtimeCommand.js.map +1 -1
- package/dist/audit/orchestrator/structureExecutors.js +1 -1
- package/dist/audit/orchestrator/structureExecutors.js.map +1 -1
- package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts +11 -1
- package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts.map +1 -1
- package/dist/audit/orchestrator/syntaxResolutionExecutor.js +25 -9
- package/dist/audit/orchestrator/syntaxResolutionExecutor.js.map +1 -1
- package/dist/audit/supervisor/operatorHandoff.d.ts +0 -1
- package/dist/audit/supervisor/operatorHandoff.d.ts.map +1 -1
- package/dist/audit/supervisor/operatorHandoff.js +2 -1
- package/dist/audit/supervisor/operatorHandoff.js.map +1 -1
- package/dist/audit/types.d.ts +18 -18
- package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/derive.js +1 -0
- package/dist/remediate/contractPipeline/derive.js.map +1 -1
- package/dist/remediate/contractPipeline/semanticProjection.d.ts +5 -4
- package/dist/remediate/contractPipeline/semanticProjection.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/semanticProjection.js +34 -19
- package/dist/remediate/contractPipeline/semanticProjection.js.map +1 -1
- package/dist/remediate/index.d.ts.map +1 -1
- package/dist/remediate/index.js +3 -2
- package/dist/remediate/index.js.map +1 -1
- package/dist/remediate/intake.js +1 -1
- package/dist/remediate/intake.js.map +1 -1
- package/dist/remediate/phases/close.d.ts +45 -2
- package/dist/remediate/phases/close.d.ts.map +1 -1
- package/dist/remediate/phases/close.js +111 -43
- package/dist/remediate/phases/close.js.map +1 -1
- package/dist/remediate/phases/closeVerifyAnalyzerLeads.js +2 -2
- package/dist/remediate/phases/closeVerifyAnalyzerLeads.js.map +1 -1
- package/dist/remediate/phases/triage.d.ts.map +1 -1
- package/dist/remediate/phases/triage.js +61 -19
- package/dist/remediate/phases/triage.js.map +1 -1
- package/dist/remediate/review/autonomousGate.d.ts +3 -7
- package/dist/remediate/review/autonomousGate.d.ts.map +1 -1
- package/dist/remediate/review/autonomousGate.js +10 -24
- package/dist/remediate/review/autonomousGate.js.map +1 -1
- package/dist/remediate/riskSignal.d.ts +8 -20
- package/dist/remediate/riskSignal.d.ts.map +1 -1
- package/dist/remediate/riskSignal.js +21 -76
- package/dist/remediate/riskSignal.js.map +1 -1
- package/dist/remediate/state/accessMemory.d.ts +2 -3
- package/dist/remediate/state/accessMemory.d.ts.map +1 -1
- package/dist/remediate/state/accessMemory.js +3 -6
- package/dist/remediate/state/accessMemory.js.map +1 -1
- package/dist/remediate/state/store.d.ts +6 -7
- package/dist/remediate/state/store.d.ts.map +1 -1
- package/dist/remediate/state/store.js.map +1 -1
- package/dist/remediate/state/types.d.ts +2 -84
- package/dist/remediate/state/types.d.ts.map +1 -1
- package/dist/remediate/state/types.js +1 -39
- package/dist/remediate/state/types.js.map +1 -1
- package/dist/remediate/steps/contractPipeline.d.ts +13 -19
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +157 -66
- package/dist/remediate/steps/contractPipeline.js.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.js +297 -141
- package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts +16 -7
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +207 -216
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/stepUtils.d.ts +10 -2
- package/dist/remediate/steps/stepUtils.d.ts.map +1 -1
- package/dist/remediate/steps/stepUtils.js +13 -3
- package/dist/remediate/steps/stepUtils.js.map +1 -1
- package/dist/remediate/steps/types.d.ts +1 -1
- package/dist/remediate/steps/types.d.ts.map +1 -1
- package/dist/remediate/steps/types.js.map +1 -1
- package/dist/remediate/types/options.d.ts +1 -1
- package/dist/remediate/validation/artifacts.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.js +1 -16
- package/dist/remediate/validation/artifacts.js.map +1 -1
- package/dist/remediate/validation/remediationState.d.ts +0 -1
- package/dist/remediate/validation/remediationState.d.ts.map +1 -1
- package/dist/remediate/validation/remediationState.js +0 -51
- package/dist/remediate/validation/remediationState.js.map +1 -1
- package/dist/shared/analyzerPolicy.d.ts +18 -6
- package/dist/shared/analyzerPolicy.d.ts.map +1 -1
- package/dist/shared/analyzerPolicy.js +15 -3
- package/dist/shared/analyzerPolicy.js.map +1 -1
- package/dist/shared/analyzers/acquisitionEngine.d.ts +71 -32
- package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
- package/dist/shared/analyzers/acquisitionEngine.js +86 -28
- package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
- package/dist/shared/analyzers/binaryAcquisition.d.ts +8 -2
- package/dist/shared/analyzers/binaryAcquisition.d.ts.map +1 -1
- package/dist/shared/analyzers/binaryAcquisition.js +4 -4
- package/dist/shared/analyzers/binaryAcquisition.js.map +1 -1
- package/dist/shared/constitutionalDocPaths.d.ts.map +1 -1
- package/dist/shared/constitutionalDocPaths.js +7 -0
- package/dist/shared/constitutionalDocPaths.js.map +1 -1
- package/dist/shared/engine/obligationEngine.d.ts +44 -3
- package/dist/shared/engine/obligationEngine.d.ts.map +1 -1
- package/dist/shared/engine/obligationEngine.js +53 -7
- package/dist/shared/engine/obligationEngine.js.map +1 -1
- package/dist/shared/findings/dedupe.d.ts.map +1 -1
- package/dist/shared/findings/dedupe.js +8 -0
- package/dist/shared/findings/dedupe.js.map +1 -1
- package/dist/shared/friction/frictionRecord.d.ts +42 -2
- package/dist/shared/friction/frictionRecord.d.ts.map +1 -1
- package/dist/shared/friction/frictionRecord.js +111 -3
- package/dist/shared/friction/frictionRecord.js.map +1 -1
- package/dist/shared/friction/triage.d.ts +15 -0
- package/dist/shared/friction/triage.d.ts.map +1 -1
- package/dist/shared/friction/triage.js +33 -2
- package/dist/shared/friction/triage.js.map +1 -1
- package/dist/shared/index.d.ts +15 -9
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +15 -7
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/intent/constraintClauses.d.ts +31 -0
- package/dist/shared/intent/constraintClauses.d.ts.map +1 -0
- package/dist/shared/intent/constraintClauses.js +37 -0
- package/dist/shared/intent/constraintClauses.js.map +1 -0
- package/dist/shared/io/fileLock.d.ts.map +1 -1
- package/dist/shared/io/fileLock.js +22 -3
- package/dist/shared/io/fileLock.js.map +1 -1
- package/dist/shared/io/frictionCapture.d.ts +73 -19
- package/dist/shared/io/frictionCapture.d.ts.map +1 -1
- package/dist/shared/io/frictionCapture.js +21 -30
- package/dist/shared/io/frictionCapture.js.map +1 -1
- package/dist/shared/io/lockedJsonStore.d.ts +24 -3
- package/dist/shared/io/lockedJsonStore.d.ts.map +1 -1
- package/dist/shared/io/lockedJsonStore.js +35 -7
- package/dist/shared/io/lockedJsonStore.js.map +1 -1
- package/dist/shared/io/stepContractWriter.d.ts +3 -3
- package/dist/shared/io/stepContractWriter.d.ts.map +1 -1
- package/dist/shared/io/stepContractWriter.js +97 -13
- package/dist/shared/io/stepContractWriter.js.map +1 -1
- package/dist/shared/submission/expectedSubmissions.d.ts +18 -0
- package/dist/shared/submission/expectedSubmissions.d.ts.map +1 -1
- package/dist/shared/submission/expectedSubmissions.js +17 -0
- package/dist/shared/submission/expectedSubmissions.js.map +1 -1
- package/dist/shared/submission/handRecovery.d.ts.map +1 -1
- package/dist/shared/submission/handRecovery.js +9 -11
- package/dist/shared/submission/handRecovery.js.map +1 -1
- package/dist/shared/submission/hostHandoffCore.d.ts +155 -0
- package/dist/shared/submission/hostHandoffCore.d.ts.map +1 -0
- package/dist/shared/submission/hostHandoffCore.js +210 -0
- package/dist/shared/submission/hostHandoffCore.js.map +1 -0
- package/dist/shared/submission/submissionLedger.d.ts +5 -1
- package/dist/shared/submission/submissionLedger.d.ts.map +1 -1
- package/dist/shared/submission/submissionLedger.js +33 -3
- package/dist/shared/submission/submissionLedger.js.map +1 -1
- package/dist/shared/tooling/commandShape.d.ts +29 -0
- package/dist/shared/tooling/commandShape.d.ts.map +1 -1
- package/dist/shared/tooling/commandShape.js +82 -10
- package/dist/shared/tooling/commandShape.js.map +1 -1
- package/dist/shared/tooling/exec.d.ts +32 -0
- package/dist/shared/tooling/exec.d.ts.map +1 -1
- package/dist/shared/tooling/exec.js +189 -0
- package/dist/shared/tooling/exec.js.map +1 -1
- package/docs/audit-pkg/contracts.md +2 -1
- package/docs/audit-pkg/operator-guide.md +12 -6
- package/docs/audit-pkg/product.md +22 -6
- package/package.json +13 -6
- package/schemas/audit-code-v1alpha1.schema.json +297 -0
- package/schemas/audit_result.schema.json +4 -0
- package/schemas/audit_results.schema.json +4 -0
- package/scripts/audit/postinstall.mjs +4 -2
- package/scripts/postinstall.mjs +1 -1
- package/scripts/remediate/postinstall.mjs +3 -1
- package/scripts/shared/install-host-assets.mjs +15 -8
- package/skills/audit-code/SKILL.md +4 -2
- package/skills/audit-code/audit-code.prompt.md +5 -2
- package/skills/audit-code/opencode-command-template.txt +1 -1
- package/wrapper/audit-code-wrapper-build.mjs +3 -3
- package/wrapper/audit-code-wrapper-install-hosts.mjs +3 -3
- package/wrapper/audit-code-wrapper-lib.mjs +82 -16
- package/wrapper/installer-verb-help.mjs +14 -4
- package/wrapper/remediate-code-wrapper-install-hosts.mjs +3 -3
- package/docs/audit-pkg/development.md +0 -113
- package/docs/audit-pkg/release.md +0 -140
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { isAbsolute
|
|
4
|
-
import { headCommit, commandLeavesDeclaredShape, FindingSchema, SUBMISSION_ISSUE_CODES, SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION, appendSubmissionEvent,
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { AUDIT_TOOLS_DIRNAME, headCommit, commandLeavesDeclaredShape, FindingSchema, SUBMISSION_ISSUE_CODES, SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION, appendSubmissionEvent, compareCodeUnits, contentSha256, hasExactKeys, hostHandoffResultPath, idsAreStrictlyAscending, isCommit, isGitRepo, isRecord, isSha256, normalizeRepoPath, parseAllWorkloadItems, parseCommandString, parseWorkloadEnvelope, promptSha256, readSubmissionDocument, readSubmissionLedger, repoRelativePath, resolveContainedPath, resolveHostHandoffPaths, resultIdentityIsBound, runTrackedAsync, sameStrings, stringArray, spawnSyncHidden, stableStringify, writeJsonFile, } from "audit-tools/shared";
|
|
5
4
|
import { RemediationHostHandoffRecordSchema, RemediationPlanSchema, isClarificationCategory, } from "../../state/types.js";
|
|
6
5
|
import { ITEM_STATUSES, isTerminalStatus, isVerifiedCompleteStatus, } from "../../state/itemStatus.js";
|
|
7
6
|
import { REMEDIATION_HOST_DECISION_CONTRACT_VERSION as DECISION_CONTRACT_VERSION, REMEDIATION_HOST_RESULT_CONTRACT_VERSION as RESULT_CONTRACT_VERSION, REMEDIATION_HOST_WORKLOAD_CONTRACT_VERSION as WORKLOAD_CONTRACT_VERSION, } from "../types.js";
|
|
@@ -95,31 +94,12 @@ const CURRENT_ITEM_KEYS = new Set([
|
|
|
95
94
|
"host_result_evidence",
|
|
96
95
|
"finding_id",
|
|
97
96
|
"incomplete_coverage_attempts",
|
|
98
|
-
"item_spec",
|
|
99
97
|
"last_successful_step",
|
|
100
98
|
"mechanical_verification",
|
|
101
99
|
"rework_count",
|
|
102
100
|
"started_at",
|
|
103
101
|
"status",
|
|
104
102
|
]);
|
|
105
|
-
function compareCodeUnits(left, right) {
|
|
106
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
107
|
-
}
|
|
108
|
-
function isRecord(value) {
|
|
109
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
110
|
-
}
|
|
111
|
-
function hasExactKeys(value, expected) {
|
|
112
|
-
const actual = Object.keys(value).sort(compareCodeUnits);
|
|
113
|
-
const canonicalExpected = [...expected].sort(compareCodeUnits);
|
|
114
|
-
return (actual.length === canonicalExpected.length &&
|
|
115
|
-
actual.every((key, index) => key === canonicalExpected[index]));
|
|
116
|
-
}
|
|
117
|
-
function isCommit(value) {
|
|
118
|
-
return typeof value === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(value);
|
|
119
|
-
}
|
|
120
|
-
function isSha256(value) {
|
|
121
|
-
return typeof value === "string" && /^[0-9a-f]{64}$/u.test(value);
|
|
122
|
-
}
|
|
123
103
|
function hasOnlyKnownKeys(value, known) {
|
|
124
104
|
return Object.keys(value).every((key) => known.has(key));
|
|
125
105
|
}
|
|
@@ -325,15 +305,19 @@ function cannotPrepareMessage(root, state, raised) {
|
|
|
325
305
|
return `Cannot prepare a remediation host workload: ${messages.join("; ")}`;
|
|
326
306
|
}
|
|
327
307
|
function resolveBoundaryPaths(params) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
308
|
+
// The remediate draw's run dir carries the `implement` lane segment — its
|
|
309
|
+
// runs directory also holds triage/closing lanes — which the core takes as a
|
|
310
|
+
// parameter rather than a fork.
|
|
311
|
+
const core = resolveHostHandoffPaths({
|
|
312
|
+
...params,
|
|
313
|
+
runDirSegments: ["implement"],
|
|
314
|
+
runIdLabel: "remediation host run id",
|
|
315
|
+
});
|
|
332
316
|
return {
|
|
333
|
-
root,
|
|
334
|
-
artifactsDir,
|
|
335
|
-
workloadPath:
|
|
336
|
-
resultDir:
|
|
317
|
+
root: core.root,
|
|
318
|
+
artifactsDir: core.artifactsDir,
|
|
319
|
+
workloadPath: core.workloadPath,
|
|
320
|
+
resultDir: core.resultDir,
|
|
337
321
|
};
|
|
338
322
|
}
|
|
339
323
|
/**
|
|
@@ -342,7 +326,13 @@ function resolveBoundaryPaths(params) {
|
|
|
342
326
|
* divergence between them would have been silent on both sides.
|
|
343
327
|
*/
|
|
344
328
|
function resultPathFor(paths, workItemId) {
|
|
345
|
-
return
|
|
329
|
+
return hostHandoffResultPath({
|
|
330
|
+
root: paths.root,
|
|
331
|
+
artifactsDir: paths.artifactsDir,
|
|
332
|
+
runDir: paths.workloadPath.slice(0, paths.workloadPath.length - "host-workload.json".length - 1),
|
|
333
|
+
resultDir: paths.resultDir,
|
|
334
|
+
workloadPath: paths.workloadPath,
|
|
335
|
+
}, workItemId);
|
|
346
336
|
}
|
|
347
337
|
/**
|
|
348
338
|
* Resolve the submission validator the INGEST applies to one work item, plus
|
|
@@ -478,7 +468,6 @@ function buildFindingAssignments(state, block) {
|
|
|
478
468
|
}
|
|
479
469
|
return {
|
|
480
470
|
finding: FindingSchema.parse(finding),
|
|
481
|
-
...(item.item_spec ? { item_spec: item.item_spec } : {}),
|
|
482
471
|
...(item.clarification_context
|
|
483
472
|
? { clarification_context: item.clarification_context }
|
|
484
473
|
: {}),
|
|
@@ -511,15 +500,12 @@ function buildWorkItem(paths, block, baselineCommit, state) {
|
|
|
511
500
|
finding_ids: [...block.items],
|
|
512
501
|
allowed_files: allowedFiles,
|
|
513
502
|
baseline_commit: baselineCommit,
|
|
514
|
-
prompt: { text: promptText, sha256:
|
|
503
|
+
prompt: { text: promptText, sha256: promptSha256(promptText) },
|
|
515
504
|
required_tests: requiredTests,
|
|
516
505
|
result_path: resultPath,
|
|
517
506
|
token_estimate: block.token_estimate ?? 0,
|
|
518
507
|
};
|
|
519
508
|
}
|
|
520
|
-
function hostWorkloadSha256(workload) {
|
|
521
|
-
return hashContent(stableStringify(workload));
|
|
522
|
-
}
|
|
523
509
|
function buildCanonicalWorkload(params) {
|
|
524
510
|
const requestedIds = params.workItemIds
|
|
525
511
|
? new Set(params.workItemIds)
|
|
@@ -566,7 +552,7 @@ function parseWorkItem(value, paths, state, expectedBaselineCommit) {
|
|
|
566
552
|
!hasExactKeys(value.prompt, ["sha256", "text"]) ||
|
|
567
553
|
typeof value.prompt.text !== "string" ||
|
|
568
554
|
!isSha256(value.prompt.sha256) ||
|
|
569
|
-
|
|
555
|
+
promptSha256(value.prompt.text) !== value.prompt.sha256) {
|
|
570
556
|
return null;
|
|
571
557
|
}
|
|
572
558
|
const block = state.plan.blocks.find((candidate) => candidate.block_id === value.id);
|
|
@@ -584,40 +570,32 @@ function parseWorkItem(value, paths, state, expectedBaselineCommit) {
|
|
|
584
570
|
: null;
|
|
585
571
|
}
|
|
586
572
|
function parseWorkload(value, paths, runId, state) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
573
|
+
// Envelope (keys, contract version, run id, items array) is the CORE's check.
|
|
574
|
+
const envelope = parseWorkloadEnvelope(value, {
|
|
575
|
+
contractVersion: WORKLOAD_CONTRACT_VERSION,
|
|
576
|
+
runId,
|
|
577
|
+
});
|
|
578
|
+
if (!envelope.ok)
|
|
592
579
|
return null;
|
|
593
|
-
}
|
|
594
580
|
const binding = state.host_handoff;
|
|
595
581
|
if (binding &&
|
|
596
582
|
(binding.run_id !== runId ||
|
|
597
|
-
|
|
583
|
+
contentSha256(value) !==
|
|
598
584
|
binding.workload_sha256)) {
|
|
599
585
|
return null;
|
|
600
586
|
}
|
|
601
|
-
const workItems =
|
|
602
|
-
if (workItems
|
|
587
|
+
const workItems = parseAllWorkloadItems(envelope.rawItems, (item) => parseWorkItem(item, paths, state, binding?.baseline_commit));
|
|
588
|
+
if (workItems === null)
|
|
603
589
|
return null;
|
|
604
590
|
const ids = workItems.map((item) => item.id);
|
|
605
|
-
|
|
606
|
-
|
|
591
|
+
// Strictly ascending covers BOTH "sorted" and "duplicate-free" — the two
|
|
592
|
+
// properties the byte-for-byte re-derivation comparison needs.
|
|
593
|
+
if (!idsAreStrictlyAscending(ids) ||
|
|
607
594
|
(binding !== undefined && !sameStrings(ids, binding.work_item_ids))) {
|
|
608
595
|
return null;
|
|
609
596
|
}
|
|
610
597
|
return value;
|
|
611
598
|
}
|
|
612
|
-
function stringArray(value) {
|
|
613
|
-
return Array.isArray(value) && value.every((entry) => typeof entry === "string")
|
|
614
|
-
? value
|
|
615
|
-
: null;
|
|
616
|
-
}
|
|
617
|
-
function sameStrings(left, right) {
|
|
618
|
-
return (left.length === right.length &&
|
|
619
|
-
left.every((entry, index) => entry === right[index]));
|
|
620
|
-
}
|
|
621
599
|
function invalidResult(reason) {
|
|
622
600
|
return { ok: false, reason };
|
|
623
601
|
}
|
|
@@ -631,11 +609,11 @@ function parseResult(value, runId, workItem) {
|
|
|
631
609
|
"prompt_sha256",
|
|
632
610
|
"outcome",
|
|
633
611
|
]) ||
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
612
|
+
!resultIdentityIsBound(value, {
|
|
613
|
+
runId,
|
|
614
|
+
workItemId: workItem.id,
|
|
615
|
+
promptSha256: workItem.prompt.sha256,
|
|
616
|
+
}) ||
|
|
639
617
|
!isRecord(value.outcome) ||
|
|
640
618
|
typeof value.outcome.status !== "string") {
|
|
641
619
|
return invalidResult("decision must match the exact current run, work-item, and prompt binding");
|
|
@@ -690,11 +668,11 @@ function parseResult(value, runId, workItem) {
|
|
|
690
668
|
"worktree_evidence",
|
|
691
669
|
]) ||
|
|
692
670
|
value.contract_version !== RESULT_CONTRACT_VERSION ||
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
671
|
+
!resultIdentityIsBound(value, {
|
|
672
|
+
runId,
|
|
673
|
+
workItemId: workItem.id,
|
|
674
|
+
promptSha256: workItem.prompt.sha256,
|
|
675
|
+
})) {
|
|
698
676
|
return invalidResult("result must match the exact current contract, run, work-item, and prompt binding");
|
|
699
677
|
}
|
|
700
678
|
const changedFiles = stringArray(value.changed_files);
|
|
@@ -797,6 +775,134 @@ function gitChangedFilesOfCommit(root, commit) {
|
|
|
797
775
|
return null;
|
|
798
776
|
return [...new Set((result.stdout ?? "").split("\0").filter(Boolean))].sort(compareCodeUnits);
|
|
799
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* Every repo-relative path the tree shows as touched since `baseline` — the
|
|
780
|
+
* commits baseline→HEAD, the working tree's own deviation from HEAD (staged and
|
|
781
|
+
* unstaged alike), and the untracked files git considers repository content.
|
|
782
|
+
*
|
|
783
|
+
* All three legs are needed to falsify a no-change claim, and they enumerate the
|
|
784
|
+
* three ways a host can have edited: committed (leg 1), edited a TRACKED file
|
|
785
|
+
* and left it uncommitted (leg 2), and CREATED a file (leg 3). A new `src/*.ts`
|
|
786
|
+
* is a real edit and the most natural shape a remediation takes; without leg 3
|
|
787
|
+
* the cheapest way to smuggle one past a no-change claim was simply never to
|
|
788
|
+
* `git add` it. `null` means git could not answer, which callers must treat as
|
|
789
|
+
* "cannot corroborate" rather than as "nothing changed".
|
|
790
|
+
*
|
|
791
|
+
* The untracked leg honours `--exclude-standard`, so it enumerates only what git
|
|
792
|
+
* itself treats as content — a repo's `.gitignore`d build and coverage output is
|
|
793
|
+
* already invisible to it. Two exemptions cover the remainder, both ground
|
|
794
|
+
* truth rather than the host's word:
|
|
795
|
+
*
|
|
796
|
+
* - THIS TOOL'S OWN ARTIFACT TREE ({@link AUDIT_TOOLS_DIRNAME}) is subtracted
|
|
797
|
+
* here, explicitly. In a real repository the tool writes a managed
|
|
798
|
+
* `.gitignore` block covering it, so it never reaches this probe at all; the
|
|
799
|
+
* explicit subtraction is what makes that independent of whether the block
|
|
800
|
+
* has been written yet, so a bare root (a fixture, a first run) cannot
|
|
801
|
+
* manufacture a false refusal out of the tool's own workload, prompt and
|
|
802
|
+
* result documents.
|
|
803
|
+
* - PRE-EXISTING untracked strays are excused by the caller's `excusedPaths`,
|
|
804
|
+
* for free: `run_start_dirty` is captured from `stagedAndUntracked` before
|
|
805
|
+
* any remediation edit exists, so it already enumerates untracked files.
|
|
806
|
+
* What survives both is an untracked file that appeared DURING the run — the
|
|
807
|
+
* only untracked class that can be this host's edit.
|
|
808
|
+
*/
|
|
809
|
+
function gitChangedFilesSince(root, baseline) {
|
|
810
|
+
const files = new Set();
|
|
811
|
+
for (const args of [
|
|
812
|
+
// baseline → HEAD: what the host committed.
|
|
813
|
+
["diff", "--name-only", "-z", baseline, "HEAD"],
|
|
814
|
+
// HEAD → working tree: what the host edited and did not commit.
|
|
815
|
+
["diff", "--name-only", "-z", "HEAD"],
|
|
816
|
+
// Never added: what the host CREATED. `--exclude-standard` keeps git's own
|
|
817
|
+
// ignore rules authoritative.
|
|
818
|
+
["ls-files", "--others", "--exclude-standard", "-z"],
|
|
819
|
+
]) {
|
|
820
|
+
const probe = spawnSyncHidden("git", args, {
|
|
821
|
+
cwd: root,
|
|
822
|
+
encoding: "utf8",
|
|
823
|
+
shell: false,
|
|
824
|
+
});
|
|
825
|
+
if (probe.error || probe.status !== 0)
|
|
826
|
+
return null;
|
|
827
|
+
for (const file of (probe.stdout ?? "").split("\0").filter(Boolean)) {
|
|
828
|
+
if (isAuditToolsArtifactPath(file))
|
|
829
|
+
continue;
|
|
830
|
+
files.add(file);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return [...files].sort(compareCodeUnits);
|
|
834
|
+
}
|
|
835
|
+
/** Whether a repo-relative path lives inside this tool's own artifact tree. */
|
|
836
|
+
function isAuditToolsArtifactPath(path) {
|
|
837
|
+
const normalized = normalizeRepoPath(path);
|
|
838
|
+
return (normalized === AUDIT_TOOLS_DIRNAME ||
|
|
839
|
+
normalized.startsWith(`${AUDIT_TOOLS_DIRNAME}/`));
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Corroborate an explicit `resolved_no_change` decision against the repository.
|
|
843
|
+
*
|
|
844
|
+
* A no-change decision used to be accepted on its evidence STRINGS alone, with
|
|
845
|
+
* only the required tests re-run — so a host that had in fact edited and then
|
|
846
|
+
* declared "nothing to do" was recorded as verified-no-change, and the edit
|
|
847
|
+
* rode into the run unattributed. The claim is mechanically falsifiable, so it
|
|
848
|
+
* is FALSIFIED.
|
|
849
|
+
*
|
|
850
|
+
* The scope of the falsification is the FULL write-scope corroboration every
|
|
851
|
+
* other acceptance path gets, NOT a narrowing to this item's `allowed_files`.
|
|
852
|
+
* `corroborateHostResult` refuses a landed commit that touched anything outside
|
|
853
|
+
* `allowed_files`; a no-change decision that narrowed the check to files INSIDE
|
|
854
|
+
* `allowed_files` would be the inverse rule — the out-of-scope edit, the more
|
|
855
|
+
* serious of the two, would be the one silently admitted. So EVERY path the
|
|
856
|
+
* tree shows as moved since the workload baseline refuses the claim.
|
|
857
|
+
*
|
|
858
|
+
* `excusedPaths` is the only exemption, and it is ground truth rather than the
|
|
859
|
+
* host's word: `run_start_dirty` (already dirty before the run began, so not
|
|
860
|
+
* evidence that this host edited anything — and because it is captured from
|
|
861
|
+
* `stagedAndUntracked`, it excuses pre-existing UNTRACKED strays too) unioned
|
|
862
|
+
* with the accepted edit surface — `applied_edit_surface` plus whatever this
|
|
863
|
+
* same ingest has already corroborated and accepted, so a sibling work item's
|
|
864
|
+
* legitimately landed files do not falsify this item's claim.
|
|
865
|
+
*
|
|
866
|
+
* Fails CLOSED, like every other corroboration here: a git that cannot answer
|
|
867
|
+
* refuses the claim rather than admitting it.
|
|
868
|
+
*/
|
|
869
|
+
function corroborateNoChangeClaim(params) {
|
|
870
|
+
const { root, workItem, excusedPaths } = params;
|
|
871
|
+
if (!isGitRepo(root))
|
|
872
|
+
return { ok: true };
|
|
873
|
+
const baseline = workItem.baseline_commit;
|
|
874
|
+
if (!gitCommitExists(root, baseline)) {
|
|
875
|
+
return {
|
|
876
|
+
ok: false,
|
|
877
|
+
code: "commit_missing",
|
|
878
|
+
message: "resolved_no_change cannot be corroborated: baseline_commit does not resolve to a real commit",
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const changed = gitChangedFilesSince(root, baseline);
|
|
882
|
+
if (changed === null) {
|
|
883
|
+
return {
|
|
884
|
+
ok: false,
|
|
885
|
+
code: "commit_missing",
|
|
886
|
+
message: "resolved_no_change cannot be corroborated: git could not enumerate the changes since the workload baseline",
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
const violating = changed.filter((path) => !excusedPaths.has(normalizeRepoPath(path)));
|
|
890
|
+
if (violating.length > 0) {
|
|
891
|
+
const inScope = violating.filter((path) => workItem.allowed_files.includes(path));
|
|
892
|
+
const outOfScope = violating.filter((path) => !workItem.allowed_files.includes(path));
|
|
893
|
+
return {
|
|
894
|
+
ok: false,
|
|
895
|
+
code: "changed_files_mismatch",
|
|
896
|
+
message: "resolved_no_change is contradicted by the tree — these files changed since the " +
|
|
897
|
+
`workload baseline: ${violating.join(", ")}` +
|
|
898
|
+
(outOfScope.length > 0
|
|
899
|
+
? ` (outside the prompt-bound allowed_files: ${outOfScope.join(", ")}` +
|
|
900
|
+
(inScope.length > 0 ? `; inside: ${inScope.join(", ")})` : ")")
|
|
901
|
+
: ""),
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
return { ok: true };
|
|
905
|
+
}
|
|
800
906
|
/**
|
|
801
907
|
* Per-command deadline. A required test is host-authored and may legitimately be
|
|
802
908
|
* a full suite, so the bound is generous; what changed is that hitting it is now
|
|
@@ -865,10 +971,19 @@ function requiredTestVerdictKey(root, command) {
|
|
|
865
971
|
* first-class outcome of this function, and an outcome that can only be reached
|
|
866
972
|
* by waiting ten real minutes is an outcome nothing ever tests.
|
|
867
973
|
*/
|
|
868
|
-
export function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT_MS) {
|
|
869
|
-
|
|
974
|
+
export async function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT_MS) {
|
|
975
|
+
// AWAITED, never `spawnSync`: ingestion runs with the remediation state lock
|
|
976
|
+
// held, and a synchronous child blocks the event loop for the whole suite —
|
|
977
|
+
// starving the lock's mtime heartbeat until a LIVE lock is classified stale
|
|
978
|
+
// and stolen mid-ingest.
|
|
979
|
+
//
|
|
980
|
+
// argv + `shell: false`, never a shell string. A required test is a workload
|
|
981
|
+
// command that already cleared the declared-shape gate at the producer, so
|
|
982
|
+
// splitting it is unambiguous, and dropping the shell removes the last place
|
|
983
|
+
// an ingest hands a declared string to `sh`/`cmd.exe`. `resolveExecArgv`
|
|
984
|
+
// inside the runner is what keeps the npm/npx shims resolvable on win32.
|
|
985
|
+
const result = await runTrackedAsync(parseCommandString(command), {
|
|
870
986
|
cwd: root,
|
|
871
|
-
shell: true,
|
|
872
987
|
// Captured, not discarded: without it a red ingest reports that something
|
|
873
988
|
// failed and nothing about why.
|
|
874
989
|
encoding: "utf8",
|
|
@@ -920,7 +1035,7 @@ export function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT
|
|
|
920
1035
|
// `status` with `signal` null — so this branch is unreachable on the platform
|
|
921
1036
|
// this repo runs its suites on, and no test exercises it. It is kept because
|
|
922
1037
|
// the runner is OS-agnostic by contract, not because it has been observed.
|
|
923
|
-
if (result.signal !== null) {
|
|
1038
|
+
if (result.signal !== null && result.signal !== undefined) {
|
|
924
1039
|
return {
|
|
925
1040
|
command,
|
|
926
1041
|
outcome: "failed",
|
|
@@ -941,7 +1056,7 @@ export function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT
|
|
|
941
1056
|
}
|
|
942
1057
|
return null;
|
|
943
1058
|
}
|
|
944
|
-
function rerunRequiredTests(root, commands,
|
|
1059
|
+
async function rerunRequiredTests(root, commands,
|
|
945
1060
|
/** `null` on the normal lane — see {@link RemediationRequiredTestVerdicts}. */
|
|
946
1061
|
verdicts) {
|
|
947
1062
|
const failures = [];
|
|
@@ -962,7 +1077,7 @@ verdicts) {
|
|
|
962
1077
|
}
|
|
963
1078
|
continue;
|
|
964
1079
|
}
|
|
965
|
-
const failure = runRequiredTest(root, command);
|
|
1080
|
+
const failure = await runRequiredTest(root, command);
|
|
966
1081
|
if (failure !== null)
|
|
967
1082
|
failures.push(failure);
|
|
968
1083
|
}
|
|
@@ -1028,11 +1143,11 @@ export async function precomputeRecoveryTestVerdicts(params) {
|
|
|
1028
1143
|
}
|
|
1029
1144
|
}
|
|
1030
1145
|
for (const command of commands) {
|
|
1031
|
-
verdicts.set(requiredTestVerdictKey(paths.root, command), runRequiredTest(paths.root, command));
|
|
1146
|
+
verdicts.set(requiredTestVerdictKey(paths.root, command), await runRequiredTest(paths.root, command));
|
|
1032
1147
|
}
|
|
1033
1148
|
return verdicts;
|
|
1034
1149
|
}
|
|
1035
|
-
function corroborateHostResult(params) {
|
|
1150
|
+
async function corroborateHostResult(params) {
|
|
1036
1151
|
const { root, state, workItem, result, verdicts } = params;
|
|
1037
1152
|
const baseline = workItem.baseline_commit;
|
|
1038
1153
|
const landed = result.commit_evidence.after;
|
|
@@ -1103,7 +1218,7 @@ function corroborateHostResult(params) {
|
|
|
1103
1218
|
message: `landed files overlap pre-existing run-start dirt: ${dirtyOverlap.join(", ")}`,
|
|
1104
1219
|
};
|
|
1105
1220
|
}
|
|
1106
|
-
const failedTests = rerunRequiredTests(root, workItem.required_tests, verdicts);
|
|
1221
|
+
const failedTests = await rerunRequiredTests(root, workItem.required_tests, verdicts);
|
|
1107
1222
|
if (failedTests.length > 0) {
|
|
1108
1223
|
const issue = requiredTestIssue(workItem, failedTests);
|
|
1109
1224
|
return { ok: false, code: issue.code, message: issue.message };
|
|
@@ -1163,7 +1278,7 @@ export async function prepareRemediationHostHandoff(params) {
|
|
|
1163
1278
|
.map((issue) => issue.message)
|
|
1164
1279
|
.join("; ")}`);
|
|
1165
1280
|
}
|
|
1166
|
-
const workloadDigest =
|
|
1281
|
+
const workloadDigest = contentSha256(workload);
|
|
1167
1282
|
if (existingRecord &&
|
|
1168
1283
|
existingRecord.workload_sha256 !== workloadDigest) {
|
|
1169
1284
|
throw new Error("Trusted remediation host workload no longer matches the persisted state binding");
|
|
@@ -1312,7 +1427,13 @@ export async function ingestRemediationHostResults(params) {
|
|
|
1312
1427
|
// the same acceptance.
|
|
1313
1428
|
let recordedRecoveryMarks = null;
|
|
1314
1429
|
const landedFiles = new Set(nextState.applied_edit_surface ?? []);
|
|
1315
|
-
|
|
1430
|
+
// Can this ingest be corroborated against ground truth AT ALL? A git root
|
|
1431
|
+
// supplies the tree; a persisted `host_handoff` supplies the trusted binding.
|
|
1432
|
+
// With NEITHER there is nothing to check a host's claim against, and the
|
|
1433
|
+
// remaining evidence is the host's own attestation — which is exactly the
|
|
1434
|
+
// claim under test. That branch is REFUSED for both result and decision
|
|
1435
|
+
// documents rather than admitted on the attestation alone.
|
|
1436
|
+
const canCorroborate = state.host_handoff !== undefined || isGitRepo(paths.root);
|
|
1316
1437
|
for (const workItem of workload.work_items) {
|
|
1317
1438
|
const pendingItems = workItem.finding_ids.filter((findingId) => nextState.items[findingId]?.status === "pending");
|
|
1318
1439
|
if (pendingItems.length === 0)
|
|
@@ -1372,7 +1493,36 @@ export async function ingestRemediationHostResults(params) {
|
|
|
1372
1493
|
const result = parsed.result;
|
|
1373
1494
|
const outcome = result.outcome;
|
|
1374
1495
|
if (outcome.status === "resolved_no_change") {
|
|
1375
|
-
|
|
1496
|
+
if (!canCorroborate) {
|
|
1497
|
+
issues.push({
|
|
1498
|
+
code: "trusted_binding_missing",
|
|
1499
|
+
work_item_id: workItem.id,
|
|
1500
|
+
result_path: workItem.result_path,
|
|
1501
|
+
message: "resolved_no_change needs a git root or a persisted host_handoff binding to corroborate the write scope against; attestation-only acceptance is refused",
|
|
1502
|
+
});
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
const noChange = corroborateNoChangeClaim({
|
|
1506
|
+
root: paths.root,
|
|
1507
|
+
workItem,
|
|
1508
|
+
// Ground truth only: pre-existing dirt plus the edit surface this run
|
|
1509
|
+
// has already corroborated and accepted (`landedFiles` starts from
|
|
1510
|
+
// `applied_edit_surface` and grows as this same ingest accepts).
|
|
1511
|
+
excusedPaths: new Set([
|
|
1512
|
+
...(state.run_start_dirty ?? []).map(normalizeRepoPath),
|
|
1513
|
+
...[...landedFiles].map(normalizeRepoPath),
|
|
1514
|
+
]),
|
|
1515
|
+
});
|
|
1516
|
+
if (!noChange.ok) {
|
|
1517
|
+
issues.push({
|
|
1518
|
+
code: noChange.code,
|
|
1519
|
+
work_item_id: workItem.id,
|
|
1520
|
+
result_path: workItem.result_path,
|
|
1521
|
+
message: noChange.message,
|
|
1522
|
+
});
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
const failedTests = await rerunRequiredTests(paths.root, workItem.required_tests, requiredTestVerdicts);
|
|
1376
1526
|
if (failedTests.length > 0) {
|
|
1377
1527
|
issues.push(requiredTestIssue(workItem, failedTests));
|
|
1378
1528
|
continue;
|
|
@@ -1414,76 +1564,82 @@ export async function ingestRemediationHostResults(params) {
|
|
|
1414
1564
|
continue;
|
|
1415
1565
|
}
|
|
1416
1566
|
const result = parsed.result;
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
result,
|
|
1424
|
-
verdicts: requiredTestVerdicts,
|
|
1425
|
-
recovery: params.recovery !== undefined,
|
|
1567
|
+
if (!canCorroborate) {
|
|
1568
|
+
issues.push({
|
|
1569
|
+
code: "trusted_binding_missing",
|
|
1570
|
+
work_item_id: workItem.id,
|
|
1571
|
+
result_path: workItem.result_path,
|
|
1572
|
+
message: "a landed result needs a git root or a persisted host_handoff binding to corroborate the write scope against; attestation-only acceptance is refused",
|
|
1426
1573
|
});
|
|
1427
|
-
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
const corroborated = await corroborateHostResult({
|
|
1577
|
+
root: paths.root,
|
|
1578
|
+
state,
|
|
1579
|
+
workItem,
|
|
1580
|
+
result,
|
|
1581
|
+
verdicts: requiredTestVerdicts,
|
|
1582
|
+
recovery: params.recovery !== undefined,
|
|
1583
|
+
});
|
|
1584
|
+
if (!corroborated.ok) {
|
|
1585
|
+
issues.push({
|
|
1586
|
+
code: corroborated.code,
|
|
1587
|
+
work_item_id: workItem.id,
|
|
1588
|
+
result_path: workItem.result_path,
|
|
1589
|
+
message: corroborated.message,
|
|
1590
|
+
});
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
if (corroborated.usedRecovery) {
|
|
1594
|
+
// No acceptance without a record. The mark goes down BEFORE the item is
|
|
1595
|
+
// marked resolved, and an append that throws refuses this item rather
|
|
1596
|
+
// than landing an acceptance the ledger cannot account for — the run
|
|
1597
|
+
// must never read as one that never drifted. The refusal is per item:
|
|
1598
|
+
// an unwritable ledger is not a reason to discard the whole ingest.
|
|
1599
|
+
try {
|
|
1600
|
+
recordedRecoveryMarks ??= (await readSubmissionLedger(paths.artifactsDir)).filter((event) => event.kind === "accepted_via_recovery");
|
|
1601
|
+
// The mark's identity is (run, item, LANDED COMMIT), not just
|
|
1602
|
+
// (run, item): an item re-opened and later re-accepted from a
|
|
1603
|
+
// DIFFERENT landing is a different relaxed acceptance and earns its
|
|
1604
|
+
// own record. Only a retry of the SAME landing is a duplicate. The
|
|
1605
|
+
// landed sha is matched inside the message because the shared event
|
|
1606
|
+
// contract carries no commit field, and a 40-hex sha this writer
|
|
1607
|
+
// itself emitted is an unambiguous token to match on.
|
|
1608
|
+
const landedCommit = result.commit_evidence.after;
|
|
1609
|
+
const alreadyMarked = recordedRecoveryMarks.some((event) => event.run_id === params.runId &&
|
|
1610
|
+
event.submission_id === workItem.id &&
|
|
1611
|
+
(event.message ?? "").includes(landedCommit));
|
|
1612
|
+
if (!alreadyMarked) {
|
|
1613
|
+
const event = {
|
|
1614
|
+
contract_version: SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION,
|
|
1615
|
+
run_id: params.runId,
|
|
1616
|
+
submission_id: workItem.id,
|
|
1617
|
+
lane: workItem.id,
|
|
1618
|
+
kind: "accepted_via_recovery",
|
|
1619
|
+
// Derived from what was actually probed, never asserted: the
|
|
1620
|
+
// baseline was found in no ref and unreachable from HEAD.
|
|
1621
|
+
message: `accepted under recovery: the trusted baseline ${workItem.baseline_commit} is ` +
|
|
1622
|
+
"contained by no ref and unreachable from HEAD, so landed commit " +
|
|
1623
|
+
`${landedCommit} was corroborated against HEAD and this ` +
|
|
1624
|
+
"item's bound scope instead of against baseline ancestry",
|
|
1625
|
+
recorded_at: new Date().toISOString(),
|
|
1626
|
+
};
|
|
1627
|
+
await appendSubmissionEvent(paths.artifactsDir, event);
|
|
1628
|
+
recordedRecoveryMarks.push(event);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
catch (error) {
|
|
1428
1632
|
issues.push({
|
|
1429
|
-
code:
|
|
1633
|
+
code: "recovery_unrecorded",
|
|
1430
1634
|
work_item_id: workItem.id,
|
|
1431
1635
|
result_path: workItem.result_path,
|
|
1432
|
-
message:
|
|
1636
|
+
message: "the recovery acceptance could not be recorded on the submission ledger, so it " +
|
|
1637
|
+
`was refused: ${error instanceof Error ? error.message : String(error)}`,
|
|
1433
1638
|
});
|
|
1434
1639
|
continue;
|
|
1435
1640
|
}
|
|
1436
|
-
if (corroborated.usedRecovery) {
|
|
1437
|
-
// No acceptance without a record. The mark goes down BEFORE the item is
|
|
1438
|
-
// marked resolved, and an append that throws refuses this item rather
|
|
1439
|
-
// than landing an acceptance the ledger cannot account for — the run
|
|
1440
|
-
// must never read as one that never drifted. The refusal is per item:
|
|
1441
|
-
// an unwritable ledger is not a reason to discard the whole ingest.
|
|
1442
|
-
try {
|
|
1443
|
-
recordedRecoveryMarks ??= (await readSubmissionLedger(paths.artifactsDir)).filter((event) => event.kind === "accepted_via_recovery");
|
|
1444
|
-
// The mark's identity is (run, item, LANDED COMMIT), not just
|
|
1445
|
-
// (run, item): an item re-opened and later re-accepted from a
|
|
1446
|
-
// DIFFERENT landing is a different relaxed acceptance and earns its
|
|
1447
|
-
// own record. Only a retry of the SAME landing is a duplicate. The
|
|
1448
|
-
// landed sha is matched inside the message because the shared event
|
|
1449
|
-
// contract carries no commit field, and a 40-hex sha this writer
|
|
1450
|
-
// itself emitted is an unambiguous token to match on.
|
|
1451
|
-
const landedCommit = result.commit_evidence.after;
|
|
1452
|
-
const alreadyMarked = recordedRecoveryMarks.some((event) => event.run_id === params.runId &&
|
|
1453
|
-
event.submission_id === workItem.id &&
|
|
1454
|
-
(event.message ?? "").includes(landedCommit));
|
|
1455
|
-
if (!alreadyMarked) {
|
|
1456
|
-
const event = {
|
|
1457
|
-
contract_version: SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION,
|
|
1458
|
-
run_id: params.runId,
|
|
1459
|
-
submission_id: workItem.id,
|
|
1460
|
-
lane: workItem.id,
|
|
1461
|
-
kind: "accepted_via_recovery",
|
|
1462
|
-
// Derived from what was actually probed, never asserted: the
|
|
1463
|
-
// baseline was found in no ref and unreachable from HEAD.
|
|
1464
|
-
message: `accepted under recovery: the trusted baseline ${workItem.baseline_commit} is ` +
|
|
1465
|
-
"contained by no ref and unreachable from HEAD, so landed commit " +
|
|
1466
|
-
`${landedCommit} was corroborated against HEAD and this ` +
|
|
1467
|
-
"item's bound scope instead of against baseline ancestry",
|
|
1468
|
-
recorded_at: new Date().toISOString(),
|
|
1469
|
-
};
|
|
1470
|
-
await appendSubmissionEvent(paths.artifactsDir, event);
|
|
1471
|
-
recordedRecoveryMarks.push(event);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
catch (error) {
|
|
1475
|
-
issues.push({
|
|
1476
|
-
code: "recovery_unrecorded",
|
|
1477
|
-
work_item_id: workItem.id,
|
|
1478
|
-
result_path: workItem.result_path,
|
|
1479
|
-
message: "the recovery acceptance could not be recorded on the submission ledger, so it " +
|
|
1480
|
-
`was refused: ${error instanceof Error ? error.message : String(error)}`,
|
|
1481
|
-
});
|
|
1482
|
-
continue;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
acceptedFiles = corroborated.changedFiles;
|
|
1486
1641
|
}
|
|
1642
|
+
const acceptedFiles = corroborated.changedFiles;
|
|
1487
1643
|
const completedAt = new Date().toISOString();
|
|
1488
1644
|
for (const findingId of pendingItems) {
|
|
1489
1645
|
const item = nextState.items[findingId];
|