@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 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. An empty required set would make *every* passing proof
347
- * trivially covering, so it is never silently treated as "requires nothing";
348
- * callers must refuse instead.
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
- * Skipped work covered under a different command string, keyed by the
408
- * skipped string with the reason as its value. Deliberately explicit: a
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. An empty required set would make *every* passing proof
401
- * trivially covering, so it is never silently treated as "requires nothing";
402
- * callers must refuse instead.
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
- return [...new Set(commands.map(({ name }) => name))].toSorted();
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, equivalents = {}, skipped }) => {
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 && !(proven.has(command) || Object.hasOwn(equivalents, command))))].toSorted();
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.0",
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",
@@ -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. An empty required set would make *every* passing proof
197
- * trivially covering, so it is never silently treated as "requires nothing";
198
- * callers must refuse instead.
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
- return [...new Set(commands.map(({ name }) => name))].toSorted();
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
- * Skipped work covered under a different command string, keyed by the
583
- * skipped string with the reason as its value. Deliberately explicit: a
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