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