audit-tools 0.43.0 → 0.44.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/dist/audit/cli/laneSubmissions.d.ts +2 -2
- package/dist/audit/cli/laneSubmissions.js +2 -2
- package/dist/audit/cli/nextStepHelpers.d.ts +0 -36
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +0 -76
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/remediate/phases/triage.d.ts.map +1 -1
- package/dist/remediate/phases/triage.js +14 -2
- package/dist/remediate/phases/triage.js.map +1 -1
- package/dist/remediate/steps/contractPipeline.d.ts +9 -0
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +144 -36
- package/dist/remediate/steps/contractPipeline.js.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.js +6 -62
- package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
- package/dist/remediate/steps/types.d.ts +0 -2
- package/dist/remediate/steps/types.d.ts.map +1 -1
- package/dist/remediate/steps/types.js +0 -2
- package/dist/remediate/steps/types.js.map +1 -1
- package/dist/remediate/validation/artifacts.d.ts +0 -2
- package/dist/remediate/validation/artifacts.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.js +1 -133
- package/dist/remediate/validation/artifacts.js.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +5 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/tooling/commandShape.d.ts +21 -0
- package/dist/shared/tooling/commandShape.d.ts.map +1 -0
- package/dist/shared/tooling/commandShape.js +120 -0
- package/dist/shared/tooling/commandShape.js.map +1 -0
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { existsSync } from "node:fs";
|
|
23
23
|
import { mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
24
24
|
import { isAbsolute, join, resolve } from "node:path";
|
|
25
|
-
import { writeJsonFile, readOptionalJsonFile, formatValidationIssues, hashContent, isRecord, withFsRetry, projectApprovedFindings, captureStepBoundaryFriction, climbOutOfAuditTools, estimateTokensFromBytes, normalizeRepoPath, repoRelativePath, } from "audit-tools/shared";
|
|
25
|
+
import { writeJsonFile, readOptionalJsonFile, formatValidationIssues, hashContent, isRecord, withFsRetry, projectApprovedFindings, captureStepBoundaryFriction, climbOutOfAuditTools, partitionCommandsByDeclaredShape, estimateTokensFromBytes, normalizeRepoPath, repoRelativePath, } from "audit-tools/shared";
|
|
26
26
|
import { createStepEmissionScaffold, } from "../../shared/steps/stepEmissionScaffold.js";
|
|
27
27
|
import { counterexampleFingerprint } from "../contractPipeline/counterexampleFingerprint.js";
|
|
28
28
|
import { CP_ARTIFACT_NAMES, contractArtifactExists, contractArtifactFilePath, contractInputFilePath, contractPipelineDir, detectStaleArtifacts, envelopePayload, envelopeSemanticHash, isEnvelope, pathASeedFilePath, payloadSemanticHash, readContractArtifact, stampToolCreatedAt, writeContractArtifact, writeDerivedContractArtifact, } from "../contractPipeline/artifactStore.js";
|
|
@@ -930,11 +930,106 @@ async function readDecomposedModules(artifactsDir) {
|
|
|
930
930
|
return result;
|
|
931
931
|
}
|
|
932
932
|
/**
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
|
|
933
|
+
* Characters that disqualify a finalized-contract entry from being read as a
|
|
934
|
+
* repo-relative write target: any whitespace (prose), `:` (the `artifact:<name>`
|
|
935
|
+
* ordering token and the Windows drive form), and the glob/redirect set no
|
|
936
|
+
* legal declared path carries.
|
|
937
|
+
*/
|
|
938
|
+
const NON_WRITE_TARGET_CHARS = /[\s:*?"<>|]/u;
|
|
939
|
+
/**
|
|
940
|
+
* A finalized module contract's `outputs` / `side_effects` are FREE PROSE that
|
|
941
|
+
* may name a file ("src/foo.ts") or may describe an effect ("writes the run
|
|
942
|
+
* ledger under .audit-tools") or carry an ordering token
|
|
943
|
+
* ("artifact:validated-roster" — see ARTIFACT_TOKEN_PATTERN in phaseCut.ts).
|
|
944
|
+
* Only the first kind is a write target, so this is a deliberately CONSERVATIVE
|
|
945
|
+
* parse: an entry qualifies only when it reads unambiguously as a repo-relative
|
|
946
|
+
* path, and everything else is silently dropped — prose stays prose. A false
|
|
947
|
+
* positive here would widen a worker's write scope on the strength of a
|
|
948
|
+
* sentence, which is strictly worse than the manual widening this replaces.
|
|
949
|
+
*
|
|
950
|
+
* Returns the forward-slashed path, or null when the entry is not one.
|
|
951
|
+
*/
|
|
952
|
+
function contractDeclaredWriteTarget(entry) {
|
|
953
|
+
if (typeof entry !== "string")
|
|
954
|
+
return null;
|
|
955
|
+
const trimmed = entry.trim();
|
|
956
|
+
if (trimmed.length === 0)
|
|
957
|
+
return null;
|
|
958
|
+
if (NON_WRITE_TARGET_CHARS.test(trimmed))
|
|
959
|
+
return null;
|
|
960
|
+
// Absolute (POSIX or Windows-UNC) forms are not repo-relative.
|
|
961
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("\\"))
|
|
962
|
+
return null;
|
|
963
|
+
const normalized = trimmed.replace(/\\/gu, "/");
|
|
964
|
+
if (normalized.split("/").includes(".."))
|
|
965
|
+
return null;
|
|
966
|
+
// A bare word ("session") is an interface name, not a path. Require either a
|
|
967
|
+
// path separator or a file extension.
|
|
968
|
+
if (!normalized.includes("/") && !/\.[a-z0-9]{1,6}$/iu.test(normalized))
|
|
969
|
+
return null;
|
|
970
|
+
return normalized;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* The path-parseable write targets each finalized module contract declares,
|
|
974
|
+
* keyed by `moduleSlug(name)` — the SAME identity the obligation ids encode, so
|
|
975
|
+
* this map joins to the decomposition's modules without a second name space.
|
|
976
|
+
*
|
|
977
|
+
* Degrades to an empty map when the artifact is absent or malformed: this
|
|
978
|
+
* resolver runs on the VALIDATOR's refusal path, so a bad contracts file must
|
|
979
|
+
* cost the widening, never wedge every subsequent next-step with a throw.
|
|
980
|
+
*/
|
|
981
|
+
async function readModuleContractWriteTargets(artifactsDir) {
|
|
982
|
+
let finalized;
|
|
983
|
+
try {
|
|
984
|
+
finalized = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
return new Map();
|
|
988
|
+
}
|
|
989
|
+
const entries = isRecord(finalized) && Array.isArray(finalized.module_contracts)
|
|
990
|
+
? finalized.module_contracts
|
|
991
|
+
: [];
|
|
992
|
+
const bySlug = new Map();
|
|
993
|
+
for (const entry of entries) {
|
|
994
|
+
if (!isRecord(entry) || typeof entry.name !== "string")
|
|
995
|
+
continue;
|
|
996
|
+
const slug = moduleSlug(entry.name);
|
|
997
|
+
if (slug.length === 0)
|
|
998
|
+
continue;
|
|
999
|
+
const targets = bySlug.get(slug) ?? [];
|
|
1000
|
+
const declared = [
|
|
1001
|
+
...(Array.isArray(entry.outputs) ? entry.outputs : []),
|
|
1002
|
+
...(Array.isArray(entry.side_effects) ? entry.side_effects : []),
|
|
1003
|
+
];
|
|
1004
|
+
for (const raw of declared) {
|
|
1005
|
+
const target = contractDeclaredWriteTarget(raw);
|
|
1006
|
+
if (target !== null && !targets.includes(target))
|
|
1007
|
+
targets.push(target);
|
|
1008
|
+
}
|
|
1009
|
+
bySlug.set(slug, targets);
|
|
1010
|
+
}
|
|
1011
|
+
return bySlug;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Single source for "which files may this DAG node write". The scope is the
|
|
1015
|
+
* UNION of two declarations, never one overriding the other:
|
|
1016
|
+
*
|
|
1017
|
+
* - the node's own declared files (`output_files`, else `files_likely_touched`);
|
|
1018
|
+
* - the path-parseable write targets (`outputs` + `side_effects`) declared by
|
|
1019
|
+
* the finalized contract of the module(s) the node's obligations belong to,
|
|
1020
|
+
* resolved by longest-`OBL-<slug>-` prefix so a short slug never mis-claims a
|
|
1021
|
+
* longer module's targets.
|
|
1022
|
+
*
|
|
1023
|
+
* A node that declared NO files of its own additionally inherits the
|
|
1024
|
+
* `file_scope` of those same modules — that inheritance is the scope-less
|
|
1025
|
+
* FALLBACK only, and is deliberately not unioned into a node that did declare.
|
|
1026
|
+
*
|
|
1027
|
+
* ⚠ The declared-files-win EARLY RETURN this used to perform is deliberately
|
|
1028
|
+
* superseded (owner decision, nightly ledger 2026-08-20). The module contract is
|
|
1029
|
+
* where a module's write targets are declared, and they never reached the node
|
|
1030
|
+
* scope — so an implementer was handed an obligation whose declared target file
|
|
1031
|
+
* was missing from `allowed_files`, and a human widened it by hand (four
|
|
1032
|
+
* recoveries in one wave). Union, not precedence.
|
|
938
1033
|
*
|
|
939
1034
|
* ⚠ Shared by the PROMOTER (which derives the scope) and the VALIDATOR (which
|
|
940
1035
|
* refuses when it resolves to nothing) on purpose. Two copies of this resolution
|
|
@@ -952,25 +1047,43 @@ async function readDecomposedModules(artifactsDir) {
|
|
|
952
1047
|
*/
|
|
953
1048
|
async function buildNodeWriteScopeResolver(artifactsDir) {
|
|
954
1049
|
const decomposedModules = await readDecomposedModules(artifactsDir);
|
|
1050
|
+
const contractTargetsBySlug = await readModuleContractWriteTargets(artifactsDir);
|
|
955
1051
|
const moduleScopesBySlug = decomposedModules
|
|
956
|
-
.map((m) => ({
|
|
1052
|
+
.map((m) => ({
|
|
1053
|
+
slug: moduleSlug(m.name),
|
|
1054
|
+
files: m.file_scope,
|
|
1055
|
+
targets: contractTargetsBySlug.get(moduleSlug(m.name)) ?? [],
|
|
1056
|
+
}))
|
|
957
1057
|
.sort((a, b) => b.slug.length - a.slug.length);
|
|
958
1058
|
const resolve = (node) => {
|
|
959
1059
|
const declared = [...new Set(node.output_files ?? node.files_likely_touched ?? [])];
|
|
960
|
-
if (declared.length > 0)
|
|
961
|
-
return declared;
|
|
962
1060
|
const obligationIds = [
|
|
963
1061
|
...(node.satisfies_obligations ?? []),
|
|
964
1062
|
...(node.verification_obligation_ids ?? []),
|
|
965
1063
|
];
|
|
966
1064
|
const inherited = new Set();
|
|
1065
|
+
const ownedTargets = new Set();
|
|
967
1066
|
for (const id of obligationIds) {
|
|
968
1067
|
const owner = moduleScopesBySlug.find((m) => id.startsWith(`OBL-${m.slug}-`));
|
|
969
|
-
if (owner)
|
|
1068
|
+
if (!owner)
|
|
1069
|
+
continue;
|
|
1070
|
+
// file_scope inheritance is the scope-less fallback ONLY; the contract's
|
|
1071
|
+
// declared targets are unioned in either way.
|
|
1072
|
+
if (declared.length === 0)
|
|
970
1073
|
for (const f of owner.files)
|
|
971
1074
|
inherited.add(f);
|
|
1075
|
+
for (const t of owner.targets)
|
|
1076
|
+
ownedTargets.add(t);
|
|
972
1077
|
}
|
|
973
|
-
|
|
1078
|
+
// Content-derived order: the node's own declarations first, in the order
|
|
1079
|
+
// they were declared, then the added targets path-sorted — so the resolved
|
|
1080
|
+
// scope (which reaches the plan's content hash through affected_files)
|
|
1081
|
+
// never churns on module ordering.
|
|
1082
|
+
const base = declared.length > 0 ? declared : [...inherited];
|
|
1083
|
+
const added = [...ownedTargets]
|
|
1084
|
+
.filter((t) => !base.includes(t))
|
|
1085
|
+
.sort((left, right) => compareCodeUnits(left, right));
|
|
1086
|
+
return [...base, ...added];
|
|
974
1087
|
};
|
|
975
1088
|
return { resolve, availableSlugs: moduleScopesBySlug.map((m) => m.slug) };
|
|
976
1089
|
}
|
|
@@ -2696,8 +2809,6 @@ function deriveObligationLensAndSeverity(kinds) {
|
|
|
2696
2809
|
// separator-inconsistent path becomes one canonical repo-relative form, a path
|
|
2697
2810
|
// that escapes the repository is refused outright, and a command carrying shell
|
|
2698
2811
|
// chaining or substitution is refused rather than handed to a shell.
|
|
2699
|
-
/** Shell metacharacters a declared package-script / test invocation never needs. */
|
|
2700
|
-
const SHELL_METACHARACTERS = /[&|;<>`$\n\r\0]/;
|
|
2701
2812
|
/**
|
|
2702
2813
|
* The tracked-path corpus for write-scope checking, or null when the tree
|
|
2703
2814
|
* cannot be read. Null degrades to "shape-only normalization" exactly as the
|
|
@@ -2788,24 +2899,13 @@ export async function evaluatePromotedPlanWriteScope(artifactsDir, root) {
|
|
|
2788
2899
|
return violations.length > 0 ? { violations } : null;
|
|
2789
2900
|
}
|
|
2790
2901
|
export function normalizeBlockTargetedCommands(commands, blockId) {
|
|
2791
|
-
const
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
}
|
|
2799
|
-
if (SHELL_METACHARACTERS.test(command)) {
|
|
2800
|
-
refusals.push(`Block "${blockId}" declares the targeted_commands entry ${JSON.stringify(raw)}, ` +
|
|
2801
|
-
`which carries shell chaining, substitution or redirection. A targeted command is ` +
|
|
2802
|
-
`executed verbatim through a shell, so it must be one invocation — split it into ` +
|
|
2803
|
-
`separate entries.`);
|
|
2804
|
-
continue;
|
|
2805
|
-
}
|
|
2806
|
-
normalized.push(command);
|
|
2807
|
-
}
|
|
2808
|
-
return { targeted_commands: normalized, refusals };
|
|
2902
|
+
const partitioned = partitionCommandsByDeclaredShape(commands, (kind, raw) => kind === "empty"
|
|
2903
|
+
? `Block "${blockId}" declares an empty targeted_commands entry.`
|
|
2904
|
+
: `Block "${blockId}" declares the targeted_commands entry ${JSON.stringify(raw)}, ` +
|
|
2905
|
+
`which carries shell chaining, substitution or redirection. A targeted command is ` +
|
|
2906
|
+
`executed verbatim through a shell, so it must be one invocation — split it into ` +
|
|
2907
|
+
`separate entries.`);
|
|
2908
|
+
return { targeted_commands: partitioned.commands, refusals: partitioned.refusals };
|
|
2809
2909
|
}
|
|
2810
2910
|
/**
|
|
2811
2911
|
* Collect every write-scope and command refusal the promotion WOULD hit, before
|
|
@@ -2882,9 +2982,10 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir, root
|
|
|
2882
2982
|
// the module decomposition instead of trusting the host to have filled it: each
|
|
2883
2983
|
// node's obligations are `OBL-<moduleSlug>-…`, and every module declares its
|
|
2884
2984
|
// `file_scope`, so a node that declared no files inherits the file_scope of the
|
|
2885
|
-
// module(s) its obligations belong to.
|
|
2886
|
-
//
|
|
2887
|
-
//
|
|
2985
|
+
// module(s) its obligations belong to. A node that DID declare files still gains
|
|
2986
|
+
// those modules' finalized-contract write targets (P38) — the scope is a UNION,
|
|
2987
|
+
// not a precedence. Single-sourced with the DAG validator, which refuses a node
|
|
2988
|
+
// this resolves to nothing for — see buildNodeWriteScopeResolver.
|
|
2888
2989
|
const { resolve: deriveNodeFiles } = await buildNodeWriteScopeResolver(artifactsDir);
|
|
2889
2990
|
const nodes = (Array.isArray(dag?.nodes) ? [...dag.nodes] : []).sort((left, right) => String(left.id).localeCompare(String(right.id)));
|
|
2890
2991
|
// Path-A promotion is an identity-preserving projection. DAG node ids describe
|
|
@@ -2951,8 +3052,9 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir, root
|
|
|
2951
3052
|
confidence: "high",
|
|
2952
3053
|
lens,
|
|
2953
3054
|
summary: node.description ?? node.title ?? "",
|
|
2954
|
-
// output_files (declared write scope) takes priority over files_likely_touched
|
|
2955
|
-
//
|
|
3055
|
+
// output_files (declared write scope) takes priority over files_likely_touched,
|
|
3056
|
+
// unioned with the owning module contract's declared write targets; when the
|
|
3057
|
+
// node declared neither, it inherits the module file_scope (deriveNodeFiles)
|
|
2956
3058
|
// so the finding is never scope-less. Map each path to the { path } shape that
|
|
2957
3059
|
// Finding.affected_files expects.
|
|
2958
3060
|
affected_files: deriveNodeFiles(node).map((p) => ({ path: p })),
|
|
@@ -3039,6 +3141,12 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir, root
|
|
|
3039
3141
|
// time promotion runs there is nothing left to refuse. The throw below is a
|
|
3040
3142
|
// BACKSTOP for a caller that skipped that gate — never the operator-facing
|
|
3041
3143
|
// path.
|
|
3144
|
+
//
|
|
3145
|
+
// "Nothing left to refuse" is now TRUE BY CONSTRUCTION, not by hope: the
|
|
3146
|
+
// command half asks the ONE shared `commandLeavesDeclaredShape` predicate
|
|
3147
|
+
// that the host-handoff consumer asks, so a command this gate admits cannot
|
|
3148
|
+
// be refused downstream (and vice versa). It used to be a claim about two
|
|
3149
|
+
// independent implementations that disagreed in both directions.
|
|
3042
3150
|
const scope = normalizeBlockTouchedFiles(root, deriveNodeFiles(node), toBlockId(nodeId));
|
|
3043
3151
|
const commands = normalizeBlockTargetedCommands(node.targeted_commands ?? [], toBlockId(nodeId));
|
|
3044
3152
|
const refusals = [...scope.refusals, ...commands.refusals];
|