@patronage/factory-ci 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.js +10 -6
- package/package.json +1 -1
- package/src/proof-reuse-gate.ts +13 -18
package/README.md
CHANGED
|
@@ -89,11 +89,13 @@ A proof is reusable only when the complete Checks API result (`filter=all`, ever
|
|
|
89
89
|
|
|
90
90
|
The only thing a consumer chooses is **what its surface requires**, expressed as the plain profile command objects that surface selects — a repository with distinct core and docs jobs selects distinct sets and gets distinct required coverage. `proofReuseRequiredCommands()` is the single derivation both the gate and the assertion go through. Identities are baked into the emitted script, so they are held to a plain `[A-Za-z0-9_][\w.:@/-]*` allow-list and shell-quoted at the interpolation site; a selection carrying anything else is unusable and degrades to a gate that always refuses.
|
|
91
91
|
|
|
92
|
+
Command names are executable authorization identities, not display labels. They must be unique in the profile; `factory-ci` also refuses a selected set with duplicates so two command strings can never collapse behind one proof identity.
|
|
93
|
+
|
|
92
94
|
The gate is a **step, not a job**, marked `continue-on-error`, with no `set -e`. A gate job that errored would leave the guarded job `skipped`, which a summary job that only fails on `failure` / `cancelled` reports as green. As a step it fails open by construction: the step errors, the output is never written, the `!= 'true'` guard reads empty, and every command runs. It writes a machine-readable `reason` output — `proven | none | pending | failed | unreadable | incomplete | ambiguous | error` — so a reuse-collapses-to-never regression is visible instead of hidden behind fail-open.
|
|
93
95
|
|
|
94
96
|
**Emit the step's `shell` verbatim.** `factoryProofGateStep()` sets `shell: bash --noprofile --norc {0}` (`FACTORY_PROOF_GATE_SHELL`) and a generator that drops it breaks the gate. GitHub's default for `run:` is `bash -e {0}` — errexit arrives from the invocation, not the script, so omitting `set -e` does not achieve it. Under the default the gate dies at the first non-zero command before its single `GITHUB_OUTPUT` write: no verdict, no `reason`, reuse silently collapsed to never while the job looks like healthy full CI. `shell: bash` is not a substitute; it expands to `bash --noprofile --norc -eo pipefail {0}`. The script is written to survive errexit as well, and its tests execute every case under both shells — a harness that runs this script under friendlier flags than the runner does is worse than no harness.
|
|
95
97
|
|
|
96
|
-
`assertProofReuseCoverage({ commands, skipped, surface })` is the compile-time guard in front of the runtime `incomplete` refusal: hand it the same selection and the command strings the workflow would skip, and it fails the consumer's build when the two drift apart. Extracting the skipped strings stays with the consumer — this package never parses workflow source, because establishing trust that way is what killed an earlier attempt.
|
|
98
|
+
`assertProofReuseCoverage({ commands, skipped, surface })` is the compile-time guard in front of the runtime `incomplete` refusal: hand it the same selection and the command strings the workflow would skip, and it fails the consumer's build when the two drift apart. Coverage is exact executable coverage. The deprecated `equivalents` input remains only for patch-release source compatibility and is ignored; prose cannot authorize a skip. Extracting the skipped strings stays with the consumer — this package never parses workflow source, because establishing trust that way is what killed an earlier attempt.
|
|
97
99
|
|
|
98
100
|
### Alchemy entries
|
|
99
101
|
|
package/dist/index.d.ts
CHANGED
|
@@ -343,9 +343,11 @@ interface ProofReuseCommand {
|
|
|
343
343
|
*
|
|
344
344
|
* `undefined` means the selection is unusable — not an array, empty, or
|
|
345
345
|
* carrying an entry whose `command` is blank or whose `name` is not a plain
|
|
346
|
-
* command identity
|
|
347
|
-
*
|
|
348
|
-
*
|
|
346
|
+
* command identity, or carrying duplicate names. A name is the executable
|
|
347
|
+
* authorization identity recorded in proof, so two commands may never collapse
|
|
348
|
+
* behind one. An empty required set would make *every* passing proof trivially
|
|
349
|
+
* covering, so it is never silently treated as "requires nothing"; callers
|
|
350
|
+
* must refuse instead.
|
|
349
351
|
*/
|
|
350
352
|
declare const proofReuseRequiredCommands: (commands: readonly ProofReuseCommand[]) => readonly string[] | undefined;
|
|
351
353
|
interface FactoryProofGateOptions {
|
|
@@ -404,10 +406,8 @@ interface ProofReuseCoverageInput {
|
|
|
404
406
|
/** The same selection handed to the gate for this surface. */
|
|
405
407
|
readonly commands: readonly ProofReuseCommand[];
|
|
406
408
|
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* prose rationale for why some narrower command is "equivalent enough" is
|
|
410
|
-
* exactly what this assertion exists to force into the open.
|
|
409
|
+
* @deprecated Ignored. Consumer prose cannot authorize executable coverage;
|
|
410
|
+
* retained only so the 0.2.1 security patch remains source-compatible.
|
|
411
411
|
*/
|
|
412
412
|
readonly equivalents?: Readonly<Record<string, string>>;
|
|
413
413
|
/**
|
|
@@ -440,7 +440,6 @@ interface ProofReuseCoverageReport {
|
|
|
440
440
|
*/
|
|
441
441
|
declare const proofReuseCoverage: ({
|
|
442
442
|
commands,
|
|
443
|
-
equivalents,
|
|
444
443
|
skipped
|
|
445
444
|
}: ProofReuseCoverageInput) => ProofReuseCoverageReport;
|
|
446
445
|
/** `proofReuseCoverage`, as a build failure. */
|
package/dist/index.js
CHANGED
|
@@ -397,14 +397,18 @@ const isProofReuseCommand = (value) => {
|
|
|
397
397
|
*
|
|
398
398
|
* `undefined` means the selection is unusable — not an array, empty, or
|
|
399
399
|
* carrying an entry whose `command` is blank or whose `name` is not a plain
|
|
400
|
-
* command identity
|
|
401
|
-
*
|
|
402
|
-
*
|
|
400
|
+
* command identity, or carrying duplicate names. A name is the executable
|
|
401
|
+
* authorization identity recorded in proof, so two commands may never collapse
|
|
402
|
+
* behind one. An empty required set would make *every* passing proof trivially
|
|
403
|
+
* covering, so it is never silently treated as "requires nothing"; callers
|
|
404
|
+
* must refuse instead.
|
|
403
405
|
*/
|
|
404
406
|
const proofReuseRequiredCommands = (commands) => {
|
|
405
407
|
if (!(Array.isArray(commands) && commands.length > 0)) return;
|
|
406
408
|
if (!commands.every(isProofReuseCommand)) return;
|
|
407
|
-
|
|
409
|
+
const names = commands.map(({ name }) => name);
|
|
410
|
+
if (new Set(names).size !== names.length) return;
|
|
411
|
+
return names.toSorted();
|
|
408
412
|
};
|
|
409
413
|
/**
|
|
410
414
|
* jq program: every page of the Checks API result in, three sanitized lines
|
|
@@ -736,10 +740,10 @@ const factoryProofGateStep = (options) => Object.freeze({
|
|
|
736
740
|
* as CI quietly verifying nothing. The drift is real history: #227 wired a
|
|
737
741
|
* workspace member into CI while the profile never learned about it.
|
|
738
742
|
*/
|
|
739
|
-
const proofReuseCoverage = ({ commands,
|
|
743
|
+
const proofReuseCoverage = ({ commands, skipped }) => {
|
|
740
744
|
const requiredCommands = proofReuseRequiredCommands(commands) ?? [];
|
|
741
745
|
const proven = new Set(requiredCommands.length > 0 ? commands.map(({ command }) => command.trim()) : []);
|
|
742
|
-
const uncovered = [...new Set((Array.isArray(skipped) ? skipped : []).map((command) => String(command).trim()).filter((command) => command.length > 0 && !
|
|
746
|
+
const uncovered = [...new Set((Array.isArray(skipped) ? skipped : []).map((command) => String(command).trim()).filter((command) => command.length > 0 && !proven.has(command)))].toSorted();
|
|
743
747
|
return Object.freeze({
|
|
744
748
|
covered: requiredCommands.length > 0 && uncovered.length === 0,
|
|
745
749
|
requiredCommands,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/factory-ci",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Deep CI and deploy building blocks for Patronage factory projects: workflow source artifacts, hosted diff classification, Alchemy entry execution, and disposable-stage semantics",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
package/src/proof-reuse-gate.ts
CHANGED
|
@@ -193,9 +193,11 @@ const isProofReuseCommand = (value: unknown): value is ProofReuseCommand => {
|
|
|
193
193
|
*
|
|
194
194
|
* `undefined` means the selection is unusable — not an array, empty, or
|
|
195
195
|
* carrying an entry whose `command` is blank or whose `name` is not a plain
|
|
196
|
-
* command identity
|
|
197
|
-
*
|
|
198
|
-
*
|
|
196
|
+
* command identity, or carrying duplicate names. A name is the executable
|
|
197
|
+
* authorization identity recorded in proof, so two commands may never collapse
|
|
198
|
+
* behind one. An empty required set would make *every* passing proof trivially
|
|
199
|
+
* covering, so it is never silently treated as "requires nothing"; callers
|
|
200
|
+
* must refuse instead.
|
|
199
201
|
*/
|
|
200
202
|
export const proofReuseRequiredCommands = (
|
|
201
203
|
commands: readonly ProofReuseCommand[]
|
|
@@ -206,7 +208,11 @@ export const proofReuseRequiredCommands = (
|
|
|
206
208
|
if (!commands.every(isProofReuseCommand)) {
|
|
207
209
|
return;
|
|
208
210
|
}
|
|
209
|
-
|
|
211
|
+
const names = commands.map(({ name }) => name);
|
|
212
|
+
if (new Set(names).size !== names.length) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
return names.toSorted();
|
|
210
216
|
};
|
|
211
217
|
|
|
212
218
|
/**
|
|
@@ -579,10 +585,8 @@ export interface ProofReuseCoverageInput {
|
|
|
579
585
|
/** The same selection handed to the gate for this surface. */
|
|
580
586
|
readonly commands: readonly ProofReuseCommand[];
|
|
581
587
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
* prose rationale for why some narrower command is "equivalent enough" is
|
|
585
|
-
* exactly what this assertion exists to force into the open.
|
|
588
|
+
* @deprecated Ignored. Consumer prose cannot authorize executable coverage;
|
|
589
|
+
* retained only so the 0.2.1 security patch remains source-compatible.
|
|
586
590
|
*/
|
|
587
591
|
readonly equivalents?: Readonly<Record<string, string>>;
|
|
588
592
|
/**
|
|
@@ -617,7 +621,6 @@ export interface ProofReuseCoverageReport {
|
|
|
617
621
|
*/
|
|
618
622
|
export const proofReuseCoverage = ({
|
|
619
623
|
commands,
|
|
620
|
-
equivalents = {},
|
|
621
624
|
skipped,
|
|
622
625
|
}: ProofReuseCoverageInput): ProofReuseCoverageReport => {
|
|
623
626
|
const requiredCommands = proofReuseRequiredCommands(commands) ?? [];
|
|
@@ -630,15 +633,7 @@ export const proofReuseCoverage = ({
|
|
|
630
633
|
...new Set(
|
|
631
634
|
(Array.isArray(skipped) ? skipped : [])
|
|
632
635
|
.map((command) => String(command).trim())
|
|
633
|
-
.filter(
|
|
634
|
-
(command) =>
|
|
635
|
-
command.length > 0 &&
|
|
636
|
-
// `Object.hasOwn`, never `in`: `in` walks the prototype chain, so
|
|
637
|
-
// a skipped command named `toString` or `constructor` would report
|
|
638
|
-
// itself as a declared equivalent of nothing. This assertion's only
|
|
639
|
-
// job is to fail loudly.
|
|
640
|
-
!(proven.has(command) || Object.hasOwn(equivalents, command))
|
|
641
|
-
)
|
|
636
|
+
.filter((command) => command.length > 0 && !proven.has(command))
|
|
642
637
|
),
|
|
643
638
|
].toSorted();
|
|
644
639
|
|