@patronage/software-factory 1.0.0-alpha.0 → 1.0.0-alpha.2
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 +10 -4
- package/dist/index.d.ts +22 -21
- package/dist/index.js +97 -29
- package/dist/schemas.d.ts +4 -4
- package/dist/schemas.js +8 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
The factory admits a pull request from evidence bound to its candidate. It validates policy and proof; it does not schedule workers, choose models, or run reviews. Those are jobs for the harness or operator.
|
|
6
6
|
|
|
7
|
-
Admission is fail closed. A clean-session review and required verification produce typed proof, then readiness decides whether the candidate may proceed. Evidence stays current when the candidate patch is unchanged; the push-triggered Verify workflow covers integration risk on every factory merge target and
|
|
7
|
+
Admission is fail closed. A clean-session review and required verification produce typed proof, then readiness decides whether the candidate may proceed. Evidence stays current when the candidate patch is unchanged; the push-triggered Verify workflow covers integration risk on every factory merge target — `main` and the `epic/**` integration branches. When a target goes red, the factory refuses new readiness decisions and authorized arming against that target; candidates armed before the target went red can still land. Epic policy, including review minimums and auto-merge authority, lives in the boundary manifest. HQ observes this work; it never blocks a gate.
|
|
8
8
|
|
|
9
9
|
## Develop the package
|
|
10
10
|
|
|
@@ -30,18 +30,24 @@ This is the release checklist. It lives here and nowhere else; the repository ru
|
|
|
30
30
|
|
|
31
31
|
Releases are attended and hand-cut. Nothing in the factory publishes, tags, or releases on its own, and no step below may be automated as a drive-by.
|
|
32
32
|
|
|
33
|
+
The factory family has one version posture (ADR 0026, `docs/adr/0026-factory-family-version-lines-move-in-lockstep.md` in the source repository): `@patronage/factory-ci` moves in lockstep majors with this CLI. From factory-ci's first release in the CLI's current series onward, majors and prerelease series move together while patch and prerelease increments stay independent per package. A consumer pins a factory-ci from the same major/prerelease series as its CLI; there is no compatibility matrix. Until that first lockstep release, factory-ci's published 0.x line is the named pre-adoption state and pairs with any CLI version. `@patronage/alchemy-d1-state` keeps independent SemVer. Lockstep is a rule the maintainer follows when cutting releases, never automation.
|
|
34
|
+
|
|
35
|
+
A stacked release train has one terminal release-owning slice. Interior slices do not bump `package.json` or cut a checkpoint unless the boundary explicitly intends a separately published release at that slice: a lower-slice bump consumes the release window before the terminal slice lands, and the train must then advance the version and re-earn its terminal evidence.
|
|
36
|
+
|
|
37
|
+
A stable cut has two phases. Phase 1 is the verified package candidate: steps 1 through 4 below, ending in the attended npm publish, tag, and GitHub release. Phase 2 is post-publication stable-doc convergence: steps 5 and 6, a normal admitted docs PR followed by the read-only convergence assertion from a clean `main`. Stable docs must never claim an unpublished version, so they cannot land in the phase-1 change. Prerelease behavior is unchanged: prerelease notes may land before publish while stable docs keep naming the prior stable version.
|
|
38
|
+
|
|
33
39
|
1. Bump `package.json` to valid SemVer. The assertion derives its expected stable or prerelease channel from the version; it accepts no channel flag.
|
|
34
40
|
2. Publish to npm per the [repository operations runbook](../docs/repository-operations.md#publishing) (attended, hand-cut, `pnpm publish` only). A stable version uses `pnpm --filter @patronage/software-factory publish`, which assigns npm `latest`. A prerelease must use `pnpm --filter @patronage/software-factory publish --tag next`; bare `pnpm publish` defaults to `latest` and would move it before the post-hoc assertion can reject the release.
|
|
35
41
|
3. Cut the `software-factory-vX.Y.Z` tag and push it.
|
|
36
42
|
4. Cut the GitHub release for that tag, with notes covering features, deletions, breaking changes, and closed issues. Mark it as a prerelease exactly when its SemVer version has a prerelease component. When the release changes the profile `schemaVersion`, state it in the notes — the profile schema version is a separate axis from the package version.
|
|
37
|
-
5. Update affected `software-factory-docs/` pages. Stable docs must state the current stable version. Prerelease docs may record the candidate and its migration notes, but must not represent it as the current stable release.
|
|
38
|
-
6.
|
|
43
|
+
5. Converge the stable docs after publication. Update affected `software-factory-docs/` pages through a normal admitted docs PR. Stable docs must state the current stable version. Prerelease docs may record the candidate and its migration notes, but must not represent it as the current stable release.
|
|
44
|
+
6. After the docs PR merges, confirm the six facts converge from a clean checkout of `main`:
|
|
39
45
|
|
|
40
46
|
```bash
|
|
41
47
|
pnpm --filter @patronage/software-factory exec tsx scripts/assert-publish-manifest.ts --release .
|
|
42
48
|
```
|
|
43
49
|
|
|
44
|
-
It reads what you just did and derives the channel from the manifest version. A stable release must converge exactly among manifest, npm `latest`, non-prerelease GitHub release, tag, and stable docs. A prerelease must exist on npm, own npm `next`, leave npm `latest` at the stable docs version, and have its prerelease GitHub release and tag. It publishes nothing. Step 6 is the only step a machine performs, and it performs it after you.
|
|
50
|
+
It reads what you just did and derives the channel from the manifest version. A stable release must converge exactly among manifest, npm `latest`, non-prerelease GitHub release, tag, and stable docs. A prerelease must exist on npm, own npm `next`, leave npm `latest` at the stable docs version, and have its prerelease GitHub release and tag. The sixth fact is the factory-ci pairing above: the assertion passes while every observed published `@patronage/factory-ci` version is pre-adoption 0.x and names that state, fails once a lockstep factory-ci release exists whose newest published version is outside the CLI's major/prerelease series, and fails when the registry read yields no readable factory-ci versions or contains an entry that does not parse — missing or unreadable data is an unsatisfied fact, not pre-adoption. It publishes nothing. Step 6 is the only step a machine performs, and it performs it after you.
|
|
45
51
|
|
|
46
52
|
## Use it in a project
|
|
47
53
|
|
package/dist/index.d.ts
CHANGED
|
@@ -537,6 +537,7 @@ declare const factoryProjectProfileSchema: z.ZodObject<{
|
|
|
537
537
|
targets: z.ZodArray<z.ZodObject<{
|
|
538
538
|
importers: z.ZodArray<z.ZodString>;
|
|
539
539
|
name: z.ZodString;
|
|
540
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
540
541
|
}, z.core.$strict>>;
|
|
541
542
|
}, z.core.$strict>>;
|
|
542
543
|
proof: z.ZodObject<{
|
|
@@ -752,8 +753,8 @@ declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
752
753
|
generationId: z.ZodNumber;
|
|
753
754
|
headSha: z.ZodString;
|
|
754
755
|
outcome: z.ZodEnum<{
|
|
755
|
-
active: "active";
|
|
756
756
|
stale: "stale";
|
|
757
|
+
active: "active";
|
|
757
758
|
}>;
|
|
758
759
|
reason: z.ZodString;
|
|
759
760
|
recordedAt: z.ZodISODateTime;
|
|
@@ -1233,8 +1234,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1233
1234
|
"run-interior-cycle": "run-interior-cycle";
|
|
1234
1235
|
}>;
|
|
1235
1236
|
stage: z.ZodEnum<{
|
|
1236
|
-
gate: "gate";
|
|
1237
1237
|
interior: "interior";
|
|
1238
|
+
gate: "gate";
|
|
1238
1239
|
"interior-complete": "interior-complete";
|
|
1239
1240
|
}>;
|
|
1240
1241
|
}, z.core.$strip>>;
|
|
@@ -1250,10 +1251,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1250
1251
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1251
1252
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
1252
1253
|
status: z.ZodEnum<{
|
|
1253
|
-
blocked: "blocked";
|
|
1254
1254
|
"not-required": "not-required";
|
|
1255
|
-
|
|
1255
|
+
blocked: "blocked";
|
|
1256
1256
|
current: "current";
|
|
1257
|
+
stale: "stale";
|
|
1257
1258
|
missing: "missing";
|
|
1258
1259
|
}>;
|
|
1259
1260
|
}, z.core.$strip>;
|
|
@@ -1262,10 +1263,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1262
1263
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1263
1264
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
1264
1265
|
status: z.ZodEnum<{
|
|
1265
|
-
blocked: "blocked";
|
|
1266
1266
|
"not-required": "not-required";
|
|
1267
|
-
|
|
1267
|
+
blocked: "blocked";
|
|
1268
1268
|
current: "current";
|
|
1269
|
+
stale: "stale";
|
|
1269
1270
|
missing: "missing";
|
|
1270
1271
|
}>;
|
|
1271
1272
|
}, z.core.$strip>>;
|
|
@@ -1276,8 +1277,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1276
1277
|
docsOnlyDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
|
|
1277
1278
|
docsOnlyVerifiedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1278
1279
|
prVerify: z.ZodEnum<{
|
|
1279
|
-
stale: "stale";
|
|
1280
1280
|
passed: "passed";
|
|
1281
|
+
stale: "stale";
|
|
1281
1282
|
missing: "missing";
|
|
1282
1283
|
}>;
|
|
1283
1284
|
trivialDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -1522,11 +1523,11 @@ declare function runPrReady(args: PrReadyArgs, dependencies?: PrReadyDependencie
|
|
|
1522
1523
|
*/
|
|
1523
1524
|
declare const impactStampSchema: z.ZodObject<{
|
|
1524
1525
|
basis: z.ZodEnum<{
|
|
1525
|
-
"
|
|
1526
|
+
"target-scoped": "target-scoped";
|
|
1526
1527
|
conservative: "conservative";
|
|
1527
1528
|
}>;
|
|
1528
1529
|
reasons: z.ZodArray<z.ZodString>;
|
|
1529
|
-
stampVersion: z.ZodLiteral<
|
|
1530
|
+
stampVersion: z.ZodLiteral<2>;
|
|
1530
1531
|
targets: z.ZodArray<z.ZodObject<{
|
|
1531
1532
|
basis: z.ZodString;
|
|
1532
1533
|
impact: z.ZodEnum<{
|
|
@@ -1560,7 +1561,7 @@ interface ImpactScopeDecision {
|
|
|
1560
1561
|
* external proof demand (#542 wave 2), a verification-battery command, or a
|
|
1561
1562
|
* preview (Alchemy) lifecycle stack (#430 wave 3).
|
|
1562
1563
|
*
|
|
1563
|
-
* The only release path is a `
|
|
1564
|
+
* The only release path is a `target-scoped` stamp of exactly this build's
|
|
1564
1565
|
* stamp version whose target entry for exactly this name is provably
|
|
1565
1566
|
* `not-affected`. Every other input — no trusted stamp, an unknown stamp
|
|
1566
1567
|
* version, a conservative stamp, a name the stamp does not classify, or an
|
|
@@ -1777,10 +1778,10 @@ declare const loadEvidenceEnvelopes: (cwd: string) => LoadedEvidenceEnvelope[];
|
|
|
1777
1778
|
//#region src/review-proof-applicability.d.ts
|
|
1778
1779
|
declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale", "missing", "blocked"];
|
|
1779
1780
|
declare const reviewStatusSchema: z.ZodEnum<{
|
|
1780
|
-
blocked: "blocked";
|
|
1781
1781
|
"not-required": "not-required";
|
|
1782
|
-
|
|
1782
|
+
blocked: "blocked";
|
|
1783
1783
|
current: "current";
|
|
1784
|
+
stale: "stale";
|
|
1784
1785
|
missing: "missing";
|
|
1785
1786
|
}>;
|
|
1786
1787
|
type ReviewStatus = z.infer<typeof reviewStatusSchema>;
|
|
@@ -2186,9 +2187,9 @@ declare const retroEnvelopeV1Schema: z.ZodObject<{
|
|
|
2186
2187
|
kind: z.ZodLiteral<"retro-envelope">;
|
|
2187
2188
|
outcome: z.ZodOptional<z.ZodObject<{
|
|
2188
2189
|
status: z.ZodEnum<{
|
|
2189
|
-
blocked: "blocked";
|
|
2190
2190
|
success: "success";
|
|
2191
2191
|
fail: "fail";
|
|
2192
|
+
blocked: "blocked";
|
|
2192
2193
|
"ship-with-followups": "ship-with-followups";
|
|
2193
2194
|
}>;
|
|
2194
2195
|
verdict: z.ZodOptional<z.ZodString>;
|
|
@@ -2948,8 +2949,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
2948
2949
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
2949
2950
|
}, z.core.$strict>>;
|
|
2950
2951
|
stage: z.ZodEnum<{
|
|
2951
|
-
gate: "gate";
|
|
2952
2952
|
interior: "interior";
|
|
2953
|
+
gate: "gate";
|
|
2953
2954
|
}>;
|
|
2954
2955
|
usage: z.ZodOptional<z.ZodObject<{
|
|
2955
2956
|
estimatedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3001,8 +3002,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3001
3002
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
3002
3003
|
}, z.core.$strict>>;
|
|
3003
3004
|
stage: z.ZodEnum<{
|
|
3004
|
-
gate: "gate";
|
|
3005
3005
|
interior: "interior";
|
|
3006
|
+
gate: "gate";
|
|
3006
3007
|
}>;
|
|
3007
3008
|
usage: z.ZodOptional<z.ZodObject<{
|
|
3008
3009
|
estimatedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3045,8 +3046,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3045
3046
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
3046
3047
|
}, z.core.$strict>>;
|
|
3047
3048
|
stage: z.ZodEnum<{
|
|
3048
|
-
gate: "gate";
|
|
3049
3049
|
interior: "interior";
|
|
3050
|
+
gate: "gate";
|
|
3050
3051
|
}>;
|
|
3051
3052
|
usage: z.ZodOptional<z.ZodObject<{
|
|
3052
3053
|
estimatedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3076,7 +3077,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3076
3077
|
staleRepeats: number;
|
|
3077
3078
|
};
|
|
3078
3079
|
observedAt: string;
|
|
3079
|
-
stage: "
|
|
3080
|
+
stage: "interior" | "gate";
|
|
3080
3081
|
issue?: number | undefined;
|
|
3081
3082
|
pr?: number | undefined;
|
|
3082
3083
|
threadId?: string | undefined;
|
|
@@ -4075,13 +4076,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4075
4076
|
reviews: {
|
|
4076
4077
|
correctness: {
|
|
4077
4078
|
required: boolean;
|
|
4078
|
-
status: "
|
|
4079
|
+
status: "not-required" | "blocked" | "current" | "stale" | "missing";
|
|
4079
4080
|
reviewedHeadSha?: string | undefined;
|
|
4080
4081
|
reviewedPatchId?: string | undefined;
|
|
4081
4082
|
};
|
|
4082
4083
|
security?: {
|
|
4083
4084
|
required: boolean;
|
|
4084
|
-
status: "
|
|
4085
|
+
status: "not-required" | "blocked" | "current" | "stale" | "missing";
|
|
4085
4086
|
reviewedHeadSha?: string | undefined;
|
|
4086
4087
|
reviewedPatchId?: string | undefined;
|
|
4087
4088
|
} | undefined;
|
|
@@ -4089,7 +4090,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4089
4090
|
schemaVersion: 1;
|
|
4090
4091
|
verification: {
|
|
4091
4092
|
command: "patronage-factory pr:verify";
|
|
4092
|
-
prVerify: "
|
|
4093
|
+
prVerify: "passed" | "stale" | "missing";
|
|
4093
4094
|
docsOnlyDeltaAccepted?: boolean | undefined;
|
|
4094
4095
|
docsOnlyVerifiedHeadSha?: string | undefined;
|
|
4095
4096
|
trivialDeltaAccepted?: boolean | undefined;
|
|
@@ -4132,7 +4133,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4132
4133
|
interior: number;
|
|
4133
4134
|
};
|
|
4134
4135
|
nextAction: "run-gate-cycle" | "accept-nonblocking-findings" | "escalate-to-triage" | "ready-for-human" | "advance-to-gate" | "run-interior-cycle";
|
|
4135
|
-
stage: "
|
|
4136
|
+
stage: "interior" | "gate" | "interior-complete";
|
|
4136
4137
|
forcedTransition?: "gate-cap-exhausted" | "interior-cap-reached" | undefined;
|
|
4137
4138
|
} | undefined;
|
|
4138
4139
|
reviewRuns?: PrReviewResult[] | undefined;
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import picomatch from "picomatch";
|
|
|
17
17
|
import { parse } from "yaml";
|
|
18
18
|
import { promisify } from "node:util";
|
|
19
19
|
//#region package.json
|
|
20
|
-
var version = "1.0.0-alpha.
|
|
20
|
+
var version = "1.0.0-alpha.2";
|
|
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,8 +946,9 @@ 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),
|
|
@@ -972,7 +980,8 @@ const requiredCheckSchema = z.object({
|
|
|
972
980
|
}).strict();
|
|
973
981
|
const impactTargetSchema = z.object({
|
|
974
982
|
importers: z.array(z.string().min(1)).min(1),
|
|
975
|
-
name: z.string().min(1)
|
|
983
|
+
name: z.string().min(1),
|
|
984
|
+
paths: z.array(repoRelativeGlobSchema).min(1).optional()
|
|
976
985
|
}).strict();
|
|
977
986
|
const impactConfigSchema = z.object({ targets: z.array(impactTargetSchema).min(1).superRefine((targets, context) => {
|
|
978
987
|
const names = targets.map((target) => target.name);
|
|
@@ -3845,9 +3854,9 @@ const impactStampTargetSchema = z.object({
|
|
|
3845
3854
|
* input widened every target to full impact.
|
|
3846
3855
|
*/
|
|
3847
3856
|
const impactStampSchema = z.object({
|
|
3848
|
-
basis: z.enum(["
|
|
3857
|
+
basis: z.enum(["target-scoped", "conservative"]),
|
|
3849
3858
|
reasons: z.array(z.string()),
|
|
3850
|
-
stampVersion: z.literal(
|
|
3859
|
+
stampVersion: z.literal(2),
|
|
3851
3860
|
targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
|
|
3852
3861
|
const names = targets.map((target) => target.name);
|
|
3853
3862
|
if (new Set(names).size !== names.length) context.addIssue({
|
|
@@ -3886,7 +3895,7 @@ const IMPACT_SCOPE_SURFACES = {
|
|
|
3886
3895
|
* external proof demand (#542 wave 2), a verification-battery command, or a
|
|
3887
3896
|
* preview (Alchemy) lifecycle stack (#430 wave 3).
|
|
3888
3897
|
*
|
|
3889
|
-
* The only release path is a `
|
|
3898
|
+
* The only release path is a `target-scoped` stamp of exactly this build's
|
|
3890
3899
|
* stamp version whose target entry for exactly this name is provably
|
|
3891
3900
|
* `not-affected`. Every other input — no trusted stamp, an unknown stamp
|
|
3892
3901
|
* version, a conservative stamp, a name the stamp does not classify, or an
|
|
@@ -3905,11 +3914,11 @@ const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
|
|
|
3905
3914
|
reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
|
|
3906
3915
|
scoped: false
|
|
3907
3916
|
};
|
|
3908
|
-
if (stamp.stampVersion !==
|
|
3909
|
-
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version
|
|
3917
|
+
if (stamp.stampVersion !== 2) return {
|
|
3918
|
+
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 2; ${floor}`,
|
|
3910
3919
|
scoped: false
|
|
3911
3920
|
};
|
|
3912
|
-
if (stamp.basis !== "
|
|
3921
|
+
if (stamp.basis !== "target-scoped") return {
|
|
3913
3922
|
reason: `impact stamp is conservative (full impact assumed); ${floor}`,
|
|
3914
3923
|
scoped: false
|
|
3915
3924
|
};
|
|
@@ -3951,7 +3960,7 @@ const impactStampDemandRelease = ({ checkName, stamp }) => {
|
|
|
3951
3960
|
const conservativeImpactStamp = (targets, reasons) => ({
|
|
3952
3961
|
basis: "conservative",
|
|
3953
3962
|
reasons,
|
|
3954
|
-
stampVersion:
|
|
3963
|
+
stampVersion: 2,
|
|
3955
3964
|
targets: targets.map((target) => ({
|
|
3956
3965
|
basis: CONSERVATIVE_TARGET_BASIS,
|
|
3957
3966
|
impact: "affected",
|
|
@@ -4087,11 +4096,10 @@ const targetImpactForLockfileDelta = (target, delta) => {
|
|
|
4087
4096
|
/**
|
|
4088
4097
|
* Compute the impact stamp for one candidate: every declared target recorded
|
|
4089
4098
|
* as affected/not-affected with its exact basis. `not-affected` is only ever
|
|
4090
|
-
* returned
|
|
4091
|
-
*
|
|
4092
|
-
*
|
|
4093
|
-
*
|
|
4094
|
-
* full impact.
|
|
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.
|
|
4095
4103
|
*
|
|
4096
4104
|
* Total by contract: this function never throws. Any unexpected failure in
|
|
4097
4105
|
* reading, parsing, or traversal is itself a doubt path and returns the
|
|
@@ -4154,22 +4162,73 @@ const readLockfileSides = (readLockfile) => {
|
|
|
4154
4162
|
const assertDeclaredImporterRoots = (targets, sides) => {
|
|
4155
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`);
|
|
4156
4164
|
};
|
|
4157
|
-
const
|
|
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) => {
|
|
4158
4209
|
if (targets.length === 0) return {
|
|
4159
4210
|
basis: "conservative",
|
|
4160
4211
|
reasons: ["no impact targets declared in the profile; every surface keeps full demand"],
|
|
4161
|
-
stampVersion:
|
|
4212
|
+
stampVersion: 2,
|
|
4162
4213
|
targets: []
|
|
4163
4214
|
};
|
|
4215
|
+
assertDeclaredPathGlobs(targets);
|
|
4164
4216
|
const sides = readLockfileSides(readLockfile);
|
|
4165
4217
|
if (sides.kind === "ok") assertDeclaredImporterRoots(targets, sides.parsed);
|
|
4166
4218
|
if (changedFiles.length === 0) return conservativeStamp(targets, ["no changed files detected; full impact assumed"]);
|
|
4167
|
-
const
|
|
4168
|
-
|
|
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]);
|
|
4169
4222
|
if (sides.kind === "threw") throw sides.error;
|
|
4170
4223
|
if (sides.kind === "unreadable") return conservativeStamp(targets, [`pnpm-lock.yaml is unreadable at the ${sides.side} side; fail closed to full impact`]);
|
|
4171
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`]);
|
|
4172
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
|
+
};
|
|
4173
4232
|
const [baseLock, headLock] = sides.parsed;
|
|
4174
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"]);
|
|
4175
4234
|
const delta = {
|
|
@@ -4178,11 +4237,13 @@ const computeImpactStampOrThrow = ({ changedFiles, readLockfile }, targets) => {
|
|
|
4178
4237
|
changedSnapshots: changedSectionKeys(baseLock.snapshots, headLock.snapshots),
|
|
4179
4238
|
sides: [baseLock, headLock]
|
|
4180
4239
|
};
|
|
4240
|
+
const lockfileTargets = targets.map((target) => targetImpactForLockfileDelta(target, delta));
|
|
4241
|
+
const mixed = sourceFiles.length > 0;
|
|
4181
4242
|
return {
|
|
4182
|
-
basis: "
|
|
4183
|
-
reasons: ["pnpm-lock.yaml is the only changed file; impact scoped by importer/snapshot graph analysis"],
|
|
4184
|
-
stampVersion:
|
|
4185
|
-
targets: targets.map((
|
|
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
|
|
4186
4247
|
};
|
|
4187
4248
|
};
|
|
4188
4249
|
//#endregion
|
|
@@ -4361,7 +4422,7 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
4361
4422
|
* so authorization is bound to the same identities the proof binds. The
|
|
4362
4423
|
* predicate is the shared {@link impactStampScopeDecision}, not a second
|
|
4363
4424
|
* reading of the stamp: an entry is authorized exactly when the stamp would
|
|
4364
|
-
* have released that target's command in the first place (
|
|
4425
|
+
* have released that target's command in the first place (target-scoped
|
|
4365
4426
|
* basis, this build's stamp version, exactly one row for the target,
|
|
4366
4427
|
* `not-affected`). A conservative stamp, an unknown version, a
|
|
4367
4428
|
* self-contradictory stamp, an unclassified name, or an `affected` verdict all
|
|
@@ -8844,7 +8905,7 @@ const renderReport = (record) => {
|
|
|
8844
8905
|
return `${lines.join("\n")}\n`;
|
|
8845
8906
|
};
|
|
8846
8907
|
function createBoundaryCheckCommand(output, action) {
|
|
8847
|
-
return new Command("boundary:check").description("Boundary readiness gate (T8): require a membership-fresh boundary-review proof at the declared closeout rung. Blocks closeout/enablement, never merge.").requiredOption("--epic <number>", "epic issue number carrying the factory-boundary manifest", positiveInteger("--epic")).option("--repo <owner/name>", "GitHub repository (default: resolved from --cwd)").option("--cwd <path>", "repository working directory", ".").option("--output <path>", "write an additional proof copy to this path").option("--json", "print the full proof record as JSON").action(withGateTiming({
|
|
8908
|
+
return new Command("boundary:check").description("Boundary readiness gate (T8): require a membership-fresh boundary-review proof at the declared closeout rung. Blocks closeout/enablement, never merge.").requiredOption("--epic <number>", "epic issue number carrying the factory-boundary manifest", positiveInteger("--epic")).option("--repo <owner/name>", "GitHub repository (default: resolved from --cwd)").option("--cwd <path>", "repository working directory", ".").option("--output <path>", "write an additional proof copy to this path").option("--profile <path>", "path to the project profile JSON file").option("--json", "print the full proof record as JSON").action(withGateTiming({
|
|
8848
8909
|
gate: "boundary:check",
|
|
8849
8910
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
8850
8911
|
stderr: output.stderr
|
|
@@ -8860,7 +8921,10 @@ function createBoundaryCheckCommand(output, action) {
|
|
|
8860
8921
|
if (!action) {
|
|
8861
8922
|
const [owner, name] = record.repo.split("/");
|
|
8862
8923
|
if (owner && name) {
|
|
8863
|
-
const hqLaneBaseUrl = hqLaneRefBaseUrlFromProfile(tryLoadProjectProfile({
|
|
8924
|
+
const hqLaneBaseUrl = hqLaneRefBaseUrlFromProfile(tryLoadProjectProfile({
|
|
8925
|
+
cwd: args.cwd,
|
|
8926
|
+
profilePath: options.profile
|
|
8927
|
+
})?.profile);
|
|
8864
8928
|
try {
|
|
8865
8929
|
publishFactoryCheckSafely(createFactoryCheckPublisher(output), {
|
|
8866
8930
|
conclusion: record.status === "ready" ? "success" : "failure",
|
|
@@ -9547,6 +9611,8 @@ const buildRetroEnvelope = (input) => {
|
|
|
9547
9611
|
const RETRO_ENVELOPE_LEDGER_DIR = ".factory-memory/retro-envelope";
|
|
9548
9612
|
const safeEnvelopeName = (agentRunId) => agentRunId.replaceAll(/[^\w.-]/gu, "-").slice(0, 120);
|
|
9549
9613
|
const retroEnvelopePath = (root, agentRunId) => path.join(root, RETRO_ENVELOPE_LEDGER_DIR, `${safeEnvelopeName(agentRunId)}.json`);
|
|
9614
|
+
const CLAUDE_CODE_SESSION_ID_ENV = "CLAUDE_CODE_SESSION_ID";
|
|
9615
|
+
const resolveNativeSessionId = (env) => normalizeSessionId(env[CLAUDE_CODE_SESSION_ID_ENV]);
|
|
9550
9616
|
const splitRepo = (repo) => {
|
|
9551
9617
|
const [owner, name] = repo.split("/");
|
|
9552
9618
|
return {
|
|
@@ -9572,11 +9638,11 @@ const otherLedgersExist = (repoRoot) => {
|
|
|
9572
9638
|
const buildAndPersistRetroEnvelope = (input) => {
|
|
9573
9639
|
const env = input.env ?? process.env;
|
|
9574
9640
|
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
9575
|
-
const laneIdentity = resolveGateTimingAgentRunId(env);
|
|
9641
|
+
const laneIdentity = resolveGateTimingAgentRunId(env) ?? resolveNativeSessionId(env);
|
|
9576
9642
|
const agentRunId = laneIdentity ?? `closeout-${safeEnvelopeName(input.epic)}`;
|
|
9577
9643
|
const ledgerPath = gateTimingLedgerPath(input.repoRoot, agentRunId);
|
|
9578
9644
|
const ledger = laneIdentity === void 0 ? [] : readGateTimingLedger(ledgerPath);
|
|
9579
|
-
const claudeSessionId = discoverFactorySessionId(env);
|
|
9645
|
+
const claudeSessionId = discoverFactorySessionId(env) ?? resolveNativeSessionId(env);
|
|
9580
9646
|
const dataGaps = [];
|
|
9581
9647
|
if (laneIdentity === void 0) dataGaps.push("agentRunId is synthetic (no lane run identity exported): keyed to the epic, ledger not folded.");
|
|
9582
9648
|
else if (!existsSync(ledgerPath) && otherLedgersExist(input.repoRoot)) dataGaps.push(`gates[] empty: no gate-timing ledger for agentRunId "${agentRunId}" although other lane ledgers exist under ${GATE_TIMING_LEDGER_DIR} — possible identity drift between ledger writes and closeout.`);
|
|
@@ -9855,6 +9921,7 @@ function createCloseoutCommand(output, dependencies = {}) {
|
|
|
9855
9921
|
});
|
|
9856
9922
|
const retro = buildRetroGate({
|
|
9857
9923
|
codexThreadIds: codexThreadIdsForCloseout(dependencies.env),
|
|
9924
|
+
...dependencies.env === void 0 ? {} : { env: dependencies.env },
|
|
9858
9925
|
epic: options.epic,
|
|
9859
9926
|
...options.issue === void 0 ? {} : { issue: options.issue },
|
|
9860
9927
|
...options.pr === void 0 ? {} : { pr: options.pr },
|
|
@@ -11167,6 +11234,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
11167
11234
|
impactStamp = computeImpactStamp({
|
|
11168
11235
|
changedFiles: files,
|
|
11169
11236
|
profile,
|
|
11237
|
+
profilePath: relativeProfile,
|
|
11170
11238
|
readLockfile: (side) => readLockfileAtRef(cwd, side === "base" ? proofMergeBaseSha : headSha, path.resolve(cwd, LOCKFILE_PATH))
|
|
11171
11239
|
});
|
|
11172
11240
|
} catch (error) {
|
package/dist/schemas.d.ts
CHANGED
|
@@ -493,11 +493,11 @@ interface PrReadyProof {
|
|
|
493
493
|
*/
|
|
494
494
|
declare const impactStampSchema: z.ZodObject<{
|
|
495
495
|
basis: z.ZodEnum<{
|
|
496
|
-
"
|
|
496
|
+
"target-scoped": "target-scoped";
|
|
497
497
|
conservative: "conservative";
|
|
498
498
|
}>;
|
|
499
499
|
reasons: z.ZodArray<z.ZodString>;
|
|
500
|
-
stampVersion: z.ZodLiteral<
|
|
500
|
+
stampVersion: z.ZodLiteral<2>;
|
|
501
501
|
targets: z.ZodArray<z.ZodObject<{
|
|
502
502
|
basis: z.ZodString;
|
|
503
503
|
impact: z.ZodEnum<{
|
|
@@ -1362,11 +1362,11 @@ declare const prVerifyProofSchema: z.ZodObject<{
|
|
|
1362
1362
|
headSha: z.ZodString;
|
|
1363
1363
|
impactStamp: z.ZodOptional<z.ZodObject<{
|
|
1364
1364
|
basis: z.ZodEnum<{
|
|
1365
|
-
"
|
|
1365
|
+
"target-scoped": "target-scoped";
|
|
1366
1366
|
conservative: "conservative";
|
|
1367
1367
|
}>;
|
|
1368
1368
|
reasons: z.ZodArray<z.ZodString>;
|
|
1369
|
-
stampVersion: z.ZodLiteral<
|
|
1369
|
+
stampVersion: z.ZodLiteral<2>;
|
|
1370
1370
|
targets: z.ZodArray<z.ZodObject<{
|
|
1371
1371
|
basis: z.ZodString;
|
|
1372
1372
|
impact: z.ZodEnum<{
|
package/dist/schemas.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import "yaml";
|
|
3
|
+
import "picomatch";
|
|
3
4
|
//#region src/review-rungs.ts
|
|
4
5
|
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
5
6
|
"independent-model",
|
|
@@ -239,9 +240,9 @@ const impactStampTargetSchema = z.object({
|
|
|
239
240
|
* input widened every target to full impact.
|
|
240
241
|
*/
|
|
241
242
|
const impactStampSchema = z.object({
|
|
242
|
-
basis: z.enum(["
|
|
243
|
+
basis: z.enum(["target-scoped", "conservative"]),
|
|
243
244
|
reasons: z.array(z.string()),
|
|
244
|
-
stampVersion: z.literal(
|
|
245
|
+
stampVersion: z.literal(2),
|
|
245
246
|
targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
|
|
246
247
|
const names = targets.map((target) => target.name);
|
|
247
248
|
if (new Set(names).size !== names.length) context.addIssue({
|
|
@@ -267,7 +268,7 @@ const IMPACT_SCOPE_SURFACES = {
|
|
|
267
268
|
* external proof demand (#542 wave 2), a verification-battery command, or a
|
|
268
269
|
* preview (Alchemy) lifecycle stack (#430 wave 3).
|
|
269
270
|
*
|
|
270
|
-
* The only release path is a `
|
|
271
|
+
* The only release path is a `target-scoped` stamp of exactly this build's
|
|
271
272
|
* stamp version whose target entry for exactly this name is provably
|
|
272
273
|
* `not-affected`. Every other input — no trusted stamp, an unknown stamp
|
|
273
274
|
* version, a conservative stamp, a name the stamp does not classify, or an
|
|
@@ -286,11 +287,11 @@ const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
|
|
|
286
287
|
reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
|
|
287
288
|
scoped: false
|
|
288
289
|
};
|
|
289
|
-
if (stamp.stampVersion !==
|
|
290
|
-
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version
|
|
290
|
+
if (stamp.stampVersion !== 2) return {
|
|
291
|
+
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 2; ${floor}`,
|
|
291
292
|
scoped: false
|
|
292
293
|
};
|
|
293
|
-
if (stamp.basis !== "
|
|
294
|
+
if (stamp.basis !== "target-scoped") return {
|
|
294
295
|
reason: `impact stamp is conservative (full impact assumed); ${floor}`,
|
|
295
296
|
scoped: false
|
|
296
297
|
};
|
|
@@ -435,7 +436,7 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
435
436
|
* so authorization is bound to the same identities the proof binds. The
|
|
436
437
|
* predicate is the shared {@link impactStampScopeDecision}, not a second
|
|
437
438
|
* reading of the stamp: an entry is authorized exactly when the stamp would
|
|
438
|
-
* have released that target's command in the first place (
|
|
439
|
+
* have released that target's command in the first place (target-scoped
|
|
439
440
|
* basis, this build's stamp version, exactly one row for the target,
|
|
440
441
|
* `not-affected`). A conservative stamp, an unknown version, a
|
|
441
442
|
* self-contradictory stamp, an unclassified name, or an `affected` verdict all
|