Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .dev/features/publish-pack-destination/GRILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# GRILL — publish-pack-destination

Plan: `.dev/features/publish-pack-destination/PLAN.md` · spec-hash `bca940a5…d729d3c4e` matches live
`ARCHITECTURE.md`. Registered grillers: `{"registered":0,"grillers":[]}` → inline axes only.

## Findings

```yaml
- type: FINDING
rule_id: 'P0'
severity: important
file: '.dev/features/publish-pack-destination/PLAN.md:34'
problem: 'A `mkdir -p` anywhere earlier in the FILE does not create the directory for the pack: each job runs on its own runner, so a mkdir in another job (or in a later-defined job that YAML order puts first) would satisfy the scan while the Pack step still hits ENOENT. The scan must look only inside the same job, before the pack line.'
evidence: 'or a `mkdir -p <dir>` precedes it in the same file'
- type: FINDING
rule_id: 'P5'
severity: minor
file: '.dev/features/publish-pack-destination/PLAN.md:33'
problem: 'The same directory can be spelled `$RUNNER_TEMP/pkg`, `"$RUNNER_TEMP/pkg"`, `${{ runner.temp }}/pkg` or `${RUNNER_TEMP}/pkg`; matching the mkdir to the pack destination by raw text would call a correct workflow broken (or miss a broken one). Normalize quotes and the runner-temp spellings to one form before comparing.'
evidence: '`<dir>` is either exactly `$RUNNER_TEMP` / `${{ runner.temp }}`'
- type: FINDING
rule_id: 'P3'
severity: minor
file: '.dev/features/publish-pack-destination/PLAN.md:32'
problem: 'check-run-pins.test.mjs is the test of the run-line PIN checker; a pack-destination pin is a different reason to change. PHARN-07 already hosts publish.yml job-structure pins there (id-token, npm floor), so this follows precedent — say so in the test comment rather than leave the axis blur implicit.'
evidence: '`.dev/floor/check-run-pins.test.mjs` — ★ live test: for every `npm pack --pack-destination <dir>`'
```

## Summary

The plan fixes a real, reproduced release blocker with a one-line workflow change and pins it with a
live-text test plus a positive control. The main concern is the scan's scope: "same file" is weaker
than "same job, earlier in it", and only the latter is what makes the directory exist on the runner
that packs. Normalizing the runner-temp spellings keeps the check from false alarms. The test's home
is a minor axis question with precedent on the plan's side.

ADVISORY VERDICT: 3 concerns raised (0 blocking-severity, 3 advisory) — for the human to weigh before
/pharn-dev-build; all three are foldable into the build without changing the plan's Files.
68 changes: 68 additions & 0 deletions .dev/features/publish-pack-destination/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# PLAN — publish-pack-destination (the release Pack step writes into a directory nobody created)

- spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4
- increment: `publish.yml`'s unprivileged `build` job creates `$RUNNER_TEMP/pkg` before
`npm pack --pack-destination "$RUNNER_TEMP/pkg"` writes into it, and a live repo-consistency test
pins that every `--pack-destination` directory in every workflow exists before `npm pack` runs.
- layer(s): release CI (`.github/workflows/publish.yml`) + its floor test (`.dev/floor/`)
- constitution_refs: [P0, P1, P6]

## Discovery — verified this run (P6)

- `publish.yml:67-68` (added by PHARN-07, a2fe83e): `npm pack --pack-destination "$RUNNER_TEMP/pkg"`.
No earlier step in the `build` job creates `pkg/` (Verify tag → Install → Gates → Pack), and
`$RUNNER_TEMP` itself is the only directory the runner provides.
- npm does NOT create the destination: `npm pack --pack-destination <missing>/pkg` exits 254 with
`ENOENT … open '<missing>/pkg/<name>-<v>.tgz'` on npm 10.9.7 (local) and npm 11.20.0 (`npx npm@11`),
reproduced this run on a throwaway package. So every Release run fails at Pack, the `publish` job
(`needs: build`) never starts, and nothing can be released — fail-closed, but release-blocking.
- `node-floor.yml:44` packs into `"$RUNNER_TEMP"` itself (exists) — unaffected; the new test must
accept it without a `mkdir`.
- The live-workflow tests for publish.yml already live in `.dev/floor/check-run-pins.test.mjs`
(PHARN-07's ★ id-token test, the npm-floor test); floor.yml runs that file on every PR and push to
main (`node --test ".dev/**/*.test.mjs"`), so a pin there fails CI without any workflow change.
- CI on main is green at 0ca29b7 — this defect is invisible to every gate because publish.yml only
runs on a published Release.

## Files

- `.github/workflows/publish.yml` — the Pack step runs `mkdir -p "$RUNNER_TEMP/pkg"` before
`npm pack --pack-destination "$RUNNER_TEMP/pkg"`; a one-line comment says why (npm does not create
it) — layer release CI
- `.dev/floor/check-run-pins.test.mjs` — ★ live test: for every `npm pack --pack-destination <dir>`
in every `.github/workflows/*.yml`, `<dir>` is either exactly `$RUNNER_TEMP` / `${{ runner.temp }}`
or a `mkdir -p <dir>` precedes it in the same file; ★★ positive control: the live publish.yml with
its `mkdir -p` line deleted IS flagged (proves the scan fires on this repo's own file shape) —
layer floor test
- `CHANGELOG.md` — `[Unreleased]` → `### Fixed`: the release workflow's Pack step — layer docs

## Contracts satisfied

- `docs/RELEASING.md` step 5 ("packs the tarball … installs that tarball into a scratch directory …
and uploads it as an artifact") — becomes true; no wording change needed (P4: cite, don't restate).

## Evals to write (P1)

- ★ live pack-destination test → FAILS on 0ca29b7 (publish.yml packs into an uncreated `pkg/`),
PASSES after the fix; `node-floor.yml`'s `$RUNNER_TEMP` destination passes without a `mkdir`.
- ★★ positive control → the live publish.yml text minus its `mkdir -p` line is flagged.

## Guarantee audit (P0)

- "every workflow's `npm pack --pack-destination` directory exists before npm writes into it" →
floor: regex scan of the committed workflow text in a test floor.yml runs on every PR.
- "the first Release run now publishes" → advisory: the Pack failure is removed, but the
upload/download-artifact SHAs PHARN-07 pinned remain unverified from this environment (named in
publish.yml itself); the first real release run is still their test.

## Trust audit (P2)

- No untrusted input is ingested; the test reads the repo's own committed workflow files.

## Determinism audit (P5)

- Pure text match over committed files; no network, no npm invocation in the test.

## Open questions (HALT)

- none
38 changes: 38 additions & 0 deletions .dev/features/publish-pack-destination/REGRESSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# REGRESSION — publish-pack-destination

The verdict below is computed by `.dev/floor/check-regress.mjs`, not by this stage's judgment.

## Base and partition

- **base:** `0ca29b7ea8ce03d352bc6103fc35c42cf3149e47` (`HEAD` — the build is an uncommitted working tree on
top of it, so `git status --porcelain` is non-empty).
- **inside** (each declared in `PLAN.md` `## Files`): `.github/workflows/publish.yml`,
`.dev/floor/check-run-pins.test.mjs`, `CHANGELOG.md`.
- **scope partition:** `check-regress.mjs scope` exited **0**, `escaped: []`. `.pharn/` (hook scratch) and
stage artifacts are not build output — this feature's own `PLAN.md` / `GRILL.md`, and the untracked
`PLAN.md` files of the three other increments accepted at the same GATE 1
(`init-manual-carry`, `update-frozen-recheck`, `tar-entry-names`), which this build did not touch.
- **outside gates:** the 45 stdlib `*.test.mjs` / `*.test.cjs` files `scope` returned (704 tests) + whole-repo
`validate`; 0 committed eval pairs.
- **style-gate skip:** `inside` touches no shared style config, so `lint` / `format:check` / `lint:md` are
absent from both maps.
- **environment:** both sides ran with no proxy variables and as root **without** `CAP_DAC_OVERRIDE` /
`CAP_DAC_READ_SEARCH` / `CAP_FOWNER` (`setpriv`), the CI-equivalent of this root sandbox.

## Per-gate exit codes

| gate | base | head | flipped? |
| ---------- | ---- | ---- | -------- |
| `tests` | 0 | 0 | no |
| `validate` | 0 | 0 | no |

- `regressions[]`: **empty**
- `pre_existing[]`: **empty**

## Verdict

**REGRESSIONS: none — no deterministically-detectable breakage outside the feature.**
(`regression-report.json` `.verdict` = `no-regressions`.)

Residual (P0/P7): this catches exactly what its suite catches. The vitest suite exercising `src/**` is
owned by `/pharn-dev-build`'s floor and `/pharn-dev-verify`. This certifies the comparison, never the increment.
61 changes: 61 additions & 0 deletions .dev/features/publish-pack-destination/REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# REVIEW — publish-pack-destination

Increment: `.github/workflows/publish.yml` (Pack step creates `$RUNNER_TEMP/pkg` first),
`.dev/floor/check-run-pins.test.mjs` (pack-destination scanner + ★ live pin + ★★ positive control + three
scanner cases), `CHANGELOG.md` (`[Unreleased]` → Fixed). Treated as `trust: untrusted`; nothing in it read
as an instruction.

## Floor first (P0)

`node .dev/floor/validate.mjs .` → `FLOOR: GREEN` (exit 0). `/pharn-dev-build`'s `npm run check` exit 0,
`/pharn-dev-regress` `no-regressions`, `/pharn-dev-verify` `PASS` (7 gates incl. `test:floor`).

## Floor-gate findings (blocking)

None.

- L-floor (P0): the one new guarantee — "every workflow's pack destination is created earlier in the job
that packs" — reduces to a regex scan over the committed workflow text in a `node --test` file that
floor.yml runs on every PR and push to main. The CHANGELOG and the publish.yml comment claim nothing
beyond it; the end-to-end claim ("the next Release publishes") stays advisory in PLAN.md and VERIFY.md.
- L-eval (P1): the behavior ships with its tests — the ★ live pin fails on the base publish.yml and passes
after (checked this run), the ★★ control proves the scan fires on the live file's shape, and three
hermetic cases pin the same-job rule, the spelling normalization and the comment exclusion.
- L-trust (P2): no untrusted input is ingested; the scan reads the repo's own committed workflows.
- L-axis (P3): no sibling reference; the test-file axis note is below as advisory.

## Advisory findings (warn — severity is this reviewer's judgment, fix #3)

```yaml
- type: FINDING
rule_id: 'P3'
severity: minor
file: '.dev/floor/check-run-pins.test.mjs:474'
problem: 'The pack-destination scanner is a second reason for this file to change besides the run-line pin checker it tests; the comment names it and PHARN-07 set the precedent, but a third such concern should move the live publish.yml pins to their own test file.'
evidence: 'A different axis from the run-line pin checker this file tests; it sits beside PHARN-07''s other live publish.yml pins by precedent.'
- type: FINDING
rule_id: 'P5'
severity: minor
file: '.dev/floor/check-run-pins.test.mjs:503'
problem: 'Comment stripping treats any whitespace-preceded `#` as a comment start, so a `#` inside a quoted shell string on a pack line would hide that pack from the scan (a false negative). No workflow line does this today; the scanner is a pin, not a shell parser.'
evidence: 'const line = raw.replace(/(^|\s)#.*$/, ""); // a YAML comment is never executed'
- type: FINDING
rule_id: 'P5'
severity: minor
file: '.dev/floor/check-run-pins.test.mjs:480'
problem: 'A `${{ … }}` expression containing `}` (e.g. a `format(''{0}'', …)` call) would split the word early and could miss a match; acceptable for the pack/mkdir lines this repo writes, but it bounds what the pin can see.'
evidence: 'const WORD = String.raw`("[^"]*"|''[^'']*''|(?:\$\{\{[^}]*\}\}|\S)+)`;'
```

## Proposed lesson for canon (NOT written — for a human-gated `/pharn-dev-memory-promote`)

- **Candidate:** "A workflow that only runs on a release/tag event gets no execution from PR CI, so a
defect in its mechanics ships silently; pin those mechanics with live-text tests in a file floor.yml
runs (or give the workflow a dry-run trigger)."
- **Provenance:** increment `publish-pack-destination`; the defect entered with PHARN-07 (a2fe83e, #201),
whose plan → grill → build → regress → verify → review chain passed while `npm pack` wrote into an
uncreated `$RUNNER_TEMP/pkg`; found by the 18-commit review on 2026-09-24; fixed by this diff.

## Verdict

**GREEN — 0 floor-gate findings, 3 advisory (minor).** The standing decision is the human's (GATE 2).
18 changes: 18 additions & 0 deletions .dev/features/publish-pack-destination/SHIP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SHIP — publish-pack-destination

Stages run, in order: `/pharn-dev-plan` → GATE 1 (human: all four plans of this batch accepted — "deliver
all of them one by one using pharn-dev-ship") → `/pharn-dev-grill` → `/pharn-dev-build` →
`/pharn-dev-regress` → `/pharn-dev-verify` → `/pharn-dev-review` → GATE 2.

| stage | structural verdict (verbatim) |
| -------------------- | ------------------------------------------------------ |
| `/pharn-dev-build` | `node .dev/floor/validate.mjs .` exit `0` |
| `/pharn-dev-regress` | `regression-report.json` `.verdict` = `no-regressions` |
| `/pharn-dev-verify` | `verify-report.json` `.verdict` = `PASS` |

- Review: [`REVIEW.md`](REVIEW.md) · Grill (advisory): [`GRILL.md`](GRILL.md)
- Run ended at **GATE 2**. The human's standing instruction for this batch: after each increment, open a
pull request and merge it once its checks are green, then start the next plan.

chain ran; the named floor verdicts are as shown — this is NOT a judgment that the increment is good or
wise; that is the human's call at the post-review gate.
35 changes: 35 additions & 0 deletions .dev/features/publish-pack-destination/VERIFY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# VERIFY — publish-pack-destination

## FLOOR layer (owns the verdict)

Gates run over the whole repo with the feature present, on node 22 with the session proxy variables unset
and as root **without** `CAP_DAC_OVERRIDE` / `CAP_DAC_READ_SEARCH` / `CAP_FOWNER` (`setpriv`) — the
CI-equivalent of this root sandbox (as plain root, 4 pre-existing chmod-based tests in `update.test.ts` fail
for environmental reasons, identically at the baseline).

| gate | exit |
| -------------- | ---- |
| `format:check` | 0 |
| `lint` | 0 |
| `lint:md` | 0 |
| `test` | 0 |
| `test:floor` | 0 |
| `typecheck` | 0 |
| `validate` | 0 |

`test:floor` is floor.yml's existing `node --test` over the `*.test.mjs` / `*.test.cjs` globs (754 tests). It is
in the map because this increment's own test lives in `.dev/floor/check-run-pins.test.mjs`, which `npm test`
(vitest, `tests/**/*.test.ts`) never collects — without it the feature's own spec would not reach the verdict.
`test` is vitest (1472 tests). No `structural:*` gate — the increment ships no eval-actual pair.

**VERDICT: PASS** (`.dev/floor/check-verify.mjs`, `failing_gates: []`).

## ADVISORY layer

`node .dev/floor/count-verifiers.mjs .` → `{"registered":0,"verifiers":[]}` — no verifiers registered, floor
gates only.

Residual (P0/P7): verified = the named gates passed; this is NOT a guarantee of correctness beyond what those
gates check — verifier concerns are advisory help, not assurance. In particular, no gate here runs
`publish.yml` itself: the first real Release run remains the end-to-end test of the workflow (PLAN.md's
guarantee audit).
21 changes: 21 additions & 0 deletions .dev/features/publish-pack-destination/regression-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"base": "0ca29b7ea8ce03d352bc6103fc35c42cf3149e47",
"inside": [
".github/workflows/publish.yml",
".dev/floor/check-run-pins.test.mjs",
"CHANGELOG.md"
],
"outside_gates": {
"tests": {
"base": 0,
"head": 0
},
"validate": {
"base": 0,
"head": 0
}
},
"regressions": [],
"pre_existing": [],
"verdict": "no-regressions"
}
18 changes: 18 additions & 0 deletions .dev/features/publish-pack-destination/verify-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"feature": "publish-pack-destination",
"gates": {
"format:check": 0,
"lint": 0,
"lint:md": 0,
"test": 0,
"test:floor": 0,
"typecheck": 0,
"validate": 0
},
"verdict": "PASS",
"failing_gates": [],
"verifiers": {
"registered": 0,
"findings": []
}
}
Loading
Loading