Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ This project uses a Keep a Changelog-style structure. Until a tagged release is

- HTTP target observation always runs through the logged, size-bounded network session under the saved run policy. The second retrieval path, reachable only by direct library callers, did not reject content encodings, cap total bytes or keep a request log; it was removed. No CLI command used it.

- Commands that validate a run and then write now confirm every file the validation read through one shared check, `assertValidatedRunStable`, instead of each carrying its own loops. The copies covered registered artifacts and saved evidence but only two also covered the historical resources used to validate older run formats, so the other commands could write after one of those files changed. All commands now share one list, and a test rejects new hand-written copies.

- Removed internal plans, implementation reviews, status notes, and research source copies from the public source tree; retained official source links and attribution.

- Reviewed the Digital Agency accessibility policy against the monitored source on 2026-09-25. The 18 additional criterion mappings and routing are unchanged; the catalog source hash, review date and attribution binding now match the reviewed page.
Expand Down
1 change: 1 addition & 0 deletions release-files.json
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@
"tests/target-observer.test.mjs",
"tests/target-registration.test.mjs",
"tests/unified-cli.test.mjs",
"tests/validated-run-stability.test.mjs",
"tests/web-capabilities-e2e.test.mjs",
"tests/web-capabilities.test.mjs",
"tests/web-evidence-adapter.test.mjs",
Expand Down
6 changes: 3 additions & 3 deletions shared/skill/scripts/bind-audit-evidence.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import { assertNewOutputPath, assertStableFile, readStableFile, resolveInside, validateAuditRun, validateArtifact, writeNewJson } from "./lib/audit-run.mjs";
import { assertNewOutputPath, assertStableFile, assertValidatedRunStable, readStableFile, resolveInside, validateAuditRun, validateArtifact, writeNewJson } from "./lib/audit-run.mjs";
import { createRunEvidenceReference, collectScreeningEvidence } from "./lib/run-evidence.mjs";

function parseArgs(argv) {
Expand Down Expand Up @@ -55,8 +55,8 @@ export function main(argv = process.argv.slice(2)) {
assertStableFile(runSnapshot, "audit run");
assertStableFile(artifactSnapshot, "draft artifact");
assertStableFile(snapshot, "raw evidence");
for (const { snapshot: registered } of validation.envelopesById.values()) assertStableFile(registered, "registered artifact");
for (const raw of [...validation.evidenceSnapshots.values(), ...evidence.snapshots.values()]) assertStableFile(raw, "raw evidence");
assertValidatedRunStable(validation);
for (const raw of evidence.snapshots.values()) assertStableFile(raw, "raw evidence");
writeNewJson(output, artifact);
process.stdout.write(`${JSON.stringify({ status: "PASS", output, observation: options.observation, evidence_count: observation.evidence_refs.length })}\n`);
}
Expand Down
5 changes: 2 additions & 3 deletions shared/skill/scripts/capture-run-targets.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import { assertNewOutputPath, assertStableFile, readStableFile, validateAuditRun, writeNewJson } from "./lib/audit-run.mjs";
import { assertNewOutputPath, assertStableFile, assertValidatedRunStable, readStableFile, validateAuditRun, writeNewJson } from "./lib/audit-run.mjs";
import { observeRunTargetsWithEvidence } from "./lib/run-targets.mjs";
import { afterChangeRun } from "./lib/declared-change.mjs";
import { networkPolicyHash } from "./lib/network-policy.mjs";
Expand Down Expand Up @@ -66,8 +66,7 @@ export async function main(argv = process.argv.slice(2)) {
const { inventory, networkEvidence } = captured;
assertStableFile(runSnapshot, "audit run input");
assertStableFile(specsSnapshot, "target specifications");
for (const { snapshot } of validation.envelopesById.values()) assertStableFile(snapshot, "registered artifact");
for (const snapshot of validation.evidenceSnapshots.values()) assertStableFile(snapshot, "raw evidence");
assertValidatedRunStable(validation);
if (networkOutput) writeNewJson(networkOutput, { schema_version: "1.0.0", kind: "run-network-evidence", run_id: run.run_id,
network_policy_sha256: needsNetwork ? networkPolicyHash(run.permissions.network_policy) : null,
publication: "private_by_default", observations: networkEvidence });
Expand Down
5 changes: 2 additions & 3 deletions shared/skill/scripts/compare-audit-evidence.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import { assertNewOutputPath, assertStableFile, readStableFile, validateAuditRun, writeNewJson } from "./lib/audit-run.mjs";
import { assertNewOutputPath, assertStableFile, assertValidatedRunStable, readStableFile, validateAuditRun, writeNewJson } from "./lib/audit-run.mjs";
import { compareEvidenceReferences } from "./lib/run-evidence.mjs";

export function main(argv = process.argv.slice(2)) {
Expand Down Expand Up @@ -28,8 +28,7 @@ export function main(argv = process.argv.slice(2)) {
comparisons: compareEvidenceReferences(before.rows, after.rows) };
for (const input of inputs) {
assertStableFile(input.snapshot, "comparison run");
for (const { snapshot } of input.validation.envelopesById.values()) assertStableFile(snapshot, "registered artifact");
for (const raw of input.validation.evidenceSnapshots.values()) assertStableFile(raw, "raw evidence");
assertValidatedRunStable(input.validation);
}
writeNewJson(options["--output"], result);
process.stdout.write(`${JSON.stringify({ status: "PASS", output: options["--output"], comparisons: result.comparisons.length })}\n`);
Expand Down
5 changes: 2 additions & 3 deletions shared/skill/scripts/compare-audit-runs.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import { assertNewOutputPath, assertStableFile, readStableFile, validateAuditRun, writeNewJson, writeNewText } from "./lib/audit-run.mjs";
import { assertNewOutputPath, assertStableFile, assertValidatedRunStable, readStableFile, validateAuditRun, writeNewJson, writeNewText } from "./lib/audit-run.mjs";
import { buildRetestDelta, renderRetestDelta } from "./lib/retest-delta.mjs";

function parseOptions(argv) {
Expand Down Expand Up @@ -45,8 +45,7 @@ export function main(argv = process.argv.slice(2)) {
const assertInputsStable = () => {
for (const input of [before, after]) {
assertStableFile(input.snapshot, "comparison run");
for (const { snapshot } of input.validation.envelopesById.values()) assertStableFile(snapshot, "registered artifact");
for (const snapshot of input.validation.evidenceSnapshots.values()) assertStableFile(snapshot, "raw evidence");
assertValidatedRunStable(input.validation);
}
if (mappingSnapshot) assertStableFile(mappingSnapshot, "comparison mapping");
};
Expand Down
5 changes: 2 additions & 3 deletions shared/skill/scripts/compare-run-targets.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import { assertNewOutputPath, assertStableFile, readStableFile, validateAuditRun, writeNewJson } from "./lib/audit-run.mjs";
import { assertNewOutputPath, assertStableFile, assertValidatedRunStable, readStableFile, validateAuditRun, writeNewJson } from "./lib/audit-run.mjs";
import { compareRunTargets } from "./lib/run-targets.mjs";

export function main(argv = process.argv.slice(2)) {
Expand Down Expand Up @@ -29,8 +29,7 @@ export function main(argv = process.argv.slice(2)) {
const comparisons = compareRunTargets(before.run, before.run.target_inventory, after.run, after.run.target_inventory);
for (const input of inputs) {
assertStableFile(input.snapshot, "comparison run");
for (const { snapshot } of input.validation.envelopesById.values()) assertStableFile(snapshot, "registered artifact");
for (const snapshot of input.validation.evidenceSnapshots.values()) assertStableFile(snapshot, "raw evidence");
assertValidatedRunStable(input.validation);
}
writeNewJson(output, { schema_version: "1.0.0", publication: "private_by_default", before_run_id: before.run.run_id,
after_run_id: after.run.run_id, comparisons, limitation: "Measured identity differences do not establish accessibility improvement. This comparison does not contact live targets." });
Expand Down
9 changes: 3 additions & 6 deletions shared/skill/scripts/create-audit-artifact.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import crypto from "node:crypto";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { assertNewOutputPath, assertStableFile, readStableFile, resolveInside, validateAuditRun, validateArtifactCandidate, writeNewJson } from "./lib/audit-run.mjs";
import { assertNewOutputPath, assertStableFile, readStableFile, resolveInside, validateAuditRun, validatedRunSnapshots, validateArtifactCandidate, writeNewJson } from "./lib/audit-run.mjs";
import { parseAttestationJson } from "./lib/attestation-canonical.mjs";
import { createRunEvidenceReference } from "./lib/run-evidence.mjs";

Expand Down Expand Up @@ -34,11 +34,8 @@ export function main(argv = process.argv.slice(2)) {
const runFile = path.resolve(options.run), run = read(runFile);
const validation = validateAuditRun(run, { runFile });
if (!validation.valid) throw new Error(validation.errors.join("\n"));
for (const { snapshot } of validation.envelopesById.values()) {
parseAttestationJson(snapshot.bytes);
snapshots.push(snapshot);
}
snapshots.push(...validation.evidenceSnapshots.values());
for (const { snapshot } of validation.envelopesById.values()) parseAttestationJson(snapshot.bytes);
snapshots.push(...validatedRunSnapshots(validation));
let artifact, output;
if (action === "init") {
if (!types.includes(options.type)) throw new Error(`--type must be one of: ${types.join(", ")}`);
Expand Down
5 changes: 2 additions & 3 deletions shared/skill/scripts/create-human-review-queue.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import { assertNewOutputPath, assertStableFile, readStableFile, validateAuditRun, validateArtifact, writeNewJson } from "./lib/audit-run.mjs";
import { assertNewOutputPath, assertStableFile, assertValidatedRunStable, readStableFile, validateAuditRun, validateArtifact, writeNewJson } from "./lib/audit-run.mjs";
import { createHumanReviewQueue, queueContextErrors } from "./lib/human-review-queue.mjs";

export function main(argv = process.argv.slice(2)) {
Expand Down Expand Up @@ -39,8 +39,7 @@ export function main(argv = process.argv.slice(2)) {
const errors = [...schema.errors, ...queueContextErrors(run, new Map([...validation.envelopesById, [artifact.artifact_id, artifact]]), profileIds)];
if (errors.length) throw new Error(errors.join("\n"));
assertStableFile(snapshot, "audit run");
for (const record of validation.envelopesById.values()) assertStableFile(record.snapshot, "registered artifact");
for (const evidence of validation.evidenceSnapshots.values()) assertStableFile(evidence, "saved evidence");
assertValidatedRunStable(validation, { evidence: "saved evidence" });
writeNewJson(output, artifact);
process.stdout.write(`${JSON.stringify({ status: "candidate", output, items: artifact.payload.items.length, priority: "unprioritized" })}\n`);
}
Expand Down
4 changes: 2 additions & 2 deletions shared/skill/scripts/human-review-worksheet.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "node:path";
import { randomUUID } from "node:crypto";
import { assertNewOutputPath, assertStableFile, readStableFile, validateAuditRun, writeNewBytes, validateHumanReviewCandidate } from "./lib/audit-run.mjs";
import { assertNewOutputPath, assertStableFile, readStableFile, validateAuditRun, validatedRunSnapshots, writeNewBytes, validateHumanReviewCandidate } from "./lib/audit-run.mjs";
import { parseAttestationJson } from "./lib/attestation-canonical.mjs";
import { worksheetItems, createWorksheetRows, encodeCsv, decodeCsv, encodeMarkdown, decodeMarkdown, reviewFromWorksheet, worksheetLimit } from "./lib/human-review-worksheet.mjs";
import { compareInstants } from "./lib/date-time.mjs";
Expand Down Expand Up @@ -43,7 +43,7 @@ export async function runWorksheetCommand(argv) {
const relative = path.relative(validation.artifactRoot, output);
if (!relative || path.isAbsolute(relative) || relative.split(path.sep).includes("..")) throw new Error("Worksheet and artifact outputs must be within the private artifact root.");
assertNewOutputPath(output);
const inputs = [snapshot, ...[...validation.envelopesById.values()].map((entry) => entry.snapshot), ...validation.evidenceSnapshots.values()];
const inputs = [snapshot, ...validatedRunSnapshots(validation)];
const assertInputs = () => { for (const input of inputs) assertStableFile(input, "worksheet input"); };
if (options.command === "export") {
const bytes = options.format === "xlsx" ? await (await import("./lib/human-review-xlsx.mjs")).encodeXlsx(expected)
Expand Down
6 changes: 3 additions & 3 deletions shared/skill/scripts/human-review.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import { readStableFile, assertStableFile, writeNewJson, validateAuditRun, loadAuditResources } from "./lib/audit-run.mjs";
import { readStableFile, assertStableFile, writeNewJson, validateAuditRun, validatedRunSnapshots, loadAuditResources } from "./lib/audit-run.mjs";
import { validateAssessment } from "./validate-assessment.mjs";
import { parseAttestationJson } from "./lib/attestation-canonical.mjs";
import { createAttestationTrust } from "./lib/attestation-verifier.mjs";
Expand Down Expand Up @@ -85,8 +85,8 @@ export function runHumanReviewCommand(argv) {
const source = validation.envelopesById.get(options["--artifact-id"]);
if (!source || source.envelope.artifact_type !== "declared-human-review") throw new Error("--artifact-id must select a registered declared-human-review in the supplied run.");
// Every registered artifact must have an unambiguous JSON representation.
for (const entry of validation.envelopesById.values()) { parseAttestationJson(entry.snapshot.bytes); snapshots.push(entry.snapshot); }
snapshots.push(...validation.evidenceSnapshots.values());
for (const entry of validation.envelopesById.values()) parseAttestationJson(entry.snapshot.bytes);
snapshots.push(...validatedRunSnapshots(validation));
context = humanReviewRunContext({ run, artifact: source.envelope, artifactSha256: source.snapshot.sha256 });
sourceArtifactBytes = source.snapshot.bytes;
review = source.envelope.payload;
Expand Down
5 changes: 2 additions & 3 deletions shared/skill/scripts/import-scanner-results.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import { assertNewOutputPath, assertStableFile, readStableFile, resolveInside, validateAuditRun, validateArtifact, writeNewJson } from "./lib/audit-run.mjs";
import { assertNewOutputPath, assertStableFile, assertValidatedRunStable, readStableFile, resolveInside, validateAuditRun, validateArtifact, writeNewJson } from "./lib/audit-run.mjs";
import { buildAxeImport, importedScreeningArtifact } from "./lib/scanner-import.mjs";
import { createRunEvidenceReference, collectScreeningEvidence } from "./lib/run-evidence.mjs";
import { targetSpecification } from "./lib/run-targets.mjs";
Expand Down Expand Up @@ -65,8 +65,7 @@ export function main(argv = process.argv.slice(2)) {
assertStableFile(runSnapshot, "audit run");
assertStableFile(inputSnapshot, "raw scanner result");
if (configSnapshot) assertStableFile(configSnapshot, "scanner configuration");
for (const { snapshot } of validation.envelopesById.values()) assertStableFile(snapshot, "registered artifact");
for (const snapshot of validation.evidenceSnapshots.values()) assertStableFile(snapshot, "saved evidence");
assertValidatedRunStable(validation, { evidence: "saved evidence" });
const current = observeLocalTarget(targetSpecification(targetSnapshot), { baseDir: path.dirname(runFile) }).snapshot;
if (current.snapshot_id !== targetSnapshot.snapshot_id) throw new Error("Target drift in the saved scanner capture.");
writeNewJson(recordFile, record);
Expand Down
21 changes: 20 additions & 1 deletion shared/skill/scripts/lib/audit-run-validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { validateParticipantBindings, collectParticipantEvidence } from "./parti
import { validateDeclaredChangeBindings, collectDeclaredChangeEvidence } from "./declared-change.mjs";
import { collectFixExecutionEvidence } from "./fix-execution-evidence.mjs";
import { targetBindingErrors } from "./run-targets.mjs";
import { pathKey, parseJsonBytes, readStableFile, resolveInside } from "./safe-file-io.mjs";
import { pathKey, parseJsonBytes, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs";
import { defaultSkillRoot, loadAuditResources } from "./audit-resources.mjs";
import { auditRunRegistryCompatibility, auditRunEnvelopeCompatibility } from "./orchestration-contracts.mjs";
import { artifactRootFor, validateArtifact, registeredArtifactPath, canonicalPermissions, remediationPermissionError, validateRegisteredArtifactEntries, validateArtifactEnvelopeSemantics, validateHumanQueueBindings, validateScreeningProfileBindings, validateDeclaredHumanBindings, validateRemediationBindings } from "./audit-artifact-validation.mjs";
Expand Down Expand Up @@ -224,3 +224,22 @@ export function validateAuditRun(run, { skillRoot = defaultSkillRoot, runFile, r
return { valid: errors.length === 0, errors, resources: runResources, artifactRoot, envelopesById, historicalResourceSnapshots,
evidenceSnapshots: new Map([...evidence.snapshots, ...contextEvidence.snapshots, ...participantEvidence.snapshots, ...changeEvidence.snapshots, ...fixEvidence.snapshots]) };
}

// The one list of files validateAuditRun read. A command that writes after validating must confirm each of them still
// holds the validated bytes. Use the two functions below instead of looping over the collections by hand: a collection
// missing from a hand-written loop is a time-of-check to time-of-use gap, and a new collection is added only here.
function* validatedRunInputs(validation) {
for (const snapshot of validation.historicalResourceSnapshots ?? []) yield { kind: "historical", snapshot };
for (const { snapshot } of validation.envelopesById?.values() ?? []) yield { kind: "artifact", snapshot };
for (const snapshot of validation.evidenceSnapshots?.values() ?? []) yield { kind: "evidence", snapshot };
}

export function validatedRunSnapshots(validation) {
return [...validatedRunInputs(validation)].map(({ snapshot }) => snapshot);
}

// Labels name the input in the failure message; pass { artifact, evidence, historical } to override the defaults.
export function assertValidatedRunStable(validation, labels = {}) {
const names = { artifact: "registered artifact", evidence: "raw evidence", historical: "historical resource", ...labels };
for (const { kind, snapshot } of validatedRunInputs(validation)) assertStableFile(snapshot, names[kind]);
}
7 changes: 3 additions & 4 deletions shared/skill/scripts/lib/audit-run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import { pathKey, isWithinPath, hasTraversal, inspectRealComponents, parseJsonBy
import { defaultSkillRoot, loadAuditResources } from "./audit-resources.mjs";
import { compareText } from "./text-order.mjs";
import { roleFor, validateArtifact, registeredArtifactPath, canonicalPermissions, remediationPermissionError, validateFixHandoffBinding, validateChangeRecordAuthorizationBinding, assertCurrentOperationalRun } from "./audit-artifact-validation.mjs";
import { validateAuditRun } from "./audit-run-validation.mjs";
import { validateAuditRun, assertValidatedRunStable } from "./audit-run-validation.mjs";

export { canonicalJson } from "./canonical-json.mjs";
export { inspectRealComponents, prepareSafeOutputDirectory, assertNewOutputPath, writeNewJson, writeNewText, writeNewBytes, sha256Bytes, sha256File, readStableFile, assertStableFile, resolveInside } from "./safe-file-io.mjs";
export { loadAuditResources, defaultSkillRoot } from "./audit-resources.mjs";
export { validateArtifact, validateArtifactCandidate, validateHumanReviewCandidate } from "./audit-artifact-validation.mjs";
export { validateAuditRun } from "./audit-run-validation.mjs";
export { validateAuditRun, assertValidatedRunStable, validatedRunSnapshots } from "./audit-run-validation.mjs";
export { mergeArtifacts } from "./audit-run-merge.mjs";

function normalizePermission(value, aliases, name) {
Expand Down Expand Up @@ -257,7 +257,6 @@ export function registerArtifact(run, artifact, options = {}) {
const nextValidation = validateAuditRun(next, { skillRoot, runFile: options.runFile });
if (!nextValidation.valid) throw new Error(`Registered run is invalid:\n- ${nextValidation.errors.join("\n- ")}`);
assertStableFile(snapshot, "artifact file");
for (const { snapshot: registeredSnapshot } of nextValidation.envelopesById.values()) assertStableFile(registeredSnapshot, "registered artifact");
for (const evidenceSnapshot of nextValidation.evidenceSnapshots.values()) assertStableFile(evidenceSnapshot, "raw evidence");
assertValidatedRunStable(nextValidation);
return next;
}
Loading
Loading