@patronage/software-factory 0.30.0 → 1.0.0-alpha.1
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 +1 -1
- package/dist/index.d.ts +354 -43
- package/dist/index.js +983 -54
- package/dist/schemas.d.ts +73 -0
- package/dist/schemas.js +299 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,10 +14,10 @@ import { setImmediate } from "node:timers";
|
|
|
14
14
|
import { setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
15
15
|
import { Worker } from "node:worker_threads";
|
|
16
16
|
import picomatch from "picomatch";
|
|
17
|
-
import { promisify } from "node:util";
|
|
18
17
|
import { parse } from "yaml";
|
|
18
|
+
import { promisify } from "node:util";
|
|
19
19
|
//#region package.json
|
|
20
|
-
var version = "0.
|
|
20
|
+
var version = "1.0.0-alpha.1";
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/review-rungs.ts
|
|
23
23
|
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
@@ -894,8 +894,15 @@ function isValidGlob(pattern) {
|
|
|
894
894
|
return false;
|
|
895
895
|
}
|
|
896
896
|
}
|
|
897
|
+
function isRepoRelativePath(value) {
|
|
898
|
+
const segments = value.split("/");
|
|
899
|
+
return value.length > 0 && !value.startsWith("/") && !/^[A-Za-z]:\//u.test(value) && !value.includes("\\") && !segments.some((segment) => segment === "" || segment === "." || segment === "..");
|
|
900
|
+
}
|
|
901
|
+
function isValidRepoRelativeGlob(pattern) {
|
|
902
|
+
return isRepoRelativePath(pattern) && !(pattern.startsWith("!") && !pattern.startsWith("!(")) && isValidGlob(pattern);
|
|
903
|
+
}
|
|
897
904
|
function matchesAnyGlob(patterns, file) {
|
|
898
|
-
return patterns.length > 0 && picomatch.isMatch(file, patterns);
|
|
905
|
+
return patterns.length > 0 && picomatch.isMatch(file, [...patterns]);
|
|
899
906
|
}
|
|
900
907
|
//#endregion
|
|
901
908
|
//#region src/diff-classification.ts
|
|
@@ -939,11 +946,13 @@ const LEGACY_PROFILE_SCHEMA_VERSIONS = [
|
|
|
939
946
|
3,
|
|
940
947
|
4
|
|
941
948
|
];
|
|
942
|
-
const PROFILE_JSON_SCHEMA_DESCRIPTION = "Editor validation covers the profile input shape and straightforward cross-field constraints. Zod remains the load-time enforcement authority: verification command names must be distinct and requiredChecks entries must have distinct names; these invariants are enforced at load time, not by this schema.";
|
|
949
|
+
const PROFILE_JSON_SCHEMA_DESCRIPTION = "Editor validation covers the profile input shape and straightforward cross-field constraints. Zod remains the load-time enforcement authority: verification command names must be distinct and requiredChecks entries must have distinct names; these invariants are enforced at load time, not by this schema. Impact target path patterns encode repo-relative safety in JSON Schema, while complete picomatch syntax validity remains a runtime-only refinement.";
|
|
943
950
|
const globSchema = z.string().min(1).refine(isValidGlob, "must be a valid glob");
|
|
951
|
+
const repoRelativeGlobSchema = z.string().min(1).regex(/^(?!(?:\/[\s\S]*|[A-Za-z]:\/[\s\S]*|!(?!\()[\s\S]*|[\s\S]*\\[\s\S]*|[\s\S]*\/\/[\s\S]*|[\s\S]*\/$|\.{1,2}(?:\/|$)[\s\S]*|[\s\S]*\/\.{1,2}(?:\/|$)[\s\S]*)).+$/u, "must be a repo-relative positive glob").refine(isValidRepoRelativeGlob, "must be a valid repo-relative glob");
|
|
944
952
|
const commandSchema = z.object({
|
|
945
953
|
command: z.string().min(1),
|
|
946
954
|
description: z.string().min(1),
|
|
955
|
+
impactTarget: z.string().min(1).optional(),
|
|
947
956
|
name: z.string().min(1),
|
|
948
957
|
requiredCheck: z.string().min(1).optional(),
|
|
949
958
|
scope: z.enum([
|
|
@@ -969,10 +978,23 @@ const requiredCheckSchema = z.object({
|
|
|
969
978
|
name: z.string().min(1),
|
|
970
979
|
scope: requiredCheckScopeSchema$1.optional()
|
|
971
980
|
}).strict();
|
|
981
|
+
const impactTargetSchema = z.object({
|
|
982
|
+
importers: z.array(z.string().min(1)).min(1),
|
|
983
|
+
name: z.string().min(1),
|
|
984
|
+
paths: z.array(repoRelativeGlobSchema).min(1).optional()
|
|
985
|
+
}).strict();
|
|
986
|
+
const impactConfigSchema = z.object({ targets: z.array(impactTargetSchema).min(1).superRefine((targets, context) => {
|
|
987
|
+
const names = targets.map((target) => target.name);
|
|
988
|
+
if (new Set(names).size !== names.length) context.addIssue({
|
|
989
|
+
code: "custom",
|
|
990
|
+
message: "impact.targets entries must have distinct names."
|
|
991
|
+
});
|
|
992
|
+
}) }).strict();
|
|
972
993
|
const factoryProjectProfileSchema = z.object({
|
|
973
994
|
$schema: z.string().min(1).optional(),
|
|
974
995
|
extensions: z.record(z.string(), z.unknown()).optional(),
|
|
975
996
|
hq: hqIngestConfigSchema.optional(),
|
|
997
|
+
impact: impactConfigSchema.optional(),
|
|
976
998
|
proof: z.object({ classificationPolicy: z.object({
|
|
977
999
|
docsOnly: z.array(globSchema).default([]),
|
|
978
1000
|
trivial: z.array(globSchema).default([])
|
|
@@ -1006,6 +1028,30 @@ const factoryProjectProfileSchema = z.object({
|
|
|
1006
1028
|
});
|
|
1007
1029
|
commandNames.add(command.name);
|
|
1008
1030
|
}
|
|
1031
|
+
const impactTargetNames = new Set((profile.impact?.targets ?? []).map((target) => target.name));
|
|
1032
|
+
for (const [commandIndex, command] of profile.verification.commands.entries()) {
|
|
1033
|
+
if (command.impactTarget === void 0) continue;
|
|
1034
|
+
if (!impactTargetNames.has(command.impactTarget)) context.addIssue({
|
|
1035
|
+
code: "custom",
|
|
1036
|
+
message: `Verification command impactTarget "${command.impactTarget}" does not name a declared impact.targets entry.`,
|
|
1037
|
+
path: [
|
|
1038
|
+
"verification",
|
|
1039
|
+
"commands",
|
|
1040
|
+
commandIndex,
|
|
1041
|
+
"impactTarget"
|
|
1042
|
+
]
|
|
1043
|
+
});
|
|
1044
|
+
if (command.requiredCheck !== void 0 && command.requiredCheck !== command.impactTarget) context.addIssue({
|
|
1045
|
+
code: "custom",
|
|
1046
|
+
message: `Verification command "${command.name}" maps requiredCheck "${command.requiredCheck}" but scopes on impactTarget "${command.impactTarget}"; a scoped command's evidence demand and impact target must be the same name.`,
|
|
1047
|
+
path: [
|
|
1048
|
+
"verification",
|
|
1049
|
+
"commands",
|
|
1050
|
+
commandIndex,
|
|
1051
|
+
"impactTarget"
|
|
1052
|
+
]
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1009
1055
|
const requiredChecks = new Map((profile.requiredChecks ?? []).map((check) => [check.name, check]));
|
|
1010
1056
|
const mappedChecks = /* @__PURE__ */ new Set();
|
|
1011
1057
|
for (const [commandIndex, command] of profile.verification.commands.entries()) {
|
|
@@ -1036,7 +1082,7 @@ const factoryProjectProfileSchema = z.object({
|
|
|
1036
1082
|
mappedChecks.add(command.requiredCheck);
|
|
1037
1083
|
}
|
|
1038
1084
|
}).meta({ description: PROFILE_JSON_SCHEMA_DESCRIPTION });
|
|
1039
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1085
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1040
1086
|
const stringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1041
1087
|
const quoteList = (values) => JSON.stringify(values);
|
|
1042
1088
|
const namesExistingFile = (root, entry) => {
|
|
@@ -1085,13 +1131,13 @@ const migratedPolicyLines = (policy, root) => {
|
|
|
1085
1131
|
*/
|
|
1086
1132
|
function v2MigrationLines(input, root) {
|
|
1087
1133
|
const lines = [];
|
|
1088
|
-
const proof = isRecord(input.proof) ? input.proof : void 0;
|
|
1089
|
-
if (proof && isRecord(proof.classificationPolicy)) lines.push(...migratedPolicyLines(proof.classificationPolicy, root));
|
|
1090
|
-
const commands = isRecord(input.verification) ? input.verification.commands : void 0;
|
|
1134
|
+
const proof = isRecord$1(input.proof) ? input.proof : void 0;
|
|
1135
|
+
if (proof && isRecord$1(proof.classificationPolicy)) lines.push(...migratedPolicyLines(proof.classificationPolicy, root));
|
|
1136
|
+
const commands = isRecord$1(input.verification) ? input.verification.commands : void 0;
|
|
1091
1137
|
if (Array.isArray(commands)) {
|
|
1092
|
-
for (const [index, command] of commands.entries()) if (isRecord(command) && command.scope === "always") lines.push(`verification.commands[${index}].scope "always" -> "docs-only"`);
|
|
1138
|
+
for (const [index, command] of commands.entries()) if (isRecord$1(command) && command.scope === "always") lines.push(`verification.commands[${index}].scope "always" -> "docs-only"`);
|
|
1093
1139
|
}
|
|
1094
|
-
if (isRecord(input.hq) && typeof input.hq.endpoint === "string") try {
|
|
1140
|
+
if (isRecord$1(input.hq) && typeof input.hq.endpoint === "string") try {
|
|
1095
1141
|
const url = new URL(input.hq.endpoint);
|
|
1096
1142
|
if (url.href !== `${url.origin}/` && url.href !== url.origin) lines.push(`hq.endpoint ${JSON.stringify(input.hq.endpoint)} -> ${JSON.stringify(url.origin)}`);
|
|
1097
1143
|
} catch {}
|
|
@@ -1103,25 +1149,25 @@ function hasCorrectnessOnlyReview(review) {
|
|
|
1103
1149
|
}
|
|
1104
1150
|
const v4MigrationLines = (input) => {
|
|
1105
1151
|
const lines = [];
|
|
1106
|
-
if (isRecord(input.project) && "key" in input.project) lines.push("project.key -> removed; current proof writers derive projectKey from repository.name");
|
|
1107
|
-
if (isRecord(input.repository) && "defaultBranch" in input.repository) lines.push("repository.defaultBranch -> removed; commands resolve the live base from Git/GitHub or an explicit --base");
|
|
1108
|
-
if (isRecord(input.env) && "required" in input.env) lines.push("env.required -> removed; the command that needs an environment value owns its fail-closed diagnostic");
|
|
1109
|
-
if (isRecord(input.review)) {
|
|
1152
|
+
if (isRecord$1(input.project) && "key" in input.project) lines.push("project.key -> removed; current proof writers derive projectKey from repository.name");
|
|
1153
|
+
if (isRecord$1(input.repository) && "defaultBranch" in input.repository) lines.push("repository.defaultBranch -> removed; commands resolve the live base from Git/GitHub or an explicit --base");
|
|
1154
|
+
if (isRecord$1(input.env) && "required" in input.env) lines.push("env.required -> removed; the command that needs an environment value owns its fail-closed diagnostic");
|
|
1155
|
+
if (isRecord$1(input.review)) {
|
|
1110
1156
|
const isCorrectnessOnlyReview = hasCorrectnessOnlyReview(input.review);
|
|
1111
1157
|
if ("defaultMaxCycles" in input.review) lines.push("review.defaultMaxCycles -> removed; the factory review cap is 5");
|
|
1112
1158
|
if ("modes" in input.review) lines.push("review.modes -> removed; correctness is always required");
|
|
1113
1159
|
if (isCorrectnessOnlyReview) lines.push("review -> removed; a correctness-only review has no v5 configuration, so delete the object");
|
|
1114
1160
|
if (Array.isArray(input.review.conditional)) {
|
|
1115
|
-
for (const [index, conditional] of input.review.conditional.entries()) if (isRecord(conditional) && "modes" in conditional) lines.push(`review.conditional[${index}].modes -> removed; each retained conditional path list demands security review`);
|
|
1161
|
+
for (const [index, conditional] of input.review.conditional.entries()) if (isRecord$1(conditional) && "modes" in conditional) lines.push(`review.conditional[${index}].modes -> removed; each retained conditional path list demands security review`);
|
|
1116
1162
|
}
|
|
1117
1163
|
}
|
|
1118
1164
|
if (Array.isArray(input.requiredChecks)) {
|
|
1119
|
-
for (const [index, check] of input.requiredChecks.entries()) if (isRecord(check) && "checkType" in check) lines.push(`requiredChecks[${index}].checkType -> removed; external required checks are verify-type`);
|
|
1165
|
+
for (const [index, check] of input.requiredChecks.entries()) if (isRecord$1(check) && "checkType" in check) lines.push(`requiredChecks[${index}].checkType -> removed; external required checks are verify-type`);
|
|
1120
1166
|
}
|
|
1121
1167
|
return lines;
|
|
1122
1168
|
};
|
|
1123
1169
|
function legacyProfileMigration(input, root) {
|
|
1124
|
-
if (!(isRecord(input) && typeof input.schemaVersion === "number" && LEGACY_PROFILE_SCHEMA_VERSIONS.includes(input.schemaVersion))) return;
|
|
1170
|
+
if (!(isRecord$1(input) && typeof input.schemaVersion === "number" && LEGACY_PROFILE_SCHEMA_VERSIONS.includes(input.schemaVersion))) return;
|
|
1125
1171
|
const foundVersion = input.schemaVersion;
|
|
1126
1172
|
const lines = [`schemaVersion ${foundVersion} -> 5`];
|
|
1127
1173
|
if (foundVersion === 2) lines.push(...v2MigrationLines(input, root));
|
|
@@ -3792,6 +3838,675 @@ const authorizeDemandWaiver = ({ authenticatedLogin, session }) => {
|
|
|
3792
3838
|
...normalizedSession && normalizedSession !== "unknown" ? { session: normalizedSession } : {}
|
|
3793
3839
|
} };
|
|
3794
3840
|
};
|
|
3841
|
+
//#endregion
|
|
3842
|
+
//#region src/impact-stamp.ts
|
|
3843
|
+
const LOCKFILE_PATH = "pnpm-lock.yaml";
|
|
3844
|
+
const SUPPORTED_LOCKFILE_VERSION = "9.0";
|
|
3845
|
+
const impactStampTargetSchema = z.object({
|
|
3846
|
+
basis: z.string().min(1),
|
|
3847
|
+
impact: z.enum(["affected", "not-affected"]),
|
|
3848
|
+
name: z.string().min(1)
|
|
3849
|
+
});
|
|
3850
|
+
/**
|
|
3851
|
+
* The recorded stamp. `targets` satisfies the completeness invariant: every
|
|
3852
|
+
* target the profile declares is present, each with its exact basis — never
|
|
3853
|
+
* silently absent. `basis: "conservative"` means an unmodelled or unprovable
|
|
3854
|
+
* input widened every target to full impact.
|
|
3855
|
+
*/
|
|
3856
|
+
const impactStampSchema = z.object({
|
|
3857
|
+
basis: z.enum(["target-scoped", "conservative"]),
|
|
3858
|
+
reasons: z.array(z.string()),
|
|
3859
|
+
stampVersion: z.literal(2),
|
|
3860
|
+
targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
|
|
3861
|
+
const names = targets.map((target) => target.name);
|
|
3862
|
+
if (new Set(names).size !== names.length) context.addIssue({
|
|
3863
|
+
code: "custom",
|
|
3864
|
+
message: "impact stamp targets must have distinct names."
|
|
3865
|
+
});
|
|
3866
|
+
})
|
|
3867
|
+
});
|
|
3868
|
+
/**
|
|
3869
|
+
* A configuration contradiction between the profile's impact declarations and
|
|
3870
|
+
* the repository's actual lockfile — e.g. a declared importer root that
|
|
3871
|
+
* matches no importer on either side of the delta. Controls fail loudly (ADR
|
|
3872
|
+
* 0024): this error must ABORT the run, never degrade to a stamp, because a
|
|
3873
|
+
* profile typo would otherwise silently mis-scope every future candidate.
|
|
3874
|
+
* Data/runtime failures (unreadable, unparseable, malformed lockfiles, reader
|
|
3875
|
+
* throws) are the opposite case and stay on the conservative-stamp path.
|
|
3876
|
+
*/
|
|
3877
|
+
var ImpactStampConfigError = class extends Error {
|
|
3878
|
+
name = "ImpactStampConfigError";
|
|
3879
|
+
};
|
|
3880
|
+
const CONSERVATIVE_TARGET_BASIS = "conservative: impact not provable; full impact assumed";
|
|
3881
|
+
/**
|
|
3882
|
+
* The surfaces that consume the stamp. One classification, consumed
|
|
3883
|
+
* everywhere (#430): every surface routes through
|
|
3884
|
+
* {@link impactStampScopeDecision}, so no surface can be more trusting than
|
|
3885
|
+
* another, and no surface re-derives path rules of its own. The value only
|
|
3886
|
+
* selects the wording of the conservative floor a refusal falls back to.
|
|
3887
|
+
*/
|
|
3888
|
+
const IMPACT_SCOPE_SURFACES = {
|
|
3889
|
+
demand: "full demand floor",
|
|
3890
|
+
"preview-lifecycle": "full preview-lifecycle floor",
|
|
3891
|
+
"verification-battery": "full verification-battery floor"
|
|
3892
|
+
};
|
|
3893
|
+
/**
|
|
3894
|
+
* Whether a trusted stamp RELEASES one target-scoped unit of work — an
|
|
3895
|
+
* external proof demand (#542 wave 2), a verification-battery command, or a
|
|
3896
|
+
* preview (Alchemy) lifecycle stack (#430 wave 3).
|
|
3897
|
+
*
|
|
3898
|
+
* The only release path is a `target-scoped` stamp of exactly this build's
|
|
3899
|
+
* stamp version whose target entry for exactly this name is provably
|
|
3900
|
+
* `not-affected`. Every other input — no trusted stamp, an unknown stamp
|
|
3901
|
+
* version, a conservative stamp, a name the stamp does not classify, or an
|
|
3902
|
+
* `affected` verdict — keeps today's full-work floor. Work only ever gets
|
|
3903
|
+
* released, never added or satisfied: this function can withdraw a demand, it
|
|
3904
|
+
* can never certify one.
|
|
3905
|
+
*
|
|
3906
|
+
* Identity binding is the CALLER's precondition: pass only a stamp computed
|
|
3907
|
+
* for, or read from a pr:verify proof bound to, the current candidate (see
|
|
3908
|
+
* `trustedImpactStamp` in `pr-readiness/verification-proof.ts`); pass
|
|
3909
|
+
* `undefined` otherwise.
|
|
3910
|
+
*/
|
|
3911
|
+
const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
|
|
3912
|
+
const floor = IMPACT_SCOPE_SURFACES[surface];
|
|
3913
|
+
if (stamp === void 0) return {
|
|
3914
|
+
reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
|
|
3915
|
+
scoped: false
|
|
3916
|
+
};
|
|
3917
|
+
if (stamp.stampVersion !== 2) return {
|
|
3918
|
+
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 2; ${floor}`,
|
|
3919
|
+
scoped: false
|
|
3920
|
+
};
|
|
3921
|
+
if (stamp.basis !== "target-scoped") return {
|
|
3922
|
+
reason: `impact stamp is conservative (full impact assumed); ${floor}`,
|
|
3923
|
+
scoped: false
|
|
3924
|
+
};
|
|
3925
|
+
const matches = stamp.targets.filter((candidate) => candidate.name === targetName);
|
|
3926
|
+
if (matches.length > 1) return {
|
|
3927
|
+
reason: `impact stamp is self-contradictory: ${matches.length} rows classify a target named "${targetName}"; ${floor}`,
|
|
3928
|
+
scoped: false
|
|
3929
|
+
};
|
|
3930
|
+
const [target] = matches;
|
|
3931
|
+
if (target === void 0) return {
|
|
3932
|
+
reason: `impact stamp does not classify a target named "${targetName}"; ${floor}`,
|
|
3933
|
+
scoped: false
|
|
3934
|
+
};
|
|
3935
|
+
if (target.impact !== "not-affected") return {
|
|
3936
|
+
reason: `impact stamp records target "${targetName}" as affected (${target.basis})`,
|
|
3937
|
+
scoped: false
|
|
3938
|
+
};
|
|
3939
|
+
return {
|
|
3940
|
+
reason: `impact stamp released: target "${targetName}" is provably not affected by this candidate's delta (${target.basis})`,
|
|
3941
|
+
scoped: true
|
|
3942
|
+
};
|
|
3943
|
+
};
|
|
3944
|
+
const impactStampDemandRelease = ({ checkName, stamp }) => {
|
|
3945
|
+
const decision = impactStampScopeDecision({
|
|
3946
|
+
stamp,
|
|
3947
|
+
surface: "demand",
|
|
3948
|
+
targetName: checkName
|
|
3949
|
+
});
|
|
3950
|
+
return {
|
|
3951
|
+
reason: decision.reason,
|
|
3952
|
+
released: decision.scoped
|
|
3953
|
+
};
|
|
3954
|
+
};
|
|
3955
|
+
/**
|
|
3956
|
+
* The full-impact stamp: every declared target `affected`. This is the floor
|
|
3957
|
+
* every doubt path lands on, and the stamp a producer must fall back to if
|
|
3958
|
+
* stamp computation itself fails for any reason.
|
|
3959
|
+
*/
|
|
3960
|
+
const conservativeImpactStamp = (targets, reasons) => ({
|
|
3961
|
+
basis: "conservative",
|
|
3962
|
+
reasons,
|
|
3963
|
+
stampVersion: 2,
|
|
3964
|
+
targets: targets.map((target) => ({
|
|
3965
|
+
basis: CONSERVATIVE_TARGET_BASIS,
|
|
3966
|
+
impact: "affected",
|
|
3967
|
+
name: target.name
|
|
3968
|
+
}))
|
|
3969
|
+
});
|
|
3970
|
+
const conservativeStamp = conservativeImpactStamp;
|
|
3971
|
+
const DEPENDENCY_FIELDS = [
|
|
3972
|
+
"dependencies",
|
|
3973
|
+
"devDependencies",
|
|
3974
|
+
"optionalDependencies"
|
|
3975
|
+
];
|
|
3976
|
+
const importerDependencySchema = z.looseObject({
|
|
3977
|
+
specifier: z.string().optional(),
|
|
3978
|
+
version: z.string()
|
|
3979
|
+
});
|
|
3980
|
+
const importerSectionSchema = z.looseObject({
|
|
3981
|
+
dependencies: z.record(z.string(), importerDependencySchema).optional(),
|
|
3982
|
+
devDependencies: z.record(z.string(), importerDependencySchema).optional(),
|
|
3983
|
+
optionalDependencies: z.record(z.string(), importerDependencySchema).optional()
|
|
3984
|
+
});
|
|
3985
|
+
const snapshotSectionSchema = z.looseObject({
|
|
3986
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
3987
|
+
devDependencies: z.record(z.string(), z.string()).optional(),
|
|
3988
|
+
optionalDependencies: z.record(z.string(), z.string()).optional()
|
|
3989
|
+
});
|
|
3990
|
+
const pnpmLockfileSchema = z.looseObject({
|
|
3991
|
+
importers: z.record(z.string(), importerSectionSchema),
|
|
3992
|
+
lockfileVersion: z.unknown().optional(),
|
|
3993
|
+
packages: z.record(z.string(), z.unknown()).optional(),
|
|
3994
|
+
snapshots: z.record(z.string(), snapshotSectionSchema).optional()
|
|
3995
|
+
});
|
|
3996
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3997
|
+
const canonical = (value) => {
|
|
3998
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
3999
|
+
if (isRecord(value)) return Object.fromEntries(Object.keys(value).toSorted().map((key) => [key, canonical(value[key])]));
|
|
4000
|
+
return value;
|
|
4001
|
+
};
|
|
4002
|
+
const stableStringify$1 = (value) => JSON.stringify(canonical(value));
|
|
4003
|
+
const parseLockfile = (content) => {
|
|
4004
|
+
try {
|
|
4005
|
+
const parsed = parse(content);
|
|
4006
|
+
if (!isRecord(parsed)) return;
|
|
4007
|
+
const validated = pnpmLockfileSchema.safeParse(parsed);
|
|
4008
|
+
return validated.success ? validated.data : void 0;
|
|
4009
|
+
} catch {
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
};
|
|
4013
|
+
const nonGraphSections = ({ importers: _importers, packages: _packages, snapshots: _snapshots, ...rest }) => rest;
|
|
4014
|
+
const changedSectionKeys = (before, after) => {
|
|
4015
|
+
return [...new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})])].filter((key) => stableStringify$1(before?.[key]) !== stableStringify$1(after?.[key]));
|
|
4016
|
+
};
|
|
4017
|
+
const linkTarget = (importerPath, version) => {
|
|
4018
|
+
if (!version.startsWith("link:")) return;
|
|
4019
|
+
const raw = version.slice(5);
|
|
4020
|
+
const joined = importerPath === "." ? raw : `${importerPath}/${raw}`;
|
|
4021
|
+
const resolved = [];
|
|
4022
|
+
for (const segment of joined.split("/")) {
|
|
4023
|
+
if (segment === "" || segment === ".") continue;
|
|
4024
|
+
if (segment === "..") {
|
|
4025
|
+
resolved.pop();
|
|
4026
|
+
continue;
|
|
4027
|
+
}
|
|
4028
|
+
resolved.push(segment);
|
|
4029
|
+
}
|
|
4030
|
+
return resolved.length === 0 ? "." : resolved.join("/");
|
|
4031
|
+
};
|
|
4032
|
+
const importerMatchesTarget = (importerPath, importerRoots) => importerRoots.some((root) => importerPath === root || importerPath.startsWith(`${root}/`));
|
|
4033
|
+
const workspaceClosure = (lockfile, importerRoots) => {
|
|
4034
|
+
const closure = new Set(["."]);
|
|
4035
|
+
const queue = Object.keys(lockfile.importers ?? {}).filter((importerPath) => importerMatchesTarget(importerPath, importerRoots));
|
|
4036
|
+
while (queue.length > 0) {
|
|
4037
|
+
const importerPath = queue.pop();
|
|
4038
|
+
if (importerPath === void 0 || closure.has(importerPath)) continue;
|
|
4039
|
+
closure.add(importerPath);
|
|
4040
|
+
const section = lockfile.importers?.[importerPath];
|
|
4041
|
+
for (const field of DEPENDENCY_FIELDS) for (const dependency of Object.values(section?.[field] ?? {})) {
|
|
4042
|
+
const target = dependency.version === void 0 ? void 0 : linkTarget(importerPath, dependency.version);
|
|
4043
|
+
if (target !== void 0 && lockfile.importers?.[target]) queue.push(target);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
return closure;
|
|
4047
|
+
};
|
|
4048
|
+
const pushReference = (lockfile, queue, name, value) => {
|
|
4049
|
+
if (value.startsWith("link:")) return;
|
|
4050
|
+
queue.push(`${name}@${value}`);
|
|
4051
|
+
if (lockfile.snapshots?.[value]) queue.push(value);
|
|
4052
|
+
};
|
|
4053
|
+
const snapshotClosure = (lockfile, importerPaths) => {
|
|
4054
|
+
const keys = /* @__PURE__ */ new Set();
|
|
4055
|
+
const queue = [];
|
|
4056
|
+
for (const importerPath of importerPaths) {
|
|
4057
|
+
const section = lockfile.importers?.[importerPath];
|
|
4058
|
+
for (const field of DEPENDENCY_FIELDS) for (const [name, dependency] of Object.entries(section?.[field] ?? {})) if (dependency.version !== void 0) pushReference(lockfile, queue, name, dependency.version);
|
|
4059
|
+
}
|
|
4060
|
+
while (queue.length > 0) {
|
|
4061
|
+
const key = queue.pop();
|
|
4062
|
+
if (key === void 0 || keys.has(key)) continue;
|
|
4063
|
+
keys.add(key);
|
|
4064
|
+
const snapshot = lockfile.snapshots?.[key];
|
|
4065
|
+
for (const field of DEPENDENCY_FIELDS) for (const [name, version] of Object.entries(snapshot?.[field] ?? {})) pushReference(lockfile, queue, name, version);
|
|
4066
|
+
}
|
|
4067
|
+
return keys;
|
|
4068
|
+
};
|
|
4069
|
+
const withoutPeerSuffix = (key) => key.split("(")[0] ?? key;
|
|
4070
|
+
const supportedLockfileVersion = (lockfile) => String(lockfile.lockfileVersion) === SUPPORTED_LOCKFILE_VERSION;
|
|
4071
|
+
const targetImpactForLockfileDelta = (target, delta) => {
|
|
4072
|
+
const closures = delta.sides.map((lockfile) => ({
|
|
4073
|
+
importers: workspaceClosure(lockfile, target.importers),
|
|
4074
|
+
lockfile
|
|
4075
|
+
}));
|
|
4076
|
+
const changedImporter = delta.changedImporters.find((importerPath) => closures.some((side) => side.importers.has(importerPath)));
|
|
4077
|
+
if (changedImporter !== void 0) return {
|
|
4078
|
+
basis: `changed importer "${changedImporter}" is in this target's workspace closure`,
|
|
4079
|
+
impact: "affected",
|
|
4080
|
+
name: target.name
|
|
4081
|
+
};
|
|
4082
|
+
const resolved = new Set(closures.flatMap((side) => [...snapshotClosure(side.lockfile, side.importers)]));
|
|
4083
|
+
const resolvedBare = new Set([...resolved].map(withoutPeerSuffix));
|
|
4084
|
+
const changedKey = delta.changedSnapshots.find((key) => resolved.has(key)) ?? delta.changedPackages.find((key) => resolved.has(key) || resolvedBare.has(key));
|
|
4085
|
+
if (changedKey !== void 0) return {
|
|
4086
|
+
basis: `changed graph key "${changedKey}" is resolved by this target's importer closure`,
|
|
4087
|
+
impact: "affected",
|
|
4088
|
+
name: target.name
|
|
4089
|
+
};
|
|
4090
|
+
return {
|
|
4091
|
+
basis: `no changed importer or resolved graph key reaches importers [${target.importers.join(", ")}]`,
|
|
4092
|
+
impact: "not-affected",
|
|
4093
|
+
name: target.name
|
|
4094
|
+
};
|
|
4095
|
+
};
|
|
4096
|
+
/**
|
|
4097
|
+
* Compute the impact stamp for one candidate: every declared target recorded
|
|
4098
|
+
* as affected/not-affected with its exact basis. `not-affected` is only ever
|
|
4099
|
+
* returned only when every changed input is owned by a declared source glob or
|
|
4100
|
+
* is an analyzable pnpm-lock.yaml (v9) delta. Source and lockfile reachability
|
|
4101
|
+
* are unioned per target. Any invalid, unmatched, unreadable, unsupported, or
|
|
4102
|
+
* otherwise doubtful input widens the whole stamp to full impact.
|
|
4103
|
+
*
|
|
4104
|
+
* Total by contract: this function never throws. Any unexpected failure in
|
|
4105
|
+
* reading, parsing, or traversal is itself a doubt path and returns the
|
|
4106
|
+
* conservative stamp, so a producer can never be aborted by stamp
|
|
4107
|
+
* computation.
|
|
4108
|
+
*/
|
|
4109
|
+
const computeImpactStamp = (input) => {
|
|
4110
|
+
const targets = input.profile.impact?.targets ?? [];
|
|
4111
|
+
try {
|
|
4112
|
+
return computeImpactStampOrThrow(input, targets);
|
|
4113
|
+
} catch (error) {
|
|
4114
|
+
if (error instanceof ImpactStampConfigError) throw error;
|
|
4115
|
+
return conservativeStamp(targets, [`impact stamp computation failed (${error instanceof Error ? error.message : String(error)}); fail closed to full impact`]);
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4118
|
+
const readLockfileSides = (readLockfile) => {
|
|
4119
|
+
let base;
|
|
4120
|
+
let head;
|
|
4121
|
+
try {
|
|
4122
|
+
base = readLockfile("base");
|
|
4123
|
+
head = readLockfile("head");
|
|
4124
|
+
} catch (error) {
|
|
4125
|
+
return {
|
|
4126
|
+
error,
|
|
4127
|
+
kind: "threw"
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
if (base === void 0 || head === void 0) return {
|
|
4131
|
+
kind: "unreadable",
|
|
4132
|
+
side: base === void 0 ? "base" : "head"
|
|
4133
|
+
};
|
|
4134
|
+
const baseLock = parseLockfile(base);
|
|
4135
|
+
const headLock = parseLockfile(head);
|
|
4136
|
+
if (!(baseLock && headLock)) return {
|
|
4137
|
+
kind: "unparseable",
|
|
4138
|
+
side: baseLock ? "head" : "base"
|
|
4139
|
+
};
|
|
4140
|
+
if (![baseLock, headLock].every(supportedLockfileVersion)) return { kind: "unsupported-version" };
|
|
4141
|
+
return {
|
|
4142
|
+
kind: "ok",
|
|
4143
|
+
parsed: [baseLock, headLock]
|
|
4144
|
+
};
|
|
4145
|
+
};
|
|
4146
|
+
/**
|
|
4147
|
+
* Every declared importer root must exist somewhere in the delta: a root
|
|
4148
|
+
* matching no importer on EITHER side is a profile typo, and scoping from it
|
|
4149
|
+
* would leave only the implicit root importer in that target's closure —
|
|
4150
|
+
* silently classifying real changes `not-affected` forever. That is a
|
|
4151
|
+
* configuration contradiction, so it fails loudly instead of degrading. A root
|
|
4152
|
+
* present on ONE side only is legitimate (this very delta adds or removes the
|
|
4153
|
+
* importer) and scopes normally.
|
|
4154
|
+
*
|
|
4155
|
+
* Checked on EVERY computation whose lockfile sides are readable, not only on
|
|
4156
|
+
* the lockfile-only deltas that can actually scope. The check answers a
|
|
4157
|
+
* question about the PROFILE, which does not depend on what this candidate
|
|
4158
|
+
* changed; running it only where scoping happens would leave a typo latent
|
|
4159
|
+
* through every code PR and first surface it on the cheap lockfile-only PR
|
|
4160
|
+
* this machinery exists to speed up.
|
|
4161
|
+
*/
|
|
4162
|
+
const assertDeclaredImporterRoots = (targets, sides) => {
|
|
4163
|
+
for (const target of targets) for (const root of target.importers) if (!sides.some((lockfile) => Object.keys(lockfile.importers).some((importerPath) => importerMatchesTarget(importerPath, [root])))) throw new ImpactStampConfigError(`impact target "${target.name}" declares importer root "${root}", but no importer under that root exists in pnpm-lock.yaml on either side of the delta; fix the profile's impact.targets declaration`);
|
|
4164
|
+
};
|
|
4165
|
+
const assertDeclaredPathGlobs = (targets) => {
|
|
4166
|
+
for (const target of targets) for (const pattern of target.paths ?? []) if (!isValidRepoRelativeGlob(pattern)) throw new ImpactStampConfigError(`impact target "${target.name}" declares invalid repo-relative path glob "${pattern}"; fix the profile's impact.targets declaration`);
|
|
4167
|
+
};
|
|
4168
|
+
const analyzeSourcePaths = (sourceFiles, targets, profilePath) => {
|
|
4169
|
+
for (const file of sourceFiles) {
|
|
4170
|
+
if (!isRepoRelativePath(file)) return {
|
|
4171
|
+
kind: "conservative",
|
|
4172
|
+
reason: `changed source path "${file}" is not a valid repo-relative path; fail closed to full impact`
|
|
4173
|
+
};
|
|
4174
|
+
if (file === profilePath) return {
|
|
4175
|
+
kind: "conservative",
|
|
4176
|
+
reason: `changed source path "${file}" is the impact profile being evaluated; fail closed to full impact`
|
|
4177
|
+
};
|
|
4178
|
+
if (!targets.some((target) => matchesAnyGlob(target.paths ?? [], file))) return {
|
|
4179
|
+
kind: "conservative",
|
|
4180
|
+
reason: `changed source path "${file}" has no declared impact target owner; fail closed to full impact`
|
|
4181
|
+
};
|
|
4182
|
+
}
|
|
4183
|
+
return {
|
|
4184
|
+
kind: "ok",
|
|
4185
|
+
targets: targets.map((target) => {
|
|
4186
|
+
const matched = sourceFiles.find((file) => matchesAnyGlob(target.paths ?? [], file));
|
|
4187
|
+
return matched === void 0 ? {
|
|
4188
|
+
basis: "no changed source path matches this target's declared paths",
|
|
4189
|
+
impact: "not-affected",
|
|
4190
|
+
name: target.name
|
|
4191
|
+
} : {
|
|
4192
|
+
basis: `changed source path "${matched}" matches this target's declared paths`,
|
|
4193
|
+
impact: "affected",
|
|
4194
|
+
name: target.name
|
|
4195
|
+
};
|
|
4196
|
+
})
|
|
4197
|
+
};
|
|
4198
|
+
};
|
|
4199
|
+
const unionTargetImpact = (source, lockfile) => {
|
|
4200
|
+
if (source.impact === "affected") return source;
|
|
4201
|
+
if (lockfile.impact === "affected") return lockfile;
|
|
4202
|
+
return {
|
|
4203
|
+
basis: `${source.basis}; ${lockfile.basis}`,
|
|
4204
|
+
impact: "not-affected",
|
|
4205
|
+
name: source.name
|
|
4206
|
+
};
|
|
4207
|
+
};
|
|
4208
|
+
const computeImpactStampOrThrow = ({ changedFiles, profilePath = "software-factory.profile.json", readLockfile }, targets) => {
|
|
4209
|
+
if (targets.length === 0) return {
|
|
4210
|
+
basis: "conservative",
|
|
4211
|
+
reasons: ["no impact targets declared in the profile; every surface keeps full demand"],
|
|
4212
|
+
stampVersion: 2,
|
|
4213
|
+
targets: []
|
|
4214
|
+
};
|
|
4215
|
+
assertDeclaredPathGlobs(targets);
|
|
4216
|
+
const sides = readLockfileSides(readLockfile);
|
|
4217
|
+
if (sides.kind === "ok") assertDeclaredImporterRoots(targets, sides.parsed);
|
|
4218
|
+
if (changedFiles.length === 0) return conservativeStamp(targets, ["no changed files detected; full impact assumed"]);
|
|
4219
|
+
const sourceFiles = changedFiles.filter((file) => file !== LOCKFILE_PATH);
|
|
4220
|
+
const source = analyzeSourcePaths(sourceFiles, targets, profilePath);
|
|
4221
|
+
if (source.kind === "conservative") return conservativeStamp(targets, [source.reason]);
|
|
4222
|
+
if (sides.kind === "threw") throw sides.error;
|
|
4223
|
+
if (sides.kind === "unreadable") return conservativeStamp(targets, [`pnpm-lock.yaml is unreadable at the ${sides.side} side; fail closed to full impact`]);
|
|
4224
|
+
if (sides.kind === "unparseable") return conservativeStamp(targets, [`pnpm-lock.yaml is unparseable or structurally malformed at the ${sides.side} side; fail closed to full impact`]);
|
|
4225
|
+
if (sides.kind === "unsupported-version") return conservativeStamp(targets, [`pnpm-lock.yaml is not lockfileVersion ${SUPPORTED_LOCKFILE_VERSION}; only pnpm v9 lockfiles are modelled; fail closed to full impact`]);
|
|
4226
|
+
if (!changedFiles.includes("pnpm-lock.yaml")) return {
|
|
4227
|
+
basis: "target-scoped",
|
|
4228
|
+
reasons: ["all changed source paths have declared target ownership; impact scoped by repo-relative path globs"],
|
|
4229
|
+
stampVersion: 2,
|
|
4230
|
+
targets: source.targets
|
|
4231
|
+
};
|
|
4232
|
+
const [baseLock, headLock] = sides.parsed;
|
|
4233
|
+
if (stableStringify$1(nonGraphSections(baseLock)) !== stableStringify$1(nonGraphSections(headLock))) return conservativeStamp(targets, ["pnpm-lock.yaml delta touches sections outside importers/packages/snapshots (e.g. settings, overrides, patchedDependencies); fail closed to full impact"]);
|
|
4234
|
+
const delta = {
|
|
4235
|
+
changedImporters: changedSectionKeys(baseLock.importers, headLock.importers),
|
|
4236
|
+
changedPackages: changedSectionKeys(baseLock.packages, headLock.packages),
|
|
4237
|
+
changedSnapshots: changedSectionKeys(baseLock.snapshots, headLock.snapshots),
|
|
4238
|
+
sides: [baseLock, headLock]
|
|
4239
|
+
};
|
|
4240
|
+
const lockfileTargets = targets.map((target) => targetImpactForLockfileDelta(target, delta));
|
|
4241
|
+
const mixed = sourceFiles.length > 0;
|
|
4242
|
+
return {
|
|
4243
|
+
basis: "target-scoped",
|
|
4244
|
+
reasons: mixed ? ["all changed source paths have declared target ownership and pnpm-lock.yaml is analyzable; impact is the union of path ownership and importer/snapshot graph analysis"] : ["pnpm-lock.yaml is the only changed file; impact scoped by importer/snapshot graph analysis"],
|
|
4245
|
+
stampVersion: 2,
|
|
4246
|
+
targets: mixed ? source.targets.map((sourceTarget, index) => unionTargetImpact(sourceTarget, lockfileTargets[index])) : lockfileTargets
|
|
4247
|
+
};
|
|
4248
|
+
};
|
|
4249
|
+
//#endregion
|
|
4250
|
+
//#region src/verification-battery.ts
|
|
4251
|
+
const UNCONDITIONAL_BASIS = "unconditional: the profile declares no impactTarget for this command";
|
|
4252
|
+
/**
|
|
4253
|
+
* The `vetoedTargets` contract, enforced at RUNTIME as well as at the type
|
|
4254
|
+
* level. TypeScript makes the parameter required for callers it compiles;
|
|
4255
|
+
* `new Set(undefined)` is a perfectly good empty set, so an untyped JavaScript
|
|
4256
|
+
* caller that omits it would otherwise scope exactly as if it had inspected
|
|
4257
|
+
* the envelopes and found nothing — the silent state requiring the parameter
|
|
4258
|
+
* exists to eliminate.
|
|
4259
|
+
*
|
|
4260
|
+
* A malformed CALL is API misuse, not data doubt: it fails loudly (ADR 0024)
|
|
4261
|
+
* rather than degrading to the conservative floor, because a producer that
|
|
4262
|
+
* never resolved the veto set has a bug its author must see.
|
|
4263
|
+
*/
|
|
4264
|
+
const assertVetoedTargets = (vetoedTargets, caller) => {
|
|
4265
|
+
if (!Array.isArray(vetoedTargets)) throw new TypeError(`${caller} requires vetoedTargets: the target names with a current, candidate-bound FAILING envelope. Resolve it with boundFailingCheckNames and pass the result; pass [] to mean "inspected the envelopes and found no bound failure". It has no default because omitting it would silently scope work whose standing demand nothing on this head could meet.`);
|
|
4266
|
+
};
|
|
4267
|
+
/**
|
|
4268
|
+
* Plan one verification battery against the candidate's impact stamp.
|
|
4269
|
+
*
|
|
4270
|
+
* Pure and total: it makes a decision, it never reads a proof, a profile file,
|
|
4271
|
+
* or the filesystem, and no INPUT can make it throw — every doubt path is a
|
|
4272
|
+
* decision, not an exception. A malformed CALL is the one exception to that,
|
|
4273
|
+
* and deliberately so: omitting `vetoedTargets` is API misuse rather than data
|
|
4274
|
+
* doubt, and it throws (see {@link assertVetoedTargets}).
|
|
4275
|
+
*
|
|
4276
|
+
* `stamp` must already be trusted by
|
|
4277
|
+
* the caller — inside `pr:verify` that is the stamp just computed for this
|
|
4278
|
+
* candidate's own identity triple; anywhere else it is `trustedImpactStamp`'s
|
|
4279
|
+
* output or `undefined`.
|
|
4280
|
+
*/
|
|
4281
|
+
const planVerificationBattery = ({ commands, stamp, vetoedTargets }) => {
|
|
4282
|
+
assertVetoedTargets(vetoedTargets, "planVerificationBattery");
|
|
4283
|
+
const vetoed = new Set(vetoedTargets);
|
|
4284
|
+
const dispositions = [];
|
|
4285
|
+
const execute = [];
|
|
4286
|
+
const notRequired = [];
|
|
4287
|
+
for (const command of commands) {
|
|
4288
|
+
const { impactTarget, name } = command;
|
|
4289
|
+
if (impactTarget === void 0) {
|
|
4290
|
+
dispositions.push({
|
|
4291
|
+
basis: UNCONDITIONAL_BASIS,
|
|
4292
|
+
disposition: "executed",
|
|
4293
|
+
name
|
|
4294
|
+
});
|
|
4295
|
+
execute.push(command);
|
|
4296
|
+
continue;
|
|
4297
|
+
}
|
|
4298
|
+
const decision = impactStampScopeDecision({
|
|
4299
|
+
stamp,
|
|
4300
|
+
surface: "verification-battery",
|
|
4301
|
+
targetName: impactTarget
|
|
4302
|
+
});
|
|
4303
|
+
const vetoedHere = decision.scoped && vetoed.has(impactTarget);
|
|
4304
|
+
const basis = vetoedHere ? `veto: a current envelope bound to this candidate records target "${impactTarget}" as FAILING, so the stamp's release is withheld (${decision.reason})` : decision.reason;
|
|
4305
|
+
const scoped = decision.scoped && !vetoedHere;
|
|
4306
|
+
dispositions.push({
|
|
4307
|
+
basis,
|
|
4308
|
+
disposition: scoped ? "not-required" : "executed",
|
|
4309
|
+
impactTarget,
|
|
4310
|
+
name
|
|
4311
|
+
});
|
|
4312
|
+
if (scoped) notRequired.push({
|
|
4313
|
+
basis,
|
|
4314
|
+
impactTarget,
|
|
4315
|
+
name
|
|
4316
|
+
});
|
|
4317
|
+
else execute.push(command);
|
|
4318
|
+
}
|
|
4319
|
+
return {
|
|
4320
|
+
dispositions,
|
|
4321
|
+
execute,
|
|
4322
|
+
notRequired
|
|
4323
|
+
};
|
|
4324
|
+
};
|
|
4325
|
+
/**
|
|
4326
|
+
* The completeness invariant as an enforceable control (ADR 0024: controls
|
|
4327
|
+
* fail loudly), shared by both pr:verify proof schemas so they cannot drift.
|
|
4328
|
+
*
|
|
4329
|
+
* Four rules:
|
|
4330
|
+
* 1. A withheld command names one the resolved mode actually selected.
|
|
4331
|
+
* 2. A command is never recorded as both executed and not-required.
|
|
4332
|
+
* 3. Withheld commands are distinct — a duplicated disposition would let one
|
|
4333
|
+
* name carry two different bases.
|
|
4334
|
+
* 4. On a PASSED proof, every selected command has a disposition: silence is
|
|
4335
|
+
* not a disposition, and "absent from both lists" is exactly the silent
|
|
4336
|
+
* skip this epic forbids.
|
|
4337
|
+
*
|
|
4338
|
+
* Rule 4 is scoped to `outcome: "passed"` because an ABORTED proof legitimately
|
|
4339
|
+
* records partial execution — the run stopped at the failing command. It is
|
|
4340
|
+
* also scoped to schemaVersion >= 4: `executedCommands` postdates v1–v3, so a
|
|
4341
|
+
* legacy proof that never recorded it is not making a false completeness claim.
|
|
4342
|
+
*
|
|
4343
|
+
* The check is one-directional on purpose. Every SELECTED command must be
|
|
4344
|
+
* accounted for, but `executedCommands` may legitimately carry MORE than the
|
|
4345
|
+
* profile selected — full mode appends the `workspace:install-resolves` probe,
|
|
4346
|
+
* which is real work no profile declares.
|
|
4347
|
+
*/
|
|
4348
|
+
const assertBatteryCompleteness = (proof, context) => {
|
|
4349
|
+
const selected = proof.verificationCommands.map((command) => command.name);
|
|
4350
|
+
const selectedSet = new Set(selected);
|
|
4351
|
+
const executed = new Set((proof.executedCommands ?? []).map((command) => command.name));
|
|
4352
|
+
const notRequired = proof.notRequiredCommands ?? [];
|
|
4353
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4354
|
+
for (const [index, entry] of notRequired.entries()) {
|
|
4355
|
+
if (!selectedSet.has(entry.name)) context.addIssue({
|
|
4356
|
+
code: "custom",
|
|
4357
|
+
message: `notRequiredCommands entry "${entry.name}" is not one of this proof's verificationCommands.`,
|
|
4358
|
+
path: [
|
|
4359
|
+
"notRequiredCommands",
|
|
4360
|
+
index,
|
|
4361
|
+
"name"
|
|
4362
|
+
]
|
|
4363
|
+
});
|
|
4364
|
+
if (executed.has(entry.name)) context.addIssue({
|
|
4365
|
+
code: "custom",
|
|
4366
|
+
message: `Command "${entry.name}" is recorded both as executed and as not-required.`,
|
|
4367
|
+
path: [
|
|
4368
|
+
"notRequiredCommands",
|
|
4369
|
+
index,
|
|
4370
|
+
"name"
|
|
4371
|
+
]
|
|
4372
|
+
});
|
|
4373
|
+
if (seen.has(entry.name)) context.addIssue({
|
|
4374
|
+
code: "custom",
|
|
4375
|
+
message: `notRequiredCommands records "${entry.name}" more than once.`,
|
|
4376
|
+
path: [
|
|
4377
|
+
"notRequiredCommands",
|
|
4378
|
+
index,
|
|
4379
|
+
"name"
|
|
4380
|
+
]
|
|
4381
|
+
});
|
|
4382
|
+
seen.add(entry.name);
|
|
4383
|
+
}
|
|
4384
|
+
if (proof.outcome !== "passed" || proof.schemaVersion < 4) return;
|
|
4385
|
+
const disposed = new Set([...executed, ...seen]);
|
|
4386
|
+
const undisposed = selected.filter((name) => !disposed.has(name));
|
|
4387
|
+
if (undisposed.length > 0) context.addIssue({
|
|
4388
|
+
code: "custom",
|
|
4389
|
+
message: `A passed pr:verify proof must give every selected verification command a disposition; [${undisposed.join(", ")}] appear in verificationCommands but in neither executedCommands nor notRequiredCommands.`,
|
|
4390
|
+
path: ["executedCommands"]
|
|
4391
|
+
});
|
|
4392
|
+
};
|
|
4393
|
+
/**
|
|
4394
|
+
* The stamp-authorization control: a passed v4 proof may only claim a command
|
|
4395
|
+
* was NOT REQUIRED if its own recorded stamp says so.
|
|
4396
|
+
*
|
|
4397
|
+
* `assertBatteryCompleteness` proves the two lists partition the selected
|
|
4398
|
+
* commands — that no command is silently absent. It does not prove the
|
|
4399
|
+
* withholding was EARNED. Without this rule a proof could name every expensive
|
|
4400
|
+
* command in `notRequiredCommands`, carry no stamp at all (or a conservative
|
|
4401
|
+
* one), and still read as full verification: the omission would be recorded,
|
|
4402
|
+
* accounted for, and completely unauthorized.
|
|
4403
|
+
*
|
|
4404
|
+
* Two things have to hold, and the first is what keeps the second honest:
|
|
4405
|
+
*
|
|
4406
|
+
* 1. COHERENCE. The entry's `impactTarget` must equal the `impactTarget` the
|
|
4407
|
+
* proof records for that same command in `verificationCommands`. The entry
|
|
4408
|
+
* does not get to nominate its own target; the proof's command→target
|
|
4409
|
+
* mapping (written by `pr:verify` from the loaded profile) does. Without
|
|
4410
|
+
* this, authorization validates a self-reported field and a crafted proof
|
|
4411
|
+
* can withhold an affected command while pointing at an unrelated released
|
|
4412
|
+
* target.
|
|
4413
|
+
* 2. AUTHORIZATION. That recorded target must be one the proof's own stamp
|
|
4414
|
+
* provably released.
|
|
4415
|
+
*
|
|
4416
|
+
* The proof is one artifact and a determined forger controls all of it; this
|
|
4417
|
+
* is internal-coherence belt-and-braces in the same trust domain as the
|
|
4418
|
+
* version guards, and write-time truth stays `pr:verify`'s job.
|
|
4419
|
+
*
|
|
4420
|
+
* The authorizing evidence is the proof's OWN `impactStamp` — the same stamp
|
|
4421
|
+
* `pr:verify` computed for this candidate's identity triple and recorded here,
|
|
4422
|
+
* so authorization is bound to the same identities the proof binds. The
|
|
4423
|
+
* predicate is the shared {@link impactStampScopeDecision}, not a second
|
|
4424
|
+
* reading of the stamp: an entry is authorized exactly when the stamp would
|
|
4425
|
+
* have released that target's command in the first place (target-scoped
|
|
4426
|
+
* basis, this build's stamp version, exactly one row for the target,
|
|
4427
|
+
* `not-affected`). A conservative stamp, an unknown version, a
|
|
4428
|
+
* self-contradictory stamp, an unclassified name, or an `affected` verdict all
|
|
4429
|
+
* fail the same way scoping itself would.
|
|
4430
|
+
*
|
|
4431
|
+
* Scoped to `outcome: "passed"` and schemaVersion >= 4 for the same reasons as
|
|
4432
|
+
* the completeness rule: an aborted run records partial execution, and a
|
|
4433
|
+
* legacy proof predates both fields, so neither is making a false completeness
|
|
4434
|
+
* claim.
|
|
4435
|
+
*/
|
|
4436
|
+
const assertNotRequiredStampAuthorization = (proof, context) => {
|
|
4437
|
+
const notRequired = proof.notRequiredCommands ?? [];
|
|
4438
|
+
if (proof.outcome !== "passed" || proof.schemaVersion < 4 || notRequired.length === 0) return;
|
|
4439
|
+
const { impactStamp } = proof;
|
|
4440
|
+
if (impactStamp === void 0) {
|
|
4441
|
+
context.addIssue({
|
|
4442
|
+
code: "custom",
|
|
4443
|
+
message: `A passed pr:verify proof that withholds verification commands must record the impact stamp that authorized the withholding; [${notRequired.map((entry) => entry.name).join(", ")}] are recorded not-required by a proof carrying no impactStamp.`,
|
|
4444
|
+
path: ["impactStamp"]
|
|
4445
|
+
});
|
|
4446
|
+
return;
|
|
4447
|
+
}
|
|
4448
|
+
const recordedTargets = new Map(proof.verificationCommands.map((command) => [command.name, command.impactTarget]));
|
|
4449
|
+
for (const [index, entry] of notRequired.entries()) {
|
|
4450
|
+
const recorded = recordedTargets.get(entry.name);
|
|
4451
|
+
if (recorded !== entry.impactTarget) {
|
|
4452
|
+
context.addIssue({
|
|
4453
|
+
code: "custom",
|
|
4454
|
+
message: recorded === void 0 ? `notRequiredCommands entry "${entry.name}" claims impactTarget "${entry.impactTarget}", but this proof records no impactTarget for that command; only a command the profile scopes can be withheld.` : `notRequiredCommands entry "${entry.name}" claims impactTarget "${entry.impactTarget}", but this proof records impactTarget "${recorded}" for that command.`,
|
|
4455
|
+
path: [
|
|
4456
|
+
"notRequiredCommands",
|
|
4457
|
+
index,
|
|
4458
|
+
"impactTarget"
|
|
4459
|
+
]
|
|
4460
|
+
});
|
|
4461
|
+
continue;
|
|
4462
|
+
}
|
|
4463
|
+
const decision = impactStampScopeDecision({
|
|
4464
|
+
stamp: impactStamp,
|
|
4465
|
+
surface: "verification-battery",
|
|
4466
|
+
targetName: entry.impactTarget
|
|
4467
|
+
});
|
|
4468
|
+
if (!decision.scoped) context.addIssue({
|
|
4469
|
+
code: "custom",
|
|
4470
|
+
message: `notRequiredCommands entry "${entry.name}" is not authorized by this proof's impactStamp: ${decision.reason}.`,
|
|
4471
|
+
path: [
|
|
4472
|
+
"notRequiredCommands",
|
|
4473
|
+
index,
|
|
4474
|
+
"impactTarget"
|
|
4475
|
+
]
|
|
4476
|
+
});
|
|
4477
|
+
}
|
|
4478
|
+
};
|
|
4479
|
+
/** Console lines naming every withheld command and why. Never silent. */
|
|
4480
|
+
const batteryScopeSummaryLines = (plan) => {
|
|
4481
|
+
if (plan.notRequired.length === 0) return [];
|
|
4482
|
+
return ["Verification battery scoped by the impact stamp (not-required):", ...plan.notRequired.map((entry) => `- ${entry.name} (${entry.impactTarget}): ${entry.basis}`)];
|
|
4483
|
+
};
|
|
4484
|
+
//#endregion
|
|
4485
|
+
//#region src/pr-verify-proof-rules.ts
|
|
4486
|
+
const assertPrVerifyProofRules = (proof, context) => {
|
|
4487
|
+
if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
|
|
4488
|
+
code: "custom",
|
|
4489
|
+
message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
|
|
4490
|
+
path: ["authoringSession"]
|
|
4491
|
+
});
|
|
4492
|
+
if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
|
|
4493
|
+
code: "custom",
|
|
4494
|
+
message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
4495
|
+
path: ["authoringSession"]
|
|
4496
|
+
});
|
|
4497
|
+
if (proof.schemaVersion < 4 && proof.impactStamp !== void 0) context.addIssue({
|
|
4498
|
+
code: "custom",
|
|
4499
|
+
message: "impactStamp is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
4500
|
+
path: ["impactStamp"]
|
|
4501
|
+
});
|
|
4502
|
+
if (proof.schemaVersion < 4 && proof.notRequiredCommands !== void 0) context.addIssue({
|
|
4503
|
+
code: "custom",
|
|
4504
|
+
message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
4505
|
+
path: ["notRequiredCommands"]
|
|
4506
|
+
});
|
|
4507
|
+
assertBatteryCompleteness(proof, context);
|
|
4508
|
+
assertNotRequiredStampAuthorization(proof, context);
|
|
4509
|
+
};
|
|
3795
4510
|
const LEGACY_CLOSEOUT_SCHEMA_VERSION = 3;
|
|
3796
4511
|
const MAX_CLOSEOUT_ROWS = 1e3;
|
|
3797
4512
|
const IdentifierSchema = z.string().min(1).max(500);
|
|
@@ -4297,6 +5012,11 @@ const executedCommandSchema$1 = z.object({
|
|
|
4297
5012
|
"full"
|
|
4298
5013
|
])
|
|
4299
5014
|
});
|
|
5015
|
+
const notRequiredCommandSchema$1 = z.object({
|
|
5016
|
+
basis: z.string().min(1),
|
|
5017
|
+
impactTarget: z.string().min(1),
|
|
5018
|
+
name: z.string().min(1)
|
|
5019
|
+
});
|
|
4300
5020
|
const prVerifySchemaVersionSchema$1 = z.number().refine((value) => SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS$1.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS$1.join(", ")}` });
|
|
4301
5021
|
z.object({
|
|
4302
5022
|
authoringSession: z.string().trim().min(1).optional(),
|
|
@@ -4317,12 +5037,14 @@ z.object({
|
|
|
4317
5037
|
endedAt: z.iso.datetime(),
|
|
4318
5038
|
executedCommands: z.array(executedCommandSchema$1).optional(),
|
|
4319
5039
|
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
5040
|
+
impactStamp: impactStampSchema.optional(),
|
|
4320
5041
|
mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
4321
5042
|
mode: z.enum([
|
|
4322
5043
|
"docs-only",
|
|
4323
5044
|
"trivial",
|
|
4324
5045
|
"full"
|
|
4325
5046
|
]),
|
|
5047
|
+
notRequiredCommands: z.array(notRequiredCommandSchema$1).min(1).optional(),
|
|
4326
5048
|
outcome: z.enum(["aborted", "passed"]).optional(),
|
|
4327
5049
|
patchId: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
4328
5050
|
profilePath: z.string().min(1),
|
|
@@ -4333,6 +5055,7 @@ z.object({
|
|
|
4333
5055
|
verificationCommands: z.array(z.object({
|
|
4334
5056
|
command: z.string().min(1),
|
|
4335
5057
|
description: z.string().min(1),
|
|
5058
|
+
impactTarget: z.string().min(1).optional(),
|
|
4336
5059
|
name: z.string().min(1),
|
|
4337
5060
|
scope: z.enum([
|
|
4338
5061
|
"always",
|
|
@@ -4341,18 +5064,7 @@ z.object({
|
|
|
4341
5064
|
"full"
|
|
4342
5065
|
])
|
|
4343
5066
|
}))
|
|
4344
|
-
}).superRefine(
|
|
4345
|
-
if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
|
|
4346
|
-
code: "custom",
|
|
4347
|
-
message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
|
|
4348
|
-
path: ["authoringSession"]
|
|
4349
|
-
});
|
|
4350
|
-
if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
|
|
4351
|
-
code: "custom",
|
|
4352
|
-
message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
4353
|
-
path: ["authoringSession"]
|
|
4354
|
-
});
|
|
4355
|
-
});
|
|
5067
|
+
}).superRefine(assertPrVerifyProofRules);
|
|
4356
5068
|
const PR_REVIEW_FINDING_PROVENANCE_VERSION$1 = 1;
|
|
4357
5069
|
const nonBlankString$3 = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
|
|
4358
5070
|
const parseableDateString$1 = z.string().refine((value) => Number.isFinite(Date.parse(value)), { message: "must be a parseable date string" });
|
|
@@ -9403,6 +10115,7 @@ var external_evidence_exports = /* @__PURE__ */ __exportAll({
|
|
|
9403
10115
|
EVIDENCE_DIR: () => EVIDENCE_DIR,
|
|
9404
10116
|
EVIDENCE_ENVELOPE_SCHEMA_VERSION: () => 1,
|
|
9405
10117
|
EVIDENCE_REVIEW_RUNGS: () => EVIDENCE_REVIEW_RUNGS$1,
|
|
10118
|
+
boundFailingCheckNames: () => boundFailingCheckNames,
|
|
9406
10119
|
evaluateRequiredChecks: () => evaluateRequiredChecks,
|
|
9407
10120
|
evidenceBindingFresh: () => evidenceBindingFresh,
|
|
9408
10121
|
evidenceEnvelopeSchema: () => evidenceEnvelopeSchema,
|
|
@@ -9491,6 +10204,41 @@ const evidenceReviewIndependence = ({ authoringSessionIds, envelope }) => {
|
|
|
9491
10204
|
};
|
|
9492
10205
|
return { independent: true };
|
|
9493
10206
|
};
|
|
10207
|
+
/**
|
|
10208
|
+
* The VETO predicate: does a current envelope, bound to exactly this
|
|
10209
|
+
* candidate, record a FAILING outcome for this check?
|
|
10210
|
+
*
|
|
10211
|
+
* An inferred graph verdict never silences a direct observation (ADR 0024:
|
|
10212
|
+
* controls fail loudly). Wave 2 uses this to withhold a demand release; wave 3
|
|
10213
|
+
* (#430) uses the same predicate — via {@link boundFailingCheckNames} — to
|
|
10214
|
+
* withhold battery and preview-lifecycle scoping. It is ONE matcher on
|
|
10215
|
+
* purpose: two vetoes that matched differently could withhold a demand while
|
|
10216
|
+
* releasing the work that would satisfy it, leaving a demand nothing on this
|
|
10217
|
+
* head can ever meet.
|
|
10218
|
+
*/
|
|
10219
|
+
const envelopeRecordsBoundFailure = ({ candidate, check, envelope }) => envelope.check === check.name && envelope.checkType === check.checkType && envelope.outcome === "fail" && evidenceBindingFresh({
|
|
10220
|
+
candidate,
|
|
10221
|
+
envelope
|
|
10222
|
+
});
|
|
10223
|
+
/**
|
|
10224
|
+
* Every check name with a current, candidate-bound FAILING envelope on disk.
|
|
10225
|
+
*
|
|
10226
|
+
* The wave-3 scoping veto input. Callers pass the resolved set to
|
|
10227
|
+
* `planVerificationBattery` / `planPreviewLifecycle`, which stay pure — the
|
|
10228
|
+
* planners decide, they never read the filesystem.
|
|
10229
|
+
*
|
|
10230
|
+
* `checkType` is the caller's, not a guess: schema-v5 requiredChecks are
|
|
10231
|
+
* verify-only (`resolveProfileRequiredChecks`), so a scoping caller passes
|
|
10232
|
+
* `"verify"` and matches exactly the pair wave 2 evaluates.
|
|
10233
|
+
*/
|
|
10234
|
+
const boundFailingCheckNames = ({ candidate, checkType, envelopes }) => [...new Set(envelopes.flatMap((loaded) => loaded.ok && envelopeRecordsBoundFailure({
|
|
10235
|
+
candidate,
|
|
10236
|
+
check: {
|
|
10237
|
+
checkType,
|
|
10238
|
+
name: loaded.envelope.check
|
|
10239
|
+
},
|
|
10240
|
+
envelope: loaded.envelope
|
|
10241
|
+
}) ? [loaded.envelope.check] : []))];
|
|
9494
10242
|
const bindingRefusalReason = (name) => `Required check "${name}" has no passing, current envelope bound to the candidate delta (patchId).`;
|
|
9495
10243
|
/**
|
|
9496
10244
|
* The `evidence:emit` invocation that would satisfy a specific demanded check.
|
|
@@ -9618,6 +10366,23 @@ const evaluateRequiredChecks = ({ authoringSessionIds, candidate, envelopes, req
|
|
|
9618
10366
|
scopeReason: scopeDecision.reason,
|
|
9619
10367
|
status: "out-of-scope"
|
|
9620
10368
|
};
|
|
10369
|
+
const release = impactStampDemandRelease({
|
|
10370
|
+
checkName: check.name,
|
|
10371
|
+
stamp: scopeContext.impactStamp
|
|
10372
|
+
});
|
|
10373
|
+
const boundFailureObserved = parsed.some((loaded) => envelopeRecordsBoundFailure({
|
|
10374
|
+
candidate,
|
|
10375
|
+
check,
|
|
10376
|
+
envelope: loaded.envelope
|
|
10377
|
+
}));
|
|
10378
|
+
if (release.released && !boundFailureObserved) return {
|
|
10379
|
+
checkType: check.checkType,
|
|
10380
|
+
inScope: false,
|
|
10381
|
+
name: check.name,
|
|
10382
|
+
...check.scope ? { scope: check.scope } : {},
|
|
10383
|
+
scopeReason: release.reason,
|
|
10384
|
+
status: "out-of-scope"
|
|
10385
|
+
};
|
|
9621
10386
|
return {
|
|
9622
10387
|
...evaluateInScopeCheck({
|
|
9623
10388
|
authoringSessionIds,
|
|
@@ -9790,6 +10555,7 @@ var verification_proof_exports = /* @__PURE__ */ __exportAll({
|
|
|
9790
10555
|
prVerifyProofHeadShas: () => prVerifyProofHeadShas,
|
|
9791
10556
|
readPrVerifyProof: () => readPrVerifyProof,
|
|
9792
10557
|
resolveVerifyProofApplicability: () => resolveVerifyProofApplicability,
|
|
10558
|
+
trustedImpactStamp: () => trustedImpactStamp,
|
|
9793
10559
|
validatePrVerifyProof: () => validatePrVerifyProof,
|
|
9794
10560
|
verificationProofApplicabilityFor: () => verificationProofApplicabilityFor,
|
|
9795
10561
|
verificationProofBlockingReason: () => verificationProofBlockingReason,
|
|
@@ -9820,6 +10586,11 @@ const executedCommandSchema = z.object({
|
|
|
9820
10586
|
"full"
|
|
9821
10587
|
])
|
|
9822
10588
|
});
|
|
10589
|
+
const notRequiredCommandSchema = z.object({
|
|
10590
|
+
basis: z.string().min(1),
|
|
10591
|
+
impactTarget: z.string().min(1),
|
|
10592
|
+
name: z.string().min(1)
|
|
10593
|
+
});
|
|
9823
10594
|
const prVerifySchemaVersionSchema = z.number().refine((value) => SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.join(", ")}` });
|
|
9824
10595
|
const prVerifyProofSchema = z.object({
|
|
9825
10596
|
authoringSession: z.string().trim().min(1).optional(),
|
|
@@ -9844,12 +10615,14 @@ const prVerifyProofSchema = z.object({
|
|
|
9844
10615
|
endedAt: z.iso.datetime(),
|
|
9845
10616
|
executedCommands: z.array(executedCommandSchema).optional(),
|
|
9846
10617
|
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
10618
|
+
impactStamp: impactStampSchema.optional(),
|
|
9847
10619
|
mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
9848
10620
|
mode: z.enum([
|
|
9849
10621
|
"docs-only",
|
|
9850
10622
|
"trivial",
|
|
9851
10623
|
"full"
|
|
9852
10624
|
]),
|
|
10625
|
+
notRequiredCommands: z.array(notRequiredCommandSchema).min(1).optional(),
|
|
9853
10626
|
outcome: z.enum(["aborted", "passed"]).optional(),
|
|
9854
10627
|
patchId: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
9855
10628
|
profilePath: z.string().min(1),
|
|
@@ -9860,6 +10633,7 @@ const prVerifyProofSchema = z.object({
|
|
|
9860
10633
|
verificationCommands: z.array(z.object({
|
|
9861
10634
|
command: z.string().min(1),
|
|
9862
10635
|
description: z.string().min(1),
|
|
10636
|
+
impactTarget: z.string().min(1).optional(),
|
|
9863
10637
|
name: z.string().min(1),
|
|
9864
10638
|
scope: z.enum([
|
|
9865
10639
|
"always",
|
|
@@ -9868,18 +10642,7 @@ const prVerifyProofSchema = z.object({
|
|
|
9868
10642
|
"full"
|
|
9869
10643
|
])
|
|
9870
10644
|
}))
|
|
9871
|
-
}).superRefine(
|
|
9872
|
-
if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
|
|
9873
|
-
code: "custom",
|
|
9874
|
-
message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
|
|
9875
|
-
path: ["authoringSession"]
|
|
9876
|
-
});
|
|
9877
|
-
if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
|
|
9878
|
-
code: "custom",
|
|
9879
|
-
message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
9880
|
-
path: ["authoringSession"]
|
|
9881
|
-
});
|
|
9882
|
-
});
|
|
10645
|
+
}).superRefine(assertPrVerifyProofRules);
|
|
9883
10646
|
function validatePrVerifyProof(value) {
|
|
9884
10647
|
return prVerifyProofSchema.parse(value);
|
|
9885
10648
|
}
|
|
@@ -9920,6 +10683,29 @@ const verifyProofIdentityMatches = ({ currentIdentity, proof }) => Boolean(proof
|
|
|
9920
10683
|
recorded: { patchId: proof.patchId },
|
|
9921
10684
|
rule: "patch-id"
|
|
9922
10685
|
}));
|
|
10686
|
+
/**
|
|
10687
|
+
* Wave-2 consumption (#542): the recorded impact stamp, released for demand
|
|
10688
|
+
* resolution ONLY when it is identity-bound to the current candidate — the
|
|
10689
|
+
* proof passed, and its recorded headSha AND patchId both name exactly this
|
|
10690
|
+
* candidate (the same identities the proof already binds). Everything else —
|
|
10691
|
+
* no proof, an aborted run, a stale head, a moved patch id, a pre-stamp proof
|
|
10692
|
+
* — returns `undefined`, and the consumer keeps today's full-demand floor.
|
|
10693
|
+
*
|
|
10694
|
+
* Deliberately stricter than verify-once applicability (which tolerates a
|
|
10695
|
+
* head-only or patch-id-only match): a stamp narrows external demands, so it
|
|
10696
|
+
* is trusted only on an exact double binding, fail closed.
|
|
10697
|
+
*/
|
|
10698
|
+
const trustedImpactStamp = ({ candidate, proof }) => {
|
|
10699
|
+
if (!proof || !verifyProofPassed(proof)) return;
|
|
10700
|
+
if (proof.schemaVersion < 4) return;
|
|
10701
|
+
const headBound = sameHeadSha(candidate.headSha, proof.headSha);
|
|
10702
|
+
const patchBound = evidenceFresh({
|
|
10703
|
+
candidate: { patchId: candidate.patchId },
|
|
10704
|
+
recorded: { patchId: proof.patchId },
|
|
10705
|
+
rule: "patch-id"
|
|
10706
|
+
});
|
|
10707
|
+
return headBound && patchBound ? proof.impactStamp : void 0;
|
|
10708
|
+
};
|
|
9923
10709
|
const prVerifyFullHeadShasForDiff = ({ docsOnlyDeltaAcceptedSince, files, proof }) => {
|
|
9924
10710
|
if (!verifyProofPassed(proof)) return [];
|
|
9925
10711
|
if (proof.mode === "full") return sameFileList(proof.changedFiles, files) ? [proof.headSha] : [];
|
|
@@ -10287,6 +11073,7 @@ function defaultPrVerifyGit() {
|
|
|
10287
11073
|
exitCode: result.exitCode
|
|
10288
11074
|
};
|
|
10289
11075
|
},
|
|
11076
|
+
showFileAtRef,
|
|
10290
11077
|
stablePatchId: runStablePatchId,
|
|
10291
11078
|
statusPorcelain
|
|
10292
11079
|
};
|
|
@@ -10316,7 +11103,7 @@ function baselineFullProofsForDocsOnly({ previousProof, profilePath, projectKey,
|
|
|
10316
11103
|
seedFilter: matchesProfile
|
|
10317
11104
|
});
|
|
10318
11105
|
}
|
|
10319
|
-
function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, classification, commands, endedAt, executedCommands, files, headSha, mode, mergeBaseSha: proofMergeBaseSha, outcome, patchId, profilePath, projectKey, repository, startedAt }) {
|
|
11106
|
+
function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, classification, commands, endedAt, executedCommands, files, headSha, impactStamp, mode, mergeBaseSha: proofMergeBaseSha, notRequiredCommands, outcome, patchId, profilePath, projectKey, repository, startedAt }) {
|
|
10320
11107
|
return {
|
|
10321
11108
|
authoringSession,
|
|
10322
11109
|
base,
|
|
@@ -10329,8 +11116,10 @@ function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, classi
|
|
|
10329
11116
|
endedAt: endedAt.toISOString(),
|
|
10330
11117
|
executedCommands,
|
|
10331
11118
|
headSha,
|
|
11119
|
+
impactStamp,
|
|
10332
11120
|
mergeBaseSha: proofMergeBaseSha,
|
|
10333
11121
|
mode,
|
|
11122
|
+
...notRequiredCommands.length > 0 ? { notRequiredCommands } : {},
|
|
10334
11123
|
outcome,
|
|
10335
11124
|
patchId,
|
|
10336
11125
|
profilePath,
|
|
@@ -10375,9 +11164,10 @@ function resolveMode({ classification, mode }) {
|
|
|
10375
11164
|
return mode;
|
|
10376
11165
|
}
|
|
10377
11166
|
function commandsForMode(profile, mode) {
|
|
10378
|
-
return profile.verification.commands.filter((command) => commandAppliesToMode(command, mode)).map(({ command, description, name, scope }) => ({
|
|
11167
|
+
return profile.verification.commands.filter((command) => commandAppliesToMode(command, mode)).map(({ command, description, impactTarget, name, scope }) => ({
|
|
10379
11168
|
command,
|
|
10380
11169
|
description,
|
|
11170
|
+
...impactTarget === void 0 ? {} : { impactTarget },
|
|
10381
11171
|
name,
|
|
10382
11172
|
scope
|
|
10383
11173
|
}));
|
|
@@ -10432,6 +11222,36 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
10432
11222
|
cwd,
|
|
10433
11223
|
git
|
|
10434
11224
|
});
|
|
11225
|
+
const readLockfileAtRef = git.showFileAtRef ?? showFileAtRef;
|
|
11226
|
+
let impactStamp;
|
|
11227
|
+
try {
|
|
11228
|
+
impactStamp = computeImpactStamp({
|
|
11229
|
+
changedFiles: files,
|
|
11230
|
+
profile,
|
|
11231
|
+
profilePath: relativeProfile,
|
|
11232
|
+
readLockfile: (side) => readLockfileAtRef(cwd, side === "base" ? proofMergeBaseSha : headSha, path.resolve(cwd, LOCKFILE_PATH))
|
|
11233
|
+
});
|
|
11234
|
+
} catch (error) {
|
|
11235
|
+
if (error instanceof ImpactStampConfigError) throw error;
|
|
11236
|
+
impactStamp = conservativeImpactStamp(profile.impact?.targets ?? [], [`impact stamp computation failed (${error instanceof Error ? error.message : String(error)}); fail closed to full impact`]);
|
|
11237
|
+
}
|
|
11238
|
+
const vetoedTargets = boundFailingCheckNames({
|
|
11239
|
+
candidate: {
|
|
11240
|
+
headSha,
|
|
11241
|
+
mergeBaseSha: proofMergeBaseSha,
|
|
11242
|
+
patchId
|
|
11243
|
+
},
|
|
11244
|
+
checkType: "verify",
|
|
11245
|
+
envelopes: loadEvidenceEnvelopes(cwd)
|
|
11246
|
+
});
|
|
11247
|
+
const battery = planVerificationBattery({
|
|
11248
|
+
commands: selectedProfileCommands,
|
|
11249
|
+
stamp: impactStamp,
|
|
11250
|
+
vetoedTargets
|
|
11251
|
+
});
|
|
11252
|
+
const notRequiredCommands = battery.notRequired;
|
|
11253
|
+
const scopedOutNames = new Set(notRequiredCommands.map(({ name }) => name));
|
|
11254
|
+
for (const line of batteryScopeSummaryLines(battery)) console.log(line);
|
|
10435
11255
|
const repository = `${profile.repository.owner}/${profile.repository.name}`;
|
|
10436
11256
|
let gateState = "failure";
|
|
10437
11257
|
const { env: verificationEnv, cleanup } = context.buildEnv();
|
|
@@ -10455,8 +11275,10 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
10455
11275
|
commands,
|
|
10456
11276
|
files,
|
|
10457
11277
|
headSha,
|
|
11278
|
+
impactStamp,
|
|
10458
11279
|
mergeBaseSha: proofMergeBaseSha,
|
|
10459
11280
|
mode: resolvedMode,
|
|
11281
|
+
notRequiredCommands,
|
|
10460
11282
|
patchId,
|
|
10461
11283
|
profilePath,
|
|
10462
11284
|
projectKey: profile.repository.name,
|
|
@@ -10488,6 +11310,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
10488
11310
|
};
|
|
10489
11311
|
try {
|
|
10490
11312
|
for (const command of commands) {
|
|
11313
|
+
if (scopedOutNames.has(command.name)) continue;
|
|
10491
11314
|
console.log(`\n> ${command.name}: ${command.command}`);
|
|
10492
11315
|
const result = git.runVerificationCommand(command.command, cwd, verificationEnv);
|
|
10493
11316
|
const executedCommand = {
|
|
@@ -10554,6 +11377,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
10554
11377
|
});
|
|
10555
11378
|
for (const command of selectedProfileCommands) {
|
|
10556
11379
|
if (!command.requiredCheck) continue;
|
|
11380
|
+
if (scopedOutNames.has(command.name)) continue;
|
|
10557
11381
|
runEvidenceEmit({
|
|
10558
11382
|
base: args.base,
|
|
10559
11383
|
check: command.requiredCheck,
|
|
@@ -11235,6 +12059,13 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
|
|
|
11235
12059
|
name: "admission:required-checks",
|
|
11236
12060
|
status: "warning"
|
|
11237
12061
|
}];
|
|
12062
|
+
const impactStamp = trustedImpactStamp({
|
|
12063
|
+
candidate: {
|
|
12064
|
+
headSha: candidate.headSha,
|
|
12065
|
+
patchId: candidate.patchId
|
|
12066
|
+
},
|
|
12067
|
+
proof: verifyProof
|
|
12068
|
+
});
|
|
11238
12069
|
const { outcomes } = evaluateRequiredChecks({
|
|
11239
12070
|
authoringSessionIds: resolveAuthoringSessionIds({ recorded: verifyProof?.authoringSession }),
|
|
11240
12071
|
candidate: {
|
|
@@ -11244,7 +12075,10 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
|
|
|
11244
12075
|
},
|
|
11245
12076
|
envelopes: loadEvidenceEnvelopes(cwd),
|
|
11246
12077
|
requiredChecks,
|
|
11247
|
-
scopeContext: {
|
|
12078
|
+
scopeContext: {
|
|
12079
|
+
classification: candidate.classification,
|
|
12080
|
+
...impactStamp ? { impactStamp } : {}
|
|
12081
|
+
}
|
|
11248
12082
|
});
|
|
11249
12083
|
return outcomes.map((outcome) => ({
|
|
11250
12084
|
message: requiredCheckMessage(outcome),
|
|
@@ -12638,8 +13472,8 @@ const inactiveMergeFreezeStateSchema = z.object({
|
|
|
12638
13472
|
});
|
|
12639
13473
|
const mergeFreezeStateSchema = z.discriminatedUnion("active", [activeMergeFreezeStateSchema, inactiveMergeFreezeStateSchema]);
|
|
12640
13474
|
/**
|
|
12641
|
-
* The write side of this contract lives in the generated
|
|
12642
|
-
* workflow (#356, ADR 0016 as amended): it is the ONLY producer of
|
|
13475
|
+
* The write side of this contract lives in the generated merge-target push
|
|
13476
|
+
* Verify workflow (#356, ADR 0016 as amended; #429): it is the ONLY producer of
|
|
12643
13477
|
* `patronage-factory/merge-freeze` generations. Its emitted `output.text`
|
|
12644
13478
|
* payload must parse under this exact reader schema, which is what the
|
|
12645
13479
|
* workflow's own tests assert through this export.
|
|
@@ -12732,7 +13566,7 @@ function selectListedMergeFreezeGeneration(listed, expectedHeadSha) {
|
|
|
12732
13566
|
function unconfiguredMergeFreeze(input, detail) {
|
|
12733
13567
|
return {
|
|
12734
13568
|
kind: "unreadable",
|
|
12735
|
-
reason: `The ${MERGE_FREEZE_CHECK_NAME} writer is unconfigured or its settle window cannot be confirmed for base ${input.headSha}: ${detail} Generate the
|
|
13569
|
+
reason: `The ${MERGE_FREEZE_CHECK_NAME} writer is unconfigured or its settle window cannot be confirmed for base ${input.headSha}: ${detail} Generate the merge-target push Verify workflow, configure FACTORY_GITHUB_APP_PRIVATE_KEY, and rerun pr:ready after the first App-owned freeze generation appears on this base branch.`
|
|
12736
13570
|
};
|
|
12737
13571
|
}
|
|
12738
13572
|
function selectMissingMergeFreezeGenerationUnchecked(api, input) {
|
|
@@ -12839,8 +13673,38 @@ function createGitHubCheckRunMergeFreezeApi(dependencies = {}) {
|
|
|
12839
13673
|
};
|
|
12840
13674
|
}
|
|
12841
13675
|
const githubMergeFreezeStore = createGitHubCheckRunMergeFreezeStore(createGitHubCheckRunMergeFreezeApi());
|
|
12842
|
-
/**
|
|
12843
|
-
|
|
13676
|
+
/**
|
|
13677
|
+
* The one refusal sentence, naming both exits, wherever the freeze refuses.
|
|
13678
|
+
*
|
|
13679
|
+
* Deliberately base-tip-scoped rather than main-scoped (#429). Since the writer
|
|
13680
|
+
* completes a generation on every factory merge target, the freeze this reader
|
|
13681
|
+
* found lives on the tip it was asked about — `main` for a main-targeting
|
|
13682
|
+
* candidate, the epic branch for an epic-targeting one — and only a later green
|
|
13683
|
+
* Verify push to THAT branch writes a newer inactive generation. The writer's
|
|
13684
|
+
* own recovery text, quoted verbatim in `reason`, names the branch; this
|
|
13685
|
+
* wrapper must not contradict it by naming a different one.
|
|
13686
|
+
*/
|
|
13687
|
+
const activeMergeFreezeReason = (reason) => `Merge freeze is active (${reason}). The generation is the one on this candidate's own base tip, so it clears automatically when a later Verify push to that base branch completes green after a fix-forward or revert; an operator may waive it for one candidate with patronage-factory demand:waive --pr <pr> --demand merge-freeze --rationale <why>.`;
|
|
13688
|
+
/**
|
|
13689
|
+
* The refusal for unreadable authority, which is NOT the active-freeze
|
|
13690
|
+
* sentence (#429 review cycles 2 and 3).
|
|
13691
|
+
*
|
|
13692
|
+
* Both refusals are equally fail-closed, but their recovery is not the same
|
|
13693
|
+
* act, and — the cycle-3 correction — "unreadable" is not one situation either.
|
|
13694
|
+
* It covers a repository with no writer at all, an unreachable Checks API, and
|
|
13695
|
+
* a defective *selected* generation: ambiguous newest runs, a malformed
|
|
13696
|
+
* payload, state inconsistent with its own conclusion. The first two are exited
|
|
13697
|
+
* by making the authority readable; the last is exited by a newer valid
|
|
13698
|
+
* App-owned generation winning the `started_at` selection, which a later Verify
|
|
13699
|
+
* push to the base branch normally produces.
|
|
13700
|
+
*
|
|
13701
|
+
* So this message states both paths without asserting which one applies. The
|
|
13702
|
+
* detail string it quotes is what distinguishes them, and the operator can read
|
|
13703
|
+
* it. Two categorical claims have already been wrong here — "clears when the
|
|
13704
|
+
* main-push verify completes green" (cycle 1) and "no Verify push clears this"
|
|
13705
|
+
* (cycle 2) — both from a refusal that spoke for states it could not see.
|
|
13706
|
+
*/
|
|
13707
|
+
const unreadableMergeFreezeReason = (headSha, detail) => `Merge freeze refuses this candidate: the authoritative ${MERGE_FREEZE_CHECK_NAME} state for its base tip ${headSha} could not be read (${detail}). Which recovery applies is what that detail says: restore readability where the authority is missing or unreachable — the merge-target push Verify workflow generated, its writer App credentials configured, GitHub's Checks API reachable with read access — and/or let a fresh valid App-owned generation supersede a defective one, which a later Verify push to this base branch normally produces. Then rerun pr:ready. An operator may waive it for one candidate with patronage-factory demand:waive --pr <pr> --demand merge-freeze --rationale <why>.`;
|
|
12844
13708
|
/**
|
|
12845
13709
|
* The freeze read during readiness, before any authorized arming (#477,
|
|
12846
13710
|
* ADR 0016 as amended 2026-07-31/2026-08-01).
|
|
@@ -12852,17 +13716,17 @@ const activeMergeFreezeReason = (reason) => `Merge freeze is active (${reason}).
|
|
|
12852
13716
|
* notice only when the writer is observable: either its generation is running,
|
|
12853
13717
|
* or the prior tip has a settled generation while this tip's source-pinned
|
|
12854
13718
|
* hosted verify is running (#521). Epic #473 decision 5 deliberately trades
|
|
12855
|
-
* the settle-window wait against the measured rarity of a red
|
|
13719
|
+
* the settle-window wait against the measured rarity of a red merge target. A
|
|
12856
13720
|
* writerless or unprovable repository fails closed.
|
|
12857
13721
|
*/
|
|
12858
13722
|
function armingMergeFreezeOutcome(input, authority) {
|
|
12859
13723
|
const generation = authority.readGeneration(input);
|
|
12860
13724
|
if (generation.kind === "settling") return {
|
|
12861
13725
|
blockingReasons: [],
|
|
12862
|
-
notices: [`Merge freeze generation for base ${input.headSha} is still settling (${generation.reason}); readiness proceeds (ADR 0016, 2026-07-31/2026-08-01 amendments). If this wave authorizes machine merge, arming follows the admission decision. The
|
|
13726
|
+
notices: [`Merge freeze generation for base ${input.headSha} is still settling (${generation.reason}); readiness proceeds (ADR 0016, 2026-07-31/2026-08-01 amendments). If this wave authorizes machine merge, arming follows the admission decision. The Verify run for that tip still refuses every later readiness decision against this base branch if it goes red.`]
|
|
12863
13727
|
};
|
|
12864
13728
|
if (generation.kind === "unreadable") return {
|
|
12865
|
-
blockingReasons: [
|
|
13729
|
+
blockingReasons: [unreadableMergeFreezeReason(input.headSha, `Authoritative GitHub merge freeze check-run state is unavailable or invalid: ${generation.reason}`)],
|
|
12866
13730
|
notices: []
|
|
12867
13731
|
};
|
|
12868
13732
|
return {
|
|
@@ -13354,12 +14218,23 @@ const requiredChecksEvaluation = (input) => {
|
|
|
13354
14218
|
mergeBaseSha: input.mergeBaseSha ?? input.baseSha,
|
|
13355
14219
|
patchId: input.patchId
|
|
13356
14220
|
};
|
|
14221
|
+
const { verificationProof } = input;
|
|
14222
|
+
const impactStamp = trustedImpactStamp({
|
|
14223
|
+
candidate: {
|
|
14224
|
+
headSha: input.headSha,
|
|
14225
|
+
patchId: input.patchId
|
|
14226
|
+
},
|
|
14227
|
+
proof: verificationProof && "proof" in verificationProof ? verificationProof.proof : void 0
|
|
14228
|
+
});
|
|
13357
14229
|
const { blockingReasons, outcomes } = evaluateRequiredChecks({
|
|
13358
14230
|
authoringSessionIds: input.authoringSessionIds ?? [],
|
|
13359
14231
|
candidate,
|
|
13360
14232
|
envelopes: input.evidenceEnvelopes ?? [],
|
|
13361
14233
|
requiredChecks,
|
|
13362
|
-
scopeContext: {
|
|
14234
|
+
scopeContext: {
|
|
14235
|
+
classification: input.classification,
|
|
14236
|
+
...impactStamp ? { impactStamp } : {}
|
|
14237
|
+
}
|
|
13363
14238
|
});
|
|
13364
14239
|
const blockers = outcomes.flatMap((outcome) => outcome.reason ? [{
|
|
13365
14240
|
demand: requiredCheckDemand(outcome.name),
|
|
@@ -15490,6 +16365,60 @@ const assembleReviewPrompt = ({ base, changedFiles, headSha, issueFocus, kind, p
|
|
|
15490
16365
|
};
|
|
15491
16366
|
};
|
|
15492
16367
|
//#endregion
|
|
16368
|
+
//#region src/preview-lifecycle.ts
|
|
16369
|
+
/**
|
|
16370
|
+
* The declared preview stacks: impact targets that are ALSO declared required
|
|
16371
|
+
* checks — i.e. targets whose proof is produced externally, which is exactly
|
|
16372
|
+
* what a preview producer is.
|
|
16373
|
+
*
|
|
16374
|
+
* Deriving the list from the two existing declarations rather than adding a
|
|
16375
|
+
* third is what makes lifecycle and demand unable to disagree: the stacks this
|
|
16376
|
+
* plan can withhold are precisely the demands wave 2 can release, so a stack
|
|
16377
|
+
* can never be skipped while its proof stays demanded.
|
|
16378
|
+
*/
|
|
16379
|
+
const previewLifecycleTargets = (profile) => {
|
|
16380
|
+
const demanded = new Set((profile.requiredChecks ?? []).map(({ name }) => name));
|
|
16381
|
+
return (profile.impact?.targets ?? []).map(({ name }) => name).filter((name) => demanded.has(name));
|
|
16382
|
+
};
|
|
16383
|
+
/**
|
|
16384
|
+
* Plan the preview lifecycle for one candidate.
|
|
16385
|
+
*
|
|
16386
|
+
* Trust is resolved here through the SAME identity gate demand release uses
|
|
16387
|
+
* (`trustedImpactStamp`): the proof must have passed and must bind both the
|
|
16388
|
+
* candidate's headSha and its patchId. A stale, aborted, absent, or
|
|
16389
|
+
* differently-bound proof yields no stamp, and every declared stack is planned
|
|
16390
|
+
* for deploy. Total over its INPUTS — no proof, stamp, or profile can make it
|
|
16391
|
+
* throw. A malformed CALL is the deliberate exception: omitting
|
|
16392
|
+
* `vetoedTargets` is API misuse, not data doubt, and throws.
|
|
16393
|
+
*/
|
|
16394
|
+
const planPreviewLifecycle = ({ candidate, profile, proof, vetoedTargets }) => {
|
|
16395
|
+
assertVetoedTargets(vetoedTargets, "planPreviewLifecycle");
|
|
16396
|
+
const stamp = trustedImpactStamp({
|
|
16397
|
+
candidate,
|
|
16398
|
+
proof
|
|
16399
|
+
});
|
|
16400
|
+
const vetoed = new Set(vetoedTargets);
|
|
16401
|
+
const targets = previewLifecycleTargets(profile).map((name) => {
|
|
16402
|
+
const decision = impactStampScopeDecision({
|
|
16403
|
+
stamp,
|
|
16404
|
+
surface: "preview-lifecycle",
|
|
16405
|
+
targetName: name
|
|
16406
|
+
});
|
|
16407
|
+
const vetoedHere = decision.scoped && vetoed.has(name);
|
|
16408
|
+
return {
|
|
16409
|
+
basis: vetoedHere ? `veto: a current envelope bound to this candidate records stack "${name}" as FAILING, so the stamp's release is withheld (${decision.reason})` : decision.reason,
|
|
16410
|
+
disposition: decision.scoped && !vetoedHere ? "not-required" : "deploy",
|
|
16411
|
+
name
|
|
16412
|
+
};
|
|
16413
|
+
});
|
|
16414
|
+
return {
|
|
16415
|
+
deploy: targets.filter((target) => target.disposition === "deploy").map((target) => target.name),
|
|
16416
|
+
notRequired: targets.filter((target) => target.disposition === "not-required"),
|
|
16417
|
+
stampTrusted: stamp !== void 0,
|
|
16418
|
+
targets
|
|
16419
|
+
};
|
|
16420
|
+
};
|
|
16421
|
+
//#endregion
|
|
15493
16422
|
//#region src/index.ts
|
|
15494
16423
|
function createProgram(options = {}) {
|
|
15495
16424
|
const factoryCliInvocation = options.factoryCliInvocation ? FactoryCliInvocationSchema.parse(options.factoryCliInvocation) : defaultFactoryCliInvocation(import.meta.filename);
|
|
@@ -15537,4 +16466,4 @@ if (isDirectCliExecution()) try {
|
|
|
15537
16466
|
process.exit(1);
|
|
15538
16467
|
}
|
|
15539
16468
|
//#endregion
|
|
15540
|
-
export { DEFAULT_DEMAND_WAIVER_PATH, DEFAULT_FACTORY_REPOSITORY, DemandWaiveRefusalError, EPIC_STRUCTURE_NODE_STATUSES, EPIC_STRUCTURE_SCHEMA_VERSION, EpicStructureValidationError, FactoryCliInvocationSchema, FollowUpActionSchema, MERGE_FREEZE_APP_SLUG, MERGE_FREEZE_CHECK_NAME, PrPublishFollowUpError, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, REVIEW_FOCUS_SECTION, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, WorkerCheckoutGuardError, appendReviewLadderStageTraceEvent, appendWithTraceSinks, applyDemandWaiver, assembleReviewPrompt, assertWorkerCheckoutAllowed, authorizeDemandWaiver, blockingLadderFindings, boundary_manifest_exports as boundaryManifest, boundary_review_proof_exports as boundaryReviewProof, buildEpicStructureEvent, buildEpicStructurePayload, buildEvidenceEnvelope, buildRetroEnvelope, buildReviewGateNotRequiredTraceEvent, buildReviewLadderStageTraceEvent, comment_provenance_exports as commentProvenance, createLocalJsonlTraceSink, createProgram, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, inferFixedInThreadDispositions, isProductionHqUrl, isRetroEnvelopeWireComplete, loadProjectProfile, normalizeIssueComments, openLadderFindings, parseRetroEnvelope, planPublishFollowUp, readiness_evaluation_exports as prReadinessEvaluation, external_evidence_exports as prReadinessExternalEvidence, pr_body_renderer_exports as prReadinessPrBodyRenderer, proof_identity_exports as prReadinessProofIdentity, review_proof_exports as prReadinessReviewProof, status_check_rollup_exports as prReadinessStatusChecks, verification_proof_exports as prReadinessVerificationProof, publishEpicStructure, readDemandWaivers, readPrReadyProof, readPrReviewProof, resolveFactoryRepository, resolveFindingBlocking, resolveReviewFindingCategory, resolveReviewFindingSeverity, resolveReviewLadderPolicy, resolveTraceWriteSinks, retroEnvelopeSchemaVersionOf, retroEnvelopeV1Schema, retroEpicReference, reviewCycleStateFor, reviewFocusFromIssueBody, reviewPromptSectionSchema, reviewPromptSectionsSchema, run, runBoundaryCheck, runDemandWaive, runEvidenceEmit, runPrPublish, runPrReady, runPrReview, runPrVerify, scanFactoryTraceDiagnostics, scanFactoryTraceEvents, selectWaiversForCandidate, staleRepeatLadderFindings, toFactoryTraceEnvelope, tryAppendReviewGateNotRequiredTraceEvent, tryAppendReviewLadderStageTraceEvent, validateBoundaryCheckProof, validateDagDocument, validateDemandWaiverStore, validateFactoryTraceEvent, validateMergeFreezeState, validatePrReadyProof, validatePrReviewProof, validatePrVerifyProof, waivedDemandNotice, waivedDemandSchema, worktree_scratch_files_exports as worktreeScratchFiles };
|
|
16469
|
+
export { DEFAULT_DEMAND_WAIVER_PATH, DEFAULT_FACTORY_REPOSITORY, DemandWaiveRefusalError, EPIC_STRUCTURE_NODE_STATUSES, EPIC_STRUCTURE_SCHEMA_VERSION, EpicStructureValidationError, FactoryCliInvocationSchema, FollowUpActionSchema, MERGE_FREEZE_APP_SLUG, MERGE_FREEZE_CHECK_NAME, PrPublishFollowUpError, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, REVIEW_FOCUS_SECTION, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, WorkerCheckoutGuardError, appendReviewLadderStageTraceEvent, appendWithTraceSinks, applyDemandWaiver, assembleReviewPrompt, assertWorkerCheckoutAllowed, authorizeDemandWaiver, batteryScopeSummaryLines, blockingLadderFindings, boundary_manifest_exports as boundaryManifest, boundary_review_proof_exports as boundaryReviewProof, buildEpicStructureEvent, buildEpicStructurePayload, buildEvidenceEnvelope, buildRetroEnvelope, buildReviewGateNotRequiredTraceEvent, buildReviewLadderStageTraceEvent, comment_provenance_exports as commentProvenance, createLocalJsonlTraceSink, createProgram, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, impactStampScopeDecision, inferFixedInThreadDispositions, isProductionHqUrl, isRetroEnvelopeWireComplete, loadProjectProfile, normalizeIssueComments, openLadderFindings, parseRetroEnvelope, planPreviewLifecycle, planPublishFollowUp, planVerificationBattery, readiness_evaluation_exports as prReadinessEvaluation, external_evidence_exports as prReadinessExternalEvidence, pr_body_renderer_exports as prReadinessPrBodyRenderer, proof_identity_exports as prReadinessProofIdentity, review_proof_exports as prReadinessReviewProof, status_check_rollup_exports as prReadinessStatusChecks, verification_proof_exports as prReadinessVerificationProof, previewLifecycleTargets, publishEpicStructure, readDemandWaivers, readPrReadyProof, readPrReviewProof, resolveFactoryRepository, resolveFindingBlocking, resolveReviewFindingCategory, resolveReviewFindingSeverity, resolveReviewLadderPolicy, resolveTraceWriteSinks, retroEnvelopeSchemaVersionOf, retroEnvelopeV1Schema, retroEpicReference, reviewCycleStateFor, reviewFocusFromIssueBody, reviewPromptSectionSchema, reviewPromptSectionsSchema, run, runBoundaryCheck, runDemandWaive, runEvidenceEmit, runPrPublish, runPrReady, runPrReview, runPrVerify, scanFactoryTraceDiagnostics, scanFactoryTraceEvents, selectWaiversForCandidate, staleRepeatLadderFindings, toFactoryTraceEnvelope, tryAppendReviewGateNotRequiredTraceEvent, tryAppendReviewLadderStageTraceEvent, validateBoundaryCheckProof, validateDagDocument, validateDemandWaiverStore, validateFactoryTraceEvent, validateMergeFreezeState, validatePrReadyProof, validatePrReviewProof, validatePrVerifyProof, waivedDemandNotice, waivedDemandSchema, worktree_scratch_files_exports as worktreeScratchFiles };
|