@patronage/software-factory 1.0.0-alpha.8 → 1.0.0-alpha.9
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/CONTEXT.md +10 -0
- package/README.md +3 -1
- package/dist/index.d.ts +15 -14
- package/dist/index.js +283 -55
- package/dist/schemas.d.ts +4 -2
- package/dist/schemas.js +5 -4
- package/package.json +2 -2
package/CONTEXT.md
CHANGED
|
@@ -6,12 +6,22 @@ The shared Patronage software factory: typed lifecycle state, proof gates, and o
|
|
|
6
6
|
|
|
7
7
|
## Language
|
|
8
8
|
|
|
9
|
+
### Admission
|
|
10
|
+
|
|
11
|
+
**Candidate**: The exact patch under admission — the `headSha` / `patchId` / `mergeBaseSha` identity every piece of evidence binds to. A GitHub draft pull request is not a Candidate; GitHub's draft boolean is the machine lifecycle authority, and ready-for-review is the Candidate state. Experiment and interactive describe human intent only and create no machine states.
|
|
12
|
+
|
|
13
|
+
**Promotion**: The one transition from a draft pull request into a Candidate. `pr:publish` is the promotion verb. Its composed handoff finishes proof while draft, promotes, then evaluates Candidate demands at the unchanged head. Draft open and synchronize events run no substantive hosted Verify battery; `ready_for_review` starts the complete applicable Candidate pass.
|
|
14
|
+
|
|
9
15
|
### Proof reuse
|
|
10
16
|
|
|
11
17
|
**Proof gate**: A command that emits a typed proof — the self-timed set is `pr:verify`, `pr:review`, `pr:ready`, `boundary:check`, and `factory:closeout`; one execution is a **gate run**. Typed proof files are the source of readiness truth; prose never substitutes.
|
|
12
18
|
|
|
13
19
|
**Proof-reuse gate**: The decision that consumes trusted verification proof and determines whether hosted CI may skip work already performed. Refusing reuse runs the hosted suite; it does not fail the candidate. Defined by ADR 0022. _Avoid_: proof gate (that produces readiness proof rather than reusing it).
|
|
14
20
|
|
|
21
|
+
### Impact
|
|
22
|
+
|
|
23
|
+
**Impact**: The candidate-bound classification of which declared product targets a changed path or lockfile graph can reach. Path impact is opt-in: a target is affected if and only if a valid changed path matches one of its declared globs. A valid path matched by no target is **unsubscribed** and demands no target-scoped work, while remaining explicit in the stamp basis and human summary. Shared paths affect every target that subscribes to them. Invalid paths and unreadable or malformed required inputs still fail closed. Impact may withdraw target-scoped work; it never certifies a merge target, whose push-triggered verification stays full. Defined by ADR 0027. _Avoid_: affectedness; ownership (the declaration is a subscription, not an implicit filesystem owner).
|
|
24
|
+
|
|
15
25
|
### Planning intake
|
|
16
26
|
|
|
17
27
|
**Spec**: The single through-line planning artifact — a technical, non-technical, or blended description of the work to be built, produced by `to-spec`. The term supersedes "PRD"; older artifacts titled "PRD:" (e.g. #232) are specs by another name. _Avoid_: PRD (retired), requirements doc.
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ A stacked release train has one terminal release-owning slice. Interior slices d
|
|
|
37
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
38
|
|
|
39
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.
|
|
40
|
-
2.
|
|
40
|
+
2. Build and review one pnpm tarball, then publish that exact tarball per the [repository operations runbook](../docs/repository-operations.md#publishing) (attended and hand-cut). Publishing the workspace directory is not allowed because it rebuilds during `prepack` after the artifact review. **Always pass `--tag` explicitly, on every publish, prerelease or stable**: `pnpm publish "$SOFTWARE_FACTORY_TARBALL" --tag next --access public` for a prerelease, `--tag latest` for a stable cut. A filtered directory publish takes pnpm's recursive path, which passes its own `--tag` (defaulting to `latest`) to npm and therefore ignores the `publishConfig.tag: next` pin both factory packages carry; that is how 1.0.0-alpha.6 briefly took `latest` (#658). The pins stay as belt and braces, but they are not the control. Omitting the flag is not a safe failure: it moves `latest`, which the convergence assertion can only report after the fact. The attended token path does not promise an npm provenance attestation; the source SHA and reviewed tarball SRI are its operational provenance.
|
|
41
41
|
3. Cut the `software-factory-vX.Y.Z` tag and push it.
|
|
42
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.
|
|
43
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.
|
|
@@ -60,6 +60,8 @@ pnpm exec patronage-factory --help # or: psf --help
|
|
|
60
60
|
|
|
61
61
|
The repository profile declares its admission policy. Operator-local settings and HQ credentials are documented separately, so they do not become committed project configuration.
|
|
62
62
|
|
|
63
|
+
Generated production workflows may call `psf production:impact --before <sha> --after <sha> --github-output <path>`. The command recomputes the profile's opt-in impact subscriptions from those exact real commits and emits stable demanded/withdrawn target outputs, the changed-file decision basis, and unsubscribed paths. It only withdraws work: missing, zero, malformed, unreachable, non-ancestral, or otherwise unclassifiable identities leave every known target demanded. The factory owns this decision seam; each consumer owns its deploy jobs, commands, credentials, topology, and post-deploy convergence/no-op proof. Merge-target Verify remains full and does not consult this command.
|
|
64
|
+
|
|
63
65
|
For factory workflow, install the four canonical skills from the repository:
|
|
64
66
|
|
|
65
67
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -743,8 +743,8 @@ declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
743
743
|
generationId: z.ZodNumber;
|
|
744
744
|
headSha: z.ZodString;
|
|
745
745
|
outcome: z.ZodEnum<{
|
|
746
|
-
active: "active";
|
|
747
746
|
stale: "stale";
|
|
747
|
+
active: "active";
|
|
748
748
|
}>;
|
|
749
749
|
reason: z.ZodString;
|
|
750
750
|
recordedAt: z.ZodISODateTime;
|
|
@@ -1243,8 +1243,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1243
1243
|
status: z.ZodEnum<{
|
|
1244
1244
|
blocked: "blocked";
|
|
1245
1245
|
"not-required": "not-required";
|
|
1246
|
-
stale: "stale";
|
|
1247
1246
|
current: "current";
|
|
1247
|
+
stale: "stale";
|
|
1248
1248
|
missing: "missing";
|
|
1249
1249
|
}>;
|
|
1250
1250
|
}, z.core.$strip>;
|
|
@@ -1255,8 +1255,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1255
1255
|
status: z.ZodEnum<{
|
|
1256
1256
|
blocked: "blocked";
|
|
1257
1257
|
"not-required": "not-required";
|
|
1258
|
-
stale: "stale";
|
|
1259
1258
|
current: "current";
|
|
1259
|
+
stale: "stale";
|
|
1260
1260
|
missing: "missing";
|
|
1261
1261
|
}>;
|
|
1262
1262
|
}, z.core.$strip>>;
|
|
@@ -1267,8 +1267,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1267
1267
|
docsOnlyDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
|
|
1268
1268
|
docsOnlyVerifiedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1269
1269
|
prVerify: z.ZodEnum<{
|
|
1270
|
-
stale: "stale";
|
|
1271
1270
|
passed: "passed";
|
|
1271
|
+
stale: "stale";
|
|
1272
1272
|
missing: "missing";
|
|
1273
1273
|
}>;
|
|
1274
1274
|
trivialDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -1517,7 +1517,7 @@ declare const impactStampSchema: z.ZodObject<{
|
|
|
1517
1517
|
conservative: "conservative";
|
|
1518
1518
|
}>;
|
|
1519
1519
|
reasons: z.ZodArray<z.ZodString>;
|
|
1520
|
-
stampVersion: z.ZodLiteral<
|
|
1520
|
+
stampVersion: z.ZodLiteral<3>;
|
|
1521
1521
|
targets: z.ZodArray<z.ZodObject<{
|
|
1522
1522
|
basis: z.ZodString;
|
|
1523
1523
|
impact: z.ZodEnum<{
|
|
@@ -1526,6 +1526,7 @@ declare const impactStampSchema: z.ZodObject<{
|
|
|
1526
1526
|
}>;
|
|
1527
1527
|
name: z.ZodString;
|
|
1528
1528
|
}, z.core.$strip>>;
|
|
1529
|
+
unsubscribedPaths: z.ZodArray<z.ZodString>;
|
|
1529
1530
|
}, z.core.$strip>;
|
|
1530
1531
|
type ImpactStamp = z.infer<typeof impactStampSchema>;
|
|
1531
1532
|
/**
|
|
@@ -1770,8 +1771,8 @@ declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale"
|
|
|
1770
1771
|
declare const reviewStatusSchema: z.ZodEnum<{
|
|
1771
1772
|
blocked: "blocked";
|
|
1772
1773
|
"not-required": "not-required";
|
|
1773
|
-
stale: "stale";
|
|
1774
1774
|
current: "current";
|
|
1775
|
+
stale: "stale";
|
|
1775
1776
|
missing: "missing";
|
|
1776
1777
|
}>;
|
|
1777
1778
|
type ReviewStatus = z.infer<typeof reviewStatusSchema>;
|
|
@@ -2177,9 +2178,9 @@ declare const retroEnvelopeV1Schema: z.ZodObject<{
|
|
|
2177
2178
|
kind: z.ZodLiteral<"retro-envelope">;
|
|
2178
2179
|
outcome: z.ZodOptional<z.ZodObject<{
|
|
2179
2180
|
status: z.ZodEnum<{
|
|
2180
|
-
blocked: "blocked";
|
|
2181
2181
|
success: "success";
|
|
2182
2182
|
fail: "fail";
|
|
2183
|
+
blocked: "blocked";
|
|
2183
2184
|
"ship-with-followups": "ship-with-followups";
|
|
2184
2185
|
}>;
|
|
2185
2186
|
verdict: z.ZodOptional<z.ZodString>;
|
|
@@ -3323,8 +3324,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3323
3324
|
interiorCycle: z.ZodOptional<z.ZodNumber>;
|
|
3324
3325
|
observedAt: z.ZodString;
|
|
3325
3326
|
purpose: z.ZodEnum<{
|
|
3326
|
-
review: "review";
|
|
3327
3327
|
code: "code";
|
|
3328
|
+
review: "review";
|
|
3328
3329
|
}>;
|
|
3329
3330
|
slotResolution: z.ZodObject<{
|
|
3330
3331
|
effort: z.ZodEnum<{
|
|
@@ -3402,8 +3403,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3402
3403
|
interiorCycle: z.ZodOptional<z.ZodNumber>;
|
|
3403
3404
|
observedAt: z.ZodString;
|
|
3404
3405
|
purpose: z.ZodEnum<{
|
|
3405
|
-
review: "review";
|
|
3406
3406
|
code: "code";
|
|
3407
|
+
review: "review";
|
|
3407
3408
|
}>;
|
|
3408
3409
|
slotResolution: z.ZodObject<{
|
|
3409
3410
|
effort: z.ZodEnum<{
|
|
@@ -3472,8 +3473,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3472
3473
|
interiorCycle: z.ZodOptional<z.ZodNumber>;
|
|
3473
3474
|
observedAt: z.ZodString;
|
|
3474
3475
|
purpose: z.ZodEnum<{
|
|
3475
|
-
review: "review";
|
|
3476
3476
|
code: "code";
|
|
3477
|
+
review: "review";
|
|
3477
3478
|
}>;
|
|
3478
3479
|
slotResolution: z.ZodObject<{
|
|
3479
3480
|
effort: z.ZodEnum<{
|
|
@@ -3545,7 +3546,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3545
3546
|
exitCode: number | null;
|
|
3546
3547
|
harness: string;
|
|
3547
3548
|
observedAt: string;
|
|
3548
|
-
purpose: "
|
|
3549
|
+
purpose: "code" | "review";
|
|
3549
3550
|
slotResolution: {
|
|
3550
3551
|
effort: "high" | "low" | "medium" | "xhigh";
|
|
3551
3552
|
engine: string;
|
|
@@ -4066,13 +4067,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4066
4067
|
reviews: {
|
|
4067
4068
|
correctness: {
|
|
4068
4069
|
required: boolean;
|
|
4069
|
-
status: "blocked" | "not-required" | "
|
|
4070
|
+
status: "blocked" | "not-required" | "current" | "stale" | "missing";
|
|
4070
4071
|
reviewedHeadSha?: string | undefined;
|
|
4071
4072
|
reviewedPatchId?: string | undefined;
|
|
4072
4073
|
};
|
|
4073
4074
|
security?: {
|
|
4074
4075
|
required: boolean;
|
|
4075
|
-
status: "blocked" | "not-required" | "
|
|
4076
|
+
status: "blocked" | "not-required" | "current" | "stale" | "missing";
|
|
4076
4077
|
reviewedHeadSha?: string | undefined;
|
|
4077
4078
|
reviewedPatchId?: string | undefined;
|
|
4078
4079
|
} | undefined;
|
|
@@ -4080,7 +4081,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4080
4081
|
schemaVersion: 1;
|
|
4081
4082
|
verification: {
|
|
4082
4083
|
command: "patronage-factory pr:verify";
|
|
4083
|
-
prVerify: "
|
|
4084
|
+
prVerify: "passed" | "stale" | "missing";
|
|
4084
4085
|
docsOnlyDeltaAccepted?: boolean | undefined;
|
|
4085
4086
|
docsOnlyVerifiedHeadSha?: string | undefined;
|
|
4086
4087
|
trivialDeltaAccepted?: boolean | undefined;
|
package/dist/index.js
CHANGED
|
@@ -15,9 +15,9 @@ import { Worker } from "node:worker_threads";
|
|
|
15
15
|
import picomatch from "picomatch";
|
|
16
16
|
import { parse } from "yaml";
|
|
17
17
|
import { promisify } from "node:util";
|
|
18
|
-
import { FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, readVitestProfileDocument } from "@patronage/factory-ci";
|
|
18
|
+
import { FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
|
|
19
19
|
//#region package.json
|
|
20
|
-
var version = "1.0.0-alpha.
|
|
20
|
+
var version = "1.0.0-alpha.9";
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/review-rungs.ts
|
|
23
23
|
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
@@ -981,7 +981,7 @@ const requiredCheckSchema = z.object({
|
|
|
981
981
|
const impactTargetSchema = z.object({
|
|
982
982
|
importers: z.array(z.string().min(1)).min(1),
|
|
983
983
|
name: z.string().min(1),
|
|
984
|
-
paths: z.array(repoRelativeGlobSchema).min(1).optional()
|
|
984
|
+
paths: z.array(repoRelativeGlobSchema).min(1).optional().describe("Opt-in repo-relative path subscriptions. A target is affected only when a valid changed path matches one of its globs; valid unmatched paths affect no target and are recorded as unsubscribed. Shared files must be listed by every subscribing target.")
|
|
985
985
|
}).strict();
|
|
986
986
|
const impactConfigSchema = z.object({ targets: z.array(impactTargetSchema).min(1).superRefine((targets, context) => {
|
|
987
987
|
const names = targets.map((target) => target.name);
|
|
@@ -3553,15 +3553,18 @@ const impactStampTargetSchema = z.object({
|
|
|
3553
3553
|
const impactStampSchema = z.object({
|
|
3554
3554
|
basis: z.enum(["target-scoped", "conservative"]),
|
|
3555
3555
|
reasons: z.array(z.string()),
|
|
3556
|
-
stampVersion: z.literal(
|
|
3556
|
+
stampVersion: z.literal(3),
|
|
3557
3557
|
targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
|
|
3558
3558
|
const names = targets.map((target) => target.name);
|
|
3559
3559
|
if (new Set(names).size !== names.length) context.addIssue({
|
|
3560
3560
|
code: "custom",
|
|
3561
3561
|
message: "impact stamp targets must have distinct names."
|
|
3562
3562
|
});
|
|
3563
|
-
})
|
|
3563
|
+
}),
|
|
3564
|
+
unsubscribedPaths: z.array(z.string())
|
|
3564
3565
|
});
|
|
3566
|
+
/** Human-readable lines for every impact decision, including quiet releases. */
|
|
3567
|
+
const impactStampSummaryLines = (stamp) => [`Impact stamp: ${stamp.basis}; ${stamp.targets.filter(({ impact }) => impact === "affected").length}/${stamp.targets.length} target(s) affected.`, stamp.unsubscribedPaths.length === 0 ? "Unsubscribed changed paths: none." : `Unsubscribed changed paths (demand no target): ${stamp.unsubscribedPaths.join(", ")}`];
|
|
3565
3568
|
/**
|
|
3566
3569
|
* A configuration contradiction between the profile's impact declarations and
|
|
3567
3570
|
* the repository's actual lockfile — e.g. a declared importer root that
|
|
@@ -3611,8 +3614,8 @@ const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
|
|
|
3611
3614
|
reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
|
|
3612
3615
|
scoped: false
|
|
3613
3616
|
};
|
|
3614
|
-
if (stamp.stampVersion !==
|
|
3615
|
-
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version
|
|
3617
|
+
if (stamp.stampVersion !== 3) return {
|
|
3618
|
+
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 3; ${floor}`,
|
|
3616
3619
|
scoped: false
|
|
3617
3620
|
};
|
|
3618
3621
|
if (stamp.basis !== "target-scoped") return {
|
|
@@ -3654,15 +3657,16 @@ const impactStampDemandRelease = ({ checkName, stamp }) => {
|
|
|
3654
3657
|
* every doubt path lands on, and the stamp a producer must fall back to if
|
|
3655
3658
|
* stamp computation itself fails for any reason.
|
|
3656
3659
|
*/
|
|
3657
|
-
const conservativeImpactStamp = (targets, reasons) => ({
|
|
3660
|
+
const conservativeImpactStamp = (targets, reasons, unsubscribedPaths = []) => ({
|
|
3658
3661
|
basis: "conservative",
|
|
3659
3662
|
reasons,
|
|
3660
|
-
stampVersion:
|
|
3663
|
+
stampVersion: 3,
|
|
3661
3664
|
targets: targets.map((target) => ({
|
|
3662
3665
|
basis: CONSERVATIVE_TARGET_BASIS,
|
|
3663
3666
|
impact: "affected",
|
|
3664
3667
|
name: target.name
|
|
3665
|
-
}))
|
|
3668
|
+
})),
|
|
3669
|
+
unsubscribedPaths
|
|
3666
3670
|
});
|
|
3667
3671
|
const conservativeStamp = conservativeImpactStamp;
|
|
3668
3672
|
const DEPENDENCY_FIELDS = [
|
|
@@ -3793,9 +3797,10 @@ const targetImpactForLockfileDelta = (target, delta) => {
|
|
|
3793
3797
|
/**
|
|
3794
3798
|
* Compute the impact stamp for one candidate: every declared target recorded
|
|
3795
3799
|
* as affected/not-affected with its exact basis. `not-affected` is only ever
|
|
3796
|
-
* returned only when every changed input is
|
|
3797
|
-
*
|
|
3798
|
-
*
|
|
3800
|
+
* returned only when every changed input is a valid source path or an
|
|
3801
|
+
* analyzable pnpm-lock.yaml (v9) delta. Source and lockfile reachability are
|
|
3802
|
+
* unioned per target. A valid source path matched by no target is recorded in
|
|
3803
|
+
* `unsubscribedPaths` and affects none. Invalid, unreadable, unsupported, or
|
|
3799
3804
|
* otherwise doubtful input widens the whole stamp to full impact.
|
|
3800
3805
|
*
|
|
3801
3806
|
* Total by contract: this function never throws. Any unexpected failure in
|
|
@@ -3872,10 +3877,6 @@ const analyzeSourcePaths = (sourceFiles, targets, profilePath) => {
|
|
|
3872
3877
|
kind: "conservative",
|
|
3873
3878
|
reason: `changed source path "${file}" is the impact profile being evaluated; fail closed to full impact`
|
|
3874
3879
|
};
|
|
3875
|
-
if (!targets.some((target) => matchesAnyGlob(target.paths ?? [], file))) return {
|
|
3876
|
-
kind: "conservative",
|
|
3877
|
-
reason: `changed source path "${file}" has no declared impact target owner; fail closed to full impact`
|
|
3878
|
-
};
|
|
3879
3880
|
}
|
|
3880
3881
|
return {
|
|
3881
3882
|
kind: "ok",
|
|
@@ -3890,7 +3891,8 @@ const analyzeSourcePaths = (sourceFiles, targets, profilePath) => {
|
|
|
3890
3891
|
impact: "affected",
|
|
3891
3892
|
name: target.name
|
|
3892
3893
|
};
|
|
3893
|
-
})
|
|
3894
|
+
}),
|
|
3895
|
+
unsubscribedPaths: sourceFiles.filter((file) => !targets.some((target) => matchesAnyGlob(target.paths ?? [], file)))
|
|
3894
3896
|
};
|
|
3895
3897
|
};
|
|
3896
3898
|
const unionTargetImpact = (source, lockfile) => {
|
|
@@ -3906,8 +3908,9 @@ const computeImpactStampOrThrow = ({ changedFiles, profilePath = "software-facto
|
|
|
3906
3908
|
if (targets.length === 0) return {
|
|
3907
3909
|
basis: "conservative",
|
|
3908
3910
|
reasons: ["no impact targets declared in the profile; every surface keeps full demand"],
|
|
3909
|
-
stampVersion:
|
|
3910
|
-
targets: []
|
|
3911
|
+
stampVersion: 3,
|
|
3912
|
+
targets: [],
|
|
3913
|
+
unsubscribedPaths: changedFiles.filter((file) => file !== "pnpm-lock.yaml" && isRepoRelativePath(file))
|
|
3911
3914
|
};
|
|
3912
3915
|
assertDeclaredPathGlobs(targets);
|
|
3913
3916
|
const sides = readLockfileSides(readLockfile);
|
|
@@ -3916,18 +3919,19 @@ const computeImpactStampOrThrow = ({ changedFiles, profilePath = "software-facto
|
|
|
3916
3919
|
const sourceFiles = changedFiles.filter((file) => file !== LOCKFILE_PATH);
|
|
3917
3920
|
const source = analyzeSourcePaths(sourceFiles, targets, profilePath);
|
|
3918
3921
|
if (source.kind === "conservative") return conservativeStamp(targets, [source.reason]);
|
|
3919
|
-
if (sides.kind === "threw")
|
|
3920
|
-
if (sides.kind === "unreadable") return conservativeStamp(targets, [`pnpm-lock.yaml is unreadable at the ${sides.side} side; fail closed to full impact`]);
|
|
3921
|
-
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`]);
|
|
3922
|
-
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`]);
|
|
3922
|
+
if (sides.kind === "threw") return conservativeStamp(targets, [`impact stamp computation failed (${sides.error instanceof Error ? sides.error.message : String(sides.error)}); fail closed to full impact`], source.unsubscribedPaths);
|
|
3923
|
+
if (sides.kind === "unreadable") return conservativeStamp(targets, [`pnpm-lock.yaml is unreadable at the ${sides.side} side; fail closed to full impact`], source.unsubscribedPaths);
|
|
3924
|
+
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`], source.unsubscribedPaths);
|
|
3925
|
+
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`], source.unsubscribedPaths);
|
|
3923
3926
|
if (!changedFiles.includes("pnpm-lock.yaml")) return {
|
|
3924
3927
|
basis: "target-scoped",
|
|
3925
|
-
reasons: ["all changed source paths
|
|
3926
|
-
stampVersion:
|
|
3927
|
-
targets: source.targets
|
|
3928
|
+
reasons: [source.unsubscribedPaths.length === 0 ? "all changed source paths match at least one declared target subscription; impact scoped by repo-relative path globs" : `${source.unsubscribedPaths.length} changed source path(s) match no declared target subscription and affect no target`],
|
|
3929
|
+
stampVersion: 3,
|
|
3930
|
+
targets: source.targets,
|
|
3931
|
+
unsubscribedPaths: source.unsubscribedPaths
|
|
3928
3932
|
};
|
|
3929
3933
|
const [baseLock, headLock] = sides.parsed;
|
|
3930
|
-
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"]);
|
|
3934
|
+
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"], source.unsubscribedPaths);
|
|
3931
3935
|
const delta = {
|
|
3932
3936
|
changedImporters: changedSectionKeys(baseLock.importers, headLock.importers),
|
|
3933
3937
|
changedPackages: changedSectionKeys(baseLock.packages, headLock.packages),
|
|
@@ -3938,9 +3942,10 @@ const computeImpactStampOrThrow = ({ changedFiles, profilePath = "software-facto
|
|
|
3938
3942
|
const mixed = sourceFiles.length > 0;
|
|
3939
3943
|
return {
|
|
3940
3944
|
basis: "target-scoped",
|
|
3941
|
-
reasons: mixed ? ["all changed source paths
|
|
3942
|
-
stampVersion:
|
|
3943
|
-
targets: mixed ? source.targets.map((sourceTarget, index) => unionTargetImpact(sourceTarget, lockfileTargets[index])) : lockfileTargets
|
|
3945
|
+
reasons: mixed ? [source.unsubscribedPaths.length === 0 ? "all changed source paths match at least one declared target subscription and pnpm-lock.yaml is analyzable; impact is the union of path subscriptions and importer/snapshot graph analysis" : `${source.unsubscribedPaths.length} changed source path(s) match no declared target subscription; subscribed path impact is unioned with importer/snapshot graph analysis`] : ["pnpm-lock.yaml is the only changed file; impact scoped by importer/snapshot graph analysis"],
|
|
3946
|
+
stampVersion: 3,
|
|
3947
|
+
targets: mixed ? source.targets.map((sourceTarget, index) => unionTargetImpact(sourceTarget, lockfileTargets[index])) : lockfileTargets,
|
|
3948
|
+
unsubscribedPaths: source.unsubscribedPaths
|
|
3944
3949
|
};
|
|
3945
3950
|
};
|
|
3946
3951
|
//#endregion
|
|
@@ -5543,17 +5548,21 @@ const asClassification = (value) => DIFF_CLASSIFICATIONS$1.includes(value) ? val
|
|
|
5543
5548
|
const prVerifyCheckPayloadFor = (proof) => {
|
|
5544
5549
|
const value = proof;
|
|
5545
5550
|
if (!value || typeof value.headSha !== "string" || value.headSha.length === 0 || !isResolvedPrVerifyMode(value.mode)) return;
|
|
5551
|
+
const carriesReleases = value.notRequiredCommands !== void 0;
|
|
5546
5552
|
return {
|
|
5547
5553
|
classification: asClassification(value.classification),
|
|
5548
5554
|
executedCommands: (value.executedCommands ?? []).map(({ name }) => name).filter((name) => typeof name === "string"),
|
|
5549
5555
|
headSha: value.headSha,
|
|
5556
|
+
...carriesReleases && value.impactStamp !== void 0 ? { impactStamp: value.impactStamp } : {},
|
|
5550
5557
|
kind: PR_VERIFY_CHECK_PAYLOAD_KIND,
|
|
5551
5558
|
mode: value.mode,
|
|
5559
|
+
...carriesReleases ? { notRequiredCommands: value.notRequiredCommands } : {},
|
|
5552
5560
|
outcome: outcomeFor(value),
|
|
5553
5561
|
proofSchemaVersion: typeof value.schemaVersion === "number" ? value.schemaVersion : 0,
|
|
5554
5562
|
schemaVersion: PR_VERIFY_CHECK_PAYLOAD_SCHEMA_VERSION,
|
|
5555
5563
|
...typeof value.patchId === "string" ? { patchId: value.patchId } : {},
|
|
5556
|
-
...typeof value.repository === "string" ? { repository: value.repository } : {}
|
|
5564
|
+
...typeof value.repository === "string" ? { repository: value.repository } : {},
|
|
5565
|
+
...carriesReleases && value.verificationCommands !== void 0 ? { verificationCommands: value.verificationCommands } : {}
|
|
5557
5566
|
};
|
|
5558
5567
|
};
|
|
5559
5568
|
const renderPrVerifyCheckPayloadText = (payload) => `\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\``;
|
|
@@ -7442,9 +7451,15 @@ const installationToken = (repository, config, request, now, timeoutMs = GITHUB_
|
|
|
7442
7451
|
function verifyOutput(proof) {
|
|
7443
7452
|
const value = proof;
|
|
7444
7453
|
const commands = value.executedCommands?.length ?? 0;
|
|
7454
|
+
const unsubscribedBasis = value.impactStamp?.unsubscribedPaths;
|
|
7455
|
+
let unsubscribedSummary;
|
|
7456
|
+
if (Array.isArray(unsubscribedBasis)) {
|
|
7457
|
+
const unsubscribed = unsubscribedBasis.filter((path) => typeof path === "string");
|
|
7458
|
+
unsubscribedSummary = unsubscribed.length === 0 ? "Unsubscribed changed paths: none." : `Unsubscribed changed paths (demand no target): ${unsubscribed.join(", ")}.`;
|
|
7459
|
+
} else unsubscribedSummary = "Unsubscribed changed paths: unavailable (legacy or malformed stamp).";
|
|
7445
7460
|
const payload = prVerifyCheckPayloadFor(proof);
|
|
7446
7461
|
return {
|
|
7447
|
-
summary: `Schema v${value.schemaVersion}; mode **${value.mode ?? "unknown"}**; classification **${value.classification ?? "unknown"}**; outcome **${value.outcome ?? "unknown"}**; ${commands} command(s) executed
|
|
7462
|
+
summary: `Schema v${value.schemaVersion}; mode **${value.mode ?? "unknown"}**; classification **${value.classification ?? "unknown"}**; outcome **${value.outcome ?? "unknown"}**; ${commands} command(s) executed. ${unsubscribedSummary}`,
|
|
7448
7463
|
...payload ? { text: renderPrVerifyCheckPayloadText(payload) } : {},
|
|
7449
7464
|
title: `pr:verify ${value.outcome ?? "unknown"}`
|
|
7450
7465
|
};
|
|
@@ -7458,8 +7473,10 @@ function reviewOutput(proof) {
|
|
|
7458
7473
|
for (const { disposition } of value.ladder ? ladderLedgerFor(value.ladder) : []) dispositionCounts[disposition] = (dispositionCounts[disposition] ?? 0) + 1;
|
|
7459
7474
|
if (!value.ladder && findings > 0) dispositionCounts.open = findings;
|
|
7460
7475
|
const dispositions = Object.entries(dispositionCounts).map(([name, count]) => `${name} ${count}`).join(", ");
|
|
7476
|
+
const unavailable = "unavailable (legacy proof)";
|
|
7477
|
+
const provenance = reviews.length === 0 ? "not required" : reviews.map(({ kind, model, producer, rung }) => `${kind ?? "unknown mode"}: rung ${rung ?? unavailable}; producer ${producer ?? unavailable}; model ${model ?? unavailable}`).join(" | ");
|
|
7461
7478
|
return {
|
|
7462
|
-
summary: `Schema v${value.schemaVersion}; modes **${modes}**; ${findings} finding(s) recorded; dispositions: ${dispositions || "none"}.`,
|
|
7479
|
+
summary: `Schema v${value.schemaVersion}; modes **${modes}**; ${findings} finding(s) recorded; dispositions: ${dispositions || "none"}. Review provenance: ${provenance}.`,
|
|
7463
7480
|
title: findings === 0 ? "pr:review clean" : "pr:review proof recorded"
|
|
7464
7481
|
};
|
|
7465
7482
|
}
|
|
@@ -11632,6 +11649,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
11632
11649
|
if (error instanceof ImpactStampConfigError) throw error;
|
|
11633
11650
|
impactStamp = conservativeImpactStamp(profile.impact?.targets ?? [], [`impact stamp computation failed (${error instanceof Error ? error.message : String(error)}); fail closed to full impact`]);
|
|
11634
11651
|
}
|
|
11652
|
+
for (const line of impactStampSummaryLines(impactStamp)) console.log(line);
|
|
11635
11653
|
const vetoedTargets = boundFailingCheckNames({
|
|
11636
11654
|
candidate: {
|
|
11637
11655
|
headSha,
|
|
@@ -13593,6 +13611,13 @@ const prVerifyFollowUp = (authoringSession) => followUpFromArgv([
|
|
|
13593
13611
|
"pr:verify",
|
|
13594
13612
|
...authoringSession ? ["--authoring-session", authoringSession] : []
|
|
13595
13613
|
]);
|
|
13614
|
+
/** The route-owned Promotion from a GitHub draft into a Candidate. */
|
|
13615
|
+
const prUndraftFollowUp = (pr) => followUpFromArgv([
|
|
13616
|
+
"gh",
|
|
13617
|
+
"pr",
|
|
13618
|
+
"ready",
|
|
13619
|
+
String(pr)
|
|
13620
|
+
]);
|
|
13596
13621
|
/** Canonical zod schema for the optional follow-up field on JSON outputs. */
|
|
13597
13622
|
const FollowUpActionSchema = z.object({
|
|
13598
13623
|
argv: z.array(z.string()),
|
|
@@ -14708,15 +14733,18 @@ const collectBlockers = ({ correctnessRequired, correctnessStatus, correctnessUn
|
|
|
14708
14733
|
command: `gh pr checks ${input.pr} --watch`
|
|
14709
14734
|
} } : {}
|
|
14710
14735
|
});
|
|
14711
|
-
if (input.draft)
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14736
|
+
if (input.draft) {
|
|
14737
|
+
const promotion = prUndraftFollowUp(input.pr);
|
|
14738
|
+
blockers.push({
|
|
14739
|
+
demand: DEMAND_KEYS.draft,
|
|
14740
|
+
reason: DRAFT_BLOCKER_REASON,
|
|
14741
|
+
repair: {
|
|
14742
|
+
action: "Mark the PR ready for review; draft is route-owned once proof gates pass.",
|
|
14743
|
+
code: "undraft-pr",
|
|
14744
|
+
command: promotion.command
|
|
14745
|
+
}
|
|
14746
|
+
});
|
|
14747
|
+
}
|
|
14720
14748
|
return blockers;
|
|
14721
14749
|
};
|
|
14722
14750
|
const orderRepairs = (repairs) => repairs.toSorted((a, b) => READINESS_REPAIR_CODES.indexOf(a.code) - READINESS_REPAIR_CODES.indexOf(b.code));
|
|
@@ -15380,12 +15408,7 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
15380
15408
|
]);
|
|
15381
15409
|
let followUp;
|
|
15382
15410
|
if (evaluation.status === "ready") followUp = arming?.outcome === "not-armed" ? readyRedispatch() : void 0;
|
|
15383
|
-
else if (undraftRepair && evaluation.humanBlockingReasons.length === 0) followUp =
|
|
15384
|
-
"gh",
|
|
15385
|
-
"pr",
|
|
15386
|
-
"ready",
|
|
15387
|
-
String(args.pr)
|
|
15388
|
-
]);
|
|
15411
|
+
else if (undraftRepair && evaluation.humanBlockingReasons.length === 0) followUp = prUndraftFollowUp(args.pr);
|
|
15389
15412
|
else if (!pendingExternalChecksOnly) followUp = prVerifyFollowUp(authoringSession);
|
|
15390
15413
|
const notices = [
|
|
15391
15414
|
...evaluation.notices,
|
|
@@ -16184,6 +16207,50 @@ const reevaluateReadiness = (args, dependencies) => (dependencies.runPrReady ??
|
|
|
16184
16207
|
...args,
|
|
16185
16208
|
throwWhenBlocked: false
|
|
16186
16209
|
}, dependencies);
|
|
16210
|
+
const requirePromotionHead = ({ actualHeadSha, expectedHeadSha, phase }) => {
|
|
16211
|
+
if (!sameHeadSha(actualHeadSha, expectedHeadSha)) throw new PrPublishTransactionAbortedError(`${phase}: live PR head ${actualHeadSha} differs from the proved publish head ${expectedHeadSha}. Re-run pr:publish against the current head.`);
|
|
16212
|
+
};
|
|
16213
|
+
/**
|
|
16214
|
+
* Promote a proved draft before the first Candidate demand evaluation (#649).
|
|
16215
|
+
* The route-owned action is shared with pr:ready; this helper only orders it.
|
|
16216
|
+
*/
|
|
16217
|
+
const promoteDraftBeforeDemandEvaluation = async ({ cwd, expectedHeadSha, factoryCliInvocation, fetchPr, pr, prSnapshot, runFollowUp }) => {
|
|
16218
|
+
if (!prSnapshot.isDraft) return { status: "not-requested" };
|
|
16219
|
+
const before = fetchPr();
|
|
16220
|
+
requirePromotionHead({
|
|
16221
|
+
actualHeadSha: before.headRefOid,
|
|
16222
|
+
expectedHeadSha,
|
|
16223
|
+
phase: "Promotion refused before transition"
|
|
16224
|
+
});
|
|
16225
|
+
if (!before.isDraft) return { status: "not-requested" };
|
|
16226
|
+
const promotion = prUndraftFollowUp(pr);
|
|
16227
|
+
try {
|
|
16228
|
+
await (runFollowUp ?? defaultFollowUpRunner(factoryCliInvocation))(promotion, cwd);
|
|
16229
|
+
} catch (error) {
|
|
16230
|
+
throw new PrPublishTransactionAbortedError(`Promotion failed (${promotion.command}): ${errorMessage(error)}`);
|
|
16231
|
+
}
|
|
16232
|
+
const after = fetchPr();
|
|
16233
|
+
requirePromotionHead({
|
|
16234
|
+
actualHeadSha: after.headRefOid,
|
|
16235
|
+
expectedHeadSha,
|
|
16236
|
+
phase: "Promotion invalidated during transition"
|
|
16237
|
+
});
|
|
16238
|
+
if (after.isDraft) throw new PrPublishTransactionAbortedError(`Promotion did not make PR #${pr} ready for review at proved head ${expectedHeadSha}. Re-run pr:publish after GitHub settles.`);
|
|
16239
|
+
return {
|
|
16240
|
+
action: promotion,
|
|
16241
|
+
reason: "Promotion precedes Candidate demand evaluation on the unchanged head",
|
|
16242
|
+
status: "succeeded"
|
|
16243
|
+
};
|
|
16244
|
+
};
|
|
16245
|
+
const requirePromotedProofHead = ({ expectedHeadSha, promotion, proof }) => {
|
|
16246
|
+
if (promotion.status !== "succeeded") return;
|
|
16247
|
+
requirePromotionHead({
|
|
16248
|
+
actualHeadSha: proof.ledger.headSha,
|
|
16249
|
+
expectedHeadSha,
|
|
16250
|
+
phase: "Promotion invalidated before Candidate demand evaluation completed"
|
|
16251
|
+
});
|
|
16252
|
+
};
|
|
16253
|
+
const candidateFollowUpAfterPromotion = (promotion, proof) => promotion.status === "not-requested" ? proof.followUp : void 0;
|
|
16187
16254
|
/**
|
|
16188
16255
|
* A proof with no follow-up that is not `ready` means `pr:ready` found the
|
|
16189
16256
|
* candidate blocked solely on a pending hosted check for the family excluded
|
|
@@ -16318,6 +16385,11 @@ async function runPrPublish(args, dependencies = {}) {
|
|
|
16318
16385
|
reviewProofPath
|
|
16319
16386
|
});
|
|
16320
16387
|
const pr = fetchPr();
|
|
16388
|
+
requirePromotionHead({
|
|
16389
|
+
actualHeadSha: pr.headRefOid,
|
|
16390
|
+
expectedHeadSha: verifyProof.headSha,
|
|
16391
|
+
phase: "Publish refused before managed body update"
|
|
16392
|
+
});
|
|
16321
16393
|
const parts = renderPrBodySectionParts({
|
|
16322
16394
|
reviewProof,
|
|
16323
16395
|
verifyProof: verifyProof && verifyProofPassed(verifyProof) ? verifyProof : void 0
|
|
@@ -16342,13 +16414,28 @@ async function runPrPublish(args, dependencies = {}) {
|
|
|
16342
16414
|
json: false,
|
|
16343
16415
|
report: false
|
|
16344
16416
|
};
|
|
16417
|
+
const promotion = await promoteDraftBeforeDemandEvaluation({
|
|
16418
|
+
cwd,
|
|
16419
|
+
expectedHeadSha: verifyProof.headSha,
|
|
16420
|
+
factoryCliInvocation,
|
|
16421
|
+
fetchPr,
|
|
16422
|
+
pr: prNumber,
|
|
16423
|
+
prSnapshot: pr,
|
|
16424
|
+
runFollowUp: dependencies.runFollowUp
|
|
16425
|
+
});
|
|
16345
16426
|
const initialProof = await reevaluateReadiness(readyEvalArgs, publishDependencies);
|
|
16427
|
+
requirePromotedProofHead({
|
|
16428
|
+
expectedHeadSha: verifyProof.headSha,
|
|
16429
|
+
promotion,
|
|
16430
|
+
proof: initialProof
|
|
16431
|
+
});
|
|
16346
16432
|
const initialHandoff = summarizeHumanHandoff(initialProof);
|
|
16347
|
-
let followUp =
|
|
16433
|
+
let followUp = promotion;
|
|
16348
16434
|
let currentProof = initialProof;
|
|
16349
|
-
|
|
16435
|
+
const candidateFollowUp = candidateFollowUpAfterPromotion(promotion, initialProof);
|
|
16436
|
+
if (candidateFollowUp) {
|
|
16350
16437
|
followUp = await settleFollowUp({
|
|
16351
|
-
action:
|
|
16438
|
+
action: candidateFollowUp,
|
|
16352
16439
|
cwd,
|
|
16353
16440
|
factoryCliInvocation,
|
|
16354
16441
|
onFailure: (failed, error) => {
|
|
@@ -16363,13 +16450,13 @@ async function runPrPublish(args, dependencies = {}) {
|
|
|
16363
16450
|
},
|
|
16364
16451
|
plan: planPublishFollowUp({
|
|
16365
16452
|
binding: verifyProofBinding,
|
|
16366
|
-
followUp:
|
|
16453
|
+
followUp: candidateFollowUp,
|
|
16367
16454
|
proofHeadSha: verifyProof.headSha,
|
|
16368
16455
|
verifiedHeadSha: verified.verifiedHeadSha
|
|
16369
16456
|
}),
|
|
16370
16457
|
runFollowUp: dependencies.runFollowUp
|
|
16371
16458
|
});
|
|
16372
|
-
if (followUp.status === "succeeded" && isSelfInvalidatingFollowUp(
|
|
16459
|
+
if (followUp.status === "succeeded" && isSelfInvalidatingFollowUp(candidateFollowUp)) currentProof = await reevaluateReadiness(readyEvalArgs, publishDependencies);
|
|
16373
16460
|
}
|
|
16374
16461
|
const proof = await awaitCurrentReadyVerdict({
|
|
16375
16462
|
args: readyEvalArgs,
|
|
@@ -16420,7 +16507,7 @@ async function deliverSpooledEvidence({ awaitPending, cwd, flush, profilePath })
|
|
|
16420
16507
|
//#endregion
|
|
16421
16508
|
//#region src/commands/pr-publish.ts
|
|
16422
16509
|
function createPrPublishCommand(_output, action = runPrPublish) {
|
|
16423
|
-
return markCwdOptionDefault(new Command("pr:publish").description("
|
|
16510
|
+
return markCwdOptionDefault(new Command("pr:publish").description("Promote a proved draft, then evaluate Candidate demands; never launches review").option("--pr <number>", "pull request number", positiveInteger("--pr")).option("--base <ref>", "explicit base branch or ref override").option("--authoring-session <id>", "known authoring session identity for verification retries").option("--cwd <path>", "working directory to evaluate", collectCwdOption).option("--epic <number>", "epic issue containing the factory-boundary manifest; the composed readiness evaluation then applies the wave-demanded review rung (#351)", positiveInteger("--epic")).option("--findings <path>", "clean-session findings to validate when no current review proof is available").option("--json", "print the publish result as JSON").option("--output <path>", "write readiness proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--review-proof <path>", "pr:review proof JSON path").option("--verify-proof <path>", "pr:verify proof JSON path").action(async (options) => {
|
|
16424
16511
|
await action({
|
|
16425
16512
|
authoringSessionIds: options.authoringSession ? [options.authoringSession] : void 0,
|
|
16426
16513
|
base: options.base ?? "",
|
|
@@ -16546,6 +16633,146 @@ function modeFor(options) {
|
|
|
16546
16633
|
if (options.full) return "full";
|
|
16547
16634
|
return "auto";
|
|
16548
16635
|
}
|
|
16636
|
+
const COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/iu;
|
|
16637
|
+
const ZERO_SHA_PATTERN = /^0{40}$/u;
|
|
16638
|
+
const demandedTargets = (profile, reason) => (profile?.impact?.targets ?? []).map(({ name }) => ({
|
|
16639
|
+
basis: reason,
|
|
16640
|
+
name,
|
|
16641
|
+
state: "demanded"
|
|
16642
|
+
}));
|
|
16643
|
+
const refusal = ({ after, before, profile, reason }) => ({
|
|
16644
|
+
...after ? { afterSha: after } : {},
|
|
16645
|
+
basis: {
|
|
16646
|
+
changedFiles: [],
|
|
16647
|
+
classification: "refused",
|
|
16648
|
+
reasons: [reason]
|
|
16649
|
+
},
|
|
16650
|
+
...before ? { beforeSha: before } : {},
|
|
16651
|
+
decision: "refused",
|
|
16652
|
+
schemaVersion: 1,
|
|
16653
|
+
targets: demandedTargets(profile, reason),
|
|
16654
|
+
unsubscribedPaths: []
|
|
16655
|
+
});
|
|
16656
|
+
const assertCommitIdentity = (value, name) => {
|
|
16657
|
+
if (value === void 0 || !COMMIT_SHA_PATTERN.test(value) || ZERO_SHA_PATTERN.test(value)) throw new Error(`${name} must be a non-zero 40-character commit SHA`);
|
|
16658
|
+
return value.toLowerCase();
|
|
16659
|
+
};
|
|
16660
|
+
const assertReachableCommit = (cwd, sha, name) => {
|
|
16661
|
+
try {
|
|
16662
|
+
runCapture("git", [
|
|
16663
|
+
"cat-file",
|
|
16664
|
+
"-e",
|
|
16665
|
+
`${sha}^{commit}`
|
|
16666
|
+
], cwd);
|
|
16667
|
+
} catch {
|
|
16668
|
+
throw new Error(`${name} commit ${sha} is not reachable in this checkout`);
|
|
16669
|
+
}
|
|
16670
|
+
};
|
|
16671
|
+
const writeGithubOutputs = (outputPath, decision) => {
|
|
16672
|
+
appendFileSync(outputPath, `${[
|
|
16673
|
+
`decision=${decision.decision}`,
|
|
16674
|
+
`basis=${JSON.stringify(decision.basis)}`,
|
|
16675
|
+
`unsubscribed_paths=${JSON.stringify(decision.unsubscribedPaths)}`,
|
|
16676
|
+
...decision.targets.map((target) => `${productionImpactTargetOutput(target.name)}=${target.state}`)
|
|
16677
|
+
].join("\n")}\n`, "utf-8");
|
|
16678
|
+
};
|
|
16679
|
+
/**
|
|
16680
|
+
* Recompute impact from one merge push's exact before/after commits. The
|
|
16681
|
+
* result may withdraw consumer work only; every doubt path returns a refused
|
|
16682
|
+
* decision whose known targets remain demanded.
|
|
16683
|
+
*/
|
|
16684
|
+
const runProductionImpact = (args) => {
|
|
16685
|
+
const cwd = path.resolve(args.cwd ?? process.cwd());
|
|
16686
|
+
let profile;
|
|
16687
|
+
let profilePath;
|
|
16688
|
+
let decision;
|
|
16689
|
+
try {
|
|
16690
|
+
const { path: loadedPath, profile: loadedProfile } = loadProjectProfile({
|
|
16691
|
+
cwd,
|
|
16692
|
+
profilePath: args.profilePath
|
|
16693
|
+
});
|
|
16694
|
+
profile = loadedProfile;
|
|
16695
|
+
profilePath = loadedPath;
|
|
16696
|
+
const before = assertCommitIdentity(args.before, "before");
|
|
16697
|
+
const after = assertCommitIdentity(args.after, "after");
|
|
16698
|
+
assertReachableCommit(cwd, before, "before");
|
|
16699
|
+
assertReachableCommit(cwd, after, "after");
|
|
16700
|
+
const checkedOutHead = runCapture("git", ["rev-parse", "HEAD"], cwd).stdout.trim().toLowerCase();
|
|
16701
|
+
if (checkedOutHead !== after) throw new Error(`after commit ${after} is not the checked-out HEAD ${checkedOutHead}; refusing to mix commit identities with declarations from another tree`);
|
|
16702
|
+
try {
|
|
16703
|
+
runCapture("git", [
|
|
16704
|
+
"merge-base",
|
|
16705
|
+
"--is-ancestor",
|
|
16706
|
+
before,
|
|
16707
|
+
after
|
|
16708
|
+
], cwd);
|
|
16709
|
+
} catch {
|
|
16710
|
+
throw new Error(`before commit ${before} is not an ancestor of after commit ${after}`);
|
|
16711
|
+
}
|
|
16712
|
+
const changedFiles = [...new Set(filesFromNameStatus(runCapture("git", [
|
|
16713
|
+
"diff",
|
|
16714
|
+
"--find-renames",
|
|
16715
|
+
"--name-status",
|
|
16716
|
+
before,
|
|
16717
|
+
after
|
|
16718
|
+
], cwd).stdout))].toSorted();
|
|
16719
|
+
const stamp = computeImpactStamp({
|
|
16720
|
+
changedFiles,
|
|
16721
|
+
profile,
|
|
16722
|
+
profilePath: path.relative(cwd, profilePath),
|
|
16723
|
+
readLockfile: (side) => showFileAtRef(cwd, side === "base" ? before : after, path.join(cwd, LOCKFILE_PATH))
|
|
16724
|
+
});
|
|
16725
|
+
const usable = stamp.basis === "target-scoped";
|
|
16726
|
+
decision = {
|
|
16727
|
+
afterSha: after,
|
|
16728
|
+
basis: {
|
|
16729
|
+
changedFiles,
|
|
16730
|
+
classification: stamp.basis,
|
|
16731
|
+
reasons: stamp.reasons
|
|
16732
|
+
},
|
|
16733
|
+
beforeSha: before,
|
|
16734
|
+
decision: usable ? "usable" : "refused",
|
|
16735
|
+
schemaVersion: 1,
|
|
16736
|
+
targets: stamp.targets.map((target) => ({
|
|
16737
|
+
basis: target.basis,
|
|
16738
|
+
name: target.name,
|
|
16739
|
+
state: usable && target.impact === "not-affected" ? "withdrawn" : "demanded"
|
|
16740
|
+
})),
|
|
16741
|
+
unsubscribedPaths: stamp.unsubscribedPaths
|
|
16742
|
+
};
|
|
16743
|
+
} catch (error) {
|
|
16744
|
+
decision = refusal({
|
|
16745
|
+
after: args.after,
|
|
16746
|
+
before: args.before,
|
|
16747
|
+
profile,
|
|
16748
|
+
reason: `production impact classification refused: ${error instanceof Error ? error.message : String(error)}`
|
|
16749
|
+
});
|
|
16750
|
+
}
|
|
16751
|
+
if (args.githubOutput) writeGithubOutputs(path.resolve(cwd, args.githubOutput), decision);
|
|
16752
|
+
if (args.githubSummary) appendFileSync(path.resolve(cwd, args.githubSummary), `### Production impact\n\n${productionImpactSummaryLines(decision).join("\n")}\n`, "utf-8");
|
|
16753
|
+
return decision;
|
|
16754
|
+
};
|
|
16755
|
+
const productionImpactSummaryLines = (decision) => decision.basis.classification === "refused" ? [
|
|
16756
|
+
"Production impact: refused; all consumer work remains demanded.",
|
|
16757
|
+
...decision.basis.reasons,
|
|
16758
|
+
"Unsubscribed changed paths: unavailable because classification was refused."
|
|
16759
|
+
] : [`Production impact: ${decision.basis.classification}; ${decision.targets.filter(({ state }) => state === "demanded").length}/${decision.targets.length} target(s) demanded.`, decision.unsubscribedPaths.length === 0 ? "Unsubscribed changed paths: none." : `Unsubscribed changed paths (demand no target): ${decision.unsubscribedPaths.join(", ")}`];
|
|
16760
|
+
//#endregion
|
|
16761
|
+
//#region src/commands/production-impact.ts
|
|
16762
|
+
function createProductionImpactCommand(output) {
|
|
16763
|
+
return markCwdOptionDefault(new Command("production:impact").description("Classify production targets from an exact merge-push before/after diff").option("--before <sha>", "merge-push before commit SHA").option("--after <sha>", "merge-push after commit SHA").option("--cwd <path>", "repository working directory", collectCwdOption).option("--profile <path>", "path to the project profile JSON file").option("--github-output <path>", "append stable decision outputs to a GitHub Actions output file").option("--github-summary <path>", "append a human-readable decision to a GitHub Actions summary file").option("--json", "print the complete decision as JSON").action((options) => {
|
|
16764
|
+
const decision = runProductionImpact({
|
|
16765
|
+
after: options.after,
|
|
16766
|
+
before: options.before,
|
|
16767
|
+
cwd: resolveCwdOption(options.cwd),
|
|
16768
|
+
githubOutput: options.githubOutput,
|
|
16769
|
+
githubSummary: options.githubSummary,
|
|
16770
|
+
profilePath: options.profile
|
|
16771
|
+
});
|
|
16772
|
+
if (options.json) output.stdout.write(`${JSON.stringify(decision, null, 2)}\n`);
|
|
16773
|
+
else output.stdout.write(`${productionImpactSummaryLines(decision).join("\n")}\n`);
|
|
16774
|
+
}));
|
|
16775
|
+
}
|
|
16549
16776
|
//#endregion
|
|
16550
16777
|
//#region src/worktree-scratch-files.ts
|
|
16551
16778
|
var worktree_scratch_files_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -16837,6 +17064,7 @@ function createProgram(options = {}) {
|
|
|
16837
17064
|
program.addCommand(createHqFlushCommand(output));
|
|
16838
17065
|
program.addCommand(createBoundaryCheckCommand(output, options.actions?.boundaryCheck));
|
|
16839
17066
|
program.addCommand(createPrVerifyCommand(output));
|
|
17067
|
+
program.addCommand(createProductionImpactCommand(output));
|
|
16840
17068
|
program.addCommand(createCloseoutCommand(output));
|
|
16841
17069
|
program.addCommand(createEpicPublishStructureCommand(output));
|
|
16842
17070
|
program.addCommand(createPrReviewCommand(output, options.actions?.prReview));
|
package/dist/schemas.d.ts
CHANGED
|
@@ -497,7 +497,7 @@ declare const impactStampSchema: z.ZodObject<{
|
|
|
497
497
|
conservative: "conservative";
|
|
498
498
|
}>;
|
|
499
499
|
reasons: z.ZodArray<z.ZodString>;
|
|
500
|
-
stampVersion: z.ZodLiteral<
|
|
500
|
+
stampVersion: z.ZodLiteral<3>;
|
|
501
501
|
targets: z.ZodArray<z.ZodObject<{
|
|
502
502
|
basis: z.ZodString;
|
|
503
503
|
impact: z.ZodEnum<{
|
|
@@ -506,6 +506,7 @@ declare const impactStampSchema: z.ZodObject<{
|
|
|
506
506
|
}>;
|
|
507
507
|
name: z.ZodString;
|
|
508
508
|
}, z.core.$strip>>;
|
|
509
|
+
unsubscribedPaths: z.ZodArray<z.ZodString>;
|
|
509
510
|
}, z.core.$strip>;
|
|
510
511
|
type ImpactStamp = z.infer<typeof impactStampSchema>;
|
|
511
512
|
//#endregion
|
|
@@ -1366,7 +1367,7 @@ declare const prVerifyProofSchema: z.ZodObject<{
|
|
|
1366
1367
|
conservative: "conservative";
|
|
1367
1368
|
}>;
|
|
1368
1369
|
reasons: z.ZodArray<z.ZodString>;
|
|
1369
|
-
stampVersion: z.ZodLiteral<
|
|
1370
|
+
stampVersion: z.ZodLiteral<3>;
|
|
1370
1371
|
targets: z.ZodArray<z.ZodObject<{
|
|
1371
1372
|
basis: z.ZodString;
|
|
1372
1373
|
impact: z.ZodEnum<{
|
|
@@ -1375,6 +1376,7 @@ declare const prVerifyProofSchema: z.ZodObject<{
|
|
|
1375
1376
|
}>;
|
|
1376
1377
|
name: z.ZodString;
|
|
1377
1378
|
}, z.core.$strip>>;
|
|
1379
|
+
unsubscribedPaths: z.ZodArray<z.ZodString>;
|
|
1378
1380
|
}, z.core.$strip>>;
|
|
1379
1381
|
mergeBaseSha: z.ZodOptional<z.ZodString>;
|
|
1380
1382
|
mode: z.ZodEnum<{
|
package/dist/schemas.js
CHANGED
|
@@ -242,14 +242,15 @@ const impactStampTargetSchema = z.object({
|
|
|
242
242
|
const impactStampSchema = z.object({
|
|
243
243
|
basis: z.enum(["target-scoped", "conservative"]),
|
|
244
244
|
reasons: z.array(z.string()),
|
|
245
|
-
stampVersion: z.literal(
|
|
245
|
+
stampVersion: z.literal(3),
|
|
246
246
|
targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
|
|
247
247
|
const names = targets.map((target) => target.name);
|
|
248
248
|
if (new Set(names).size !== names.length) context.addIssue({
|
|
249
249
|
code: "custom",
|
|
250
250
|
message: "impact stamp targets must have distinct names."
|
|
251
251
|
});
|
|
252
|
-
})
|
|
252
|
+
}),
|
|
253
|
+
unsubscribedPaths: z.array(z.string())
|
|
253
254
|
});
|
|
254
255
|
/**
|
|
255
256
|
* The surfaces that consume the stamp. One classification, consumed
|
|
@@ -287,8 +288,8 @@ const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
|
|
|
287
288
|
reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
|
|
288
289
|
scoped: false
|
|
289
290
|
};
|
|
290
|
-
if (stamp.stampVersion !==
|
|
291
|
-
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version
|
|
291
|
+
if (stamp.stampVersion !== 3) return {
|
|
292
|
+
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 3; ${floor}`,
|
|
292
293
|
scoped: false
|
|
293
294
|
};
|
|
294
295
|
if (stamp.basis !== "target-scoped") return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/software-factory",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.9",
|
|
4
4
|
"description": "Shared Patronage software factory CLI and project-profile validation tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"picomatch": "^4.0.5",
|
|
38
38
|
"yaml": "^2.9.0",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@patronage/factory-ci": "1.0.0-alpha.
|
|
40
|
+
"@patronage/factory-ci": "1.0.0-alpha.9"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "24.13.3",
|