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
29 changes: 29 additions & 0 deletions .dev/features/engines-node-floor/GRILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# GRILL — engines-node-floor

Plan: `.dev/features/engines-node-floor/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/engines-node-floor/PLAN.md:47'
problem: 'The smoke job is not a required status check, so a later dependency bump that raises the real floor again would turn it red without blocking a merge; the unit test is the only floor-grade guard, and it only sees engines fields that dependencies declare.'
evidence: 'CI smoke job on exactly 20.12.0 (not a required check — advisory signal'
- type: FINDING
rule_id: 'P1'
severity: minor
file: '.dev/features/engines-node-floor/PLAN.md:30'
problem: 'The unit test should read the INSTALLED dependency manifests (node_modules), which is what npm resolves for a consumer at the pinned lockfile — say so, since a consumer resolving ^ ranges fresh may get a newer dependency with a higher floor.'
evidence: "every runtime dependency's installed `engines.node` lower bound"
- type: FINDING
rule_id: 'P7'
severity: minor
file: '.dev/features/engines-node-floor/PLAN.md:31'
problem: 'Pinning doc text in a unit test couples tests to prose; keep it to the badge and the engines string only.'
evidence: "and README's badge / docs state the same floor"
```

ADVISORY VERDICT: 3 concerns raised (0 blocking-severity, 3 advisory) — for the human to weigh before /pharn-dev-build.
67 changes: 67 additions & 0 deletions .dev/features/engines-node-floor/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# PLAN — engines-node-floor (PHARN-06: the declared Node floor must be one the CLI actually starts on)

- spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4
- increment: raise `engines.node` from `>=20` to `>=20.12.0` (the floor `@clack/prompts` / `@clack/core`
declare — they import `styleText` from `node:util`), pin that the CLI's floor is never below any
runtime dependency's declared floor with a unit test, add a separately-named CI job that runs the
PACKED CLI on exactly Node 20.12.0, and correct every doc that repeats `>=20`.
- layer(s): package metadata, tests, CI tooling, docs
- constitution_refs: [P0, P1, P4, P7]

## Discovery — verified this run (P6)

- Review evidence: on Node 20.0.0 and 20.11.1, `pharn --version` dies with `SyntaxError: The requested
module 'node:util' does not provide an export named 'styleText'`; 20.12.0 works.
- `node_modules/@clack/prompts/package.json` and `@clack/core` declare `"engines"`; `package.json:29-31`
and the root `package-lock.json` entry say `>=20`; `scripts/build.mjs` keeps `@clack/prompts`,
`minimist`, `picocolors` EXTERNAL, so a smoke test must install the packed tarball, not run the bundle alone.
- Claims repeating `>=20`: README badge + "Current scope", `docs/getting-started.md:11`,
`docs/troubleshooting.md` (prerequisites), `docs/contributing.md:56`, `SECURITY.md:7`, `CLAUDE.md`.
- `ci.yml` has a six-job contract pinned by `tests/ci-workflow.test.ts`; a new, separately-named workflow
leaves those contexts untouched (the shape `docs/contributing.md` prescribes).

## Files

- `package.json` — `engines.node: ">=20.12.0"` — layer metadata
- `package-lock.json` — root `engines` regenerated with `npm install --package-lock-only` — layer metadata
- `tests/engines.test.ts` — the declared lower bound is ≥ every runtime dependency's installed
`engines.node` lower bound (parsed `>=x.y.z` comparator), and README's badge / docs state the same floor
- `.github/workflows/node-floor.yml` — job `Smoke (node 20.12.0)`: build on Node 24, `npm pack`, install
the tarball into a temp dir on Node 20.12.0, run `pharn --version` and `pharn --help` — layer CI
- `.dev/floor/check-run-pins.test.mjs` — the live-repo count of skipped (lockfile / local-path) installs
goes 7 → 9 for the new workflow's `npm ci` + local-tarball install, with the reason recorded
- `.github/workflows/ci.yml` — header comment only (it quotes `>=20`); jobs untouched (P4)
- `README.md` — badge + "Current scope" (P4)
- `docs/getting-started.md` — Node row (P4)
- `docs/troubleshooting.md` — prerequisites (P4)
- `docs/contributing.md` — the untested-claims table: floor now exercised by the smoke job (P4)
- `SECURITY.md` — `engines.node >= 20.12.0` (P4)
- `CLAUDE.md` — CI paragraph (P4)

## Contracts satisfied

- `docs/contributing.md` "a separate, additionally-named job is the only safe shape" — followed; the six
required contexts are unchanged.

## Evals to write (P1)

- `tests/engines.test.ts` fails on the base (`>=20` < `>=20.12.0` declared by @clack) and passes after.

## Guarantee audit (P0)

- "the declared floor is not below any runtime dependency's declared floor" → floor: version comparison
over installed package metadata (unit test).
- "the CLI starts on the declared floor" → CI smoke job on exactly 20.12.0 (not a required check —
advisory signal unless a maintainer adds it to the ruleset; named in `docs/contributing.md`).

## Trust audit (P2)

- No runtime input change.

## Determinism audit (P5)

- Numeric semver compare of `major.minor.patch`.

## Open questions (HALT)

- none
30 changes: 30 additions & 0 deletions .dev/features/engines-node-floor/REGRESSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# REGRESSION — engines-node-floor

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

## Base and partition

- **base:** `28397171eba488bf6a31961808f9dbecba18da48` (`origin/main` at build time; the build is an uncommitted working tree on top of it).
- **inside** (each declared in `PLAN.md` `## Files`): `.dev/floor/check-run-pins.test.mjs`, `.github/workflows/ci.yml`, `.github/workflows/node-floor.yml`, `CLAUDE.md`, `README.md`, `SECURITY.md`, `docs/contributing.md`, `docs/getting-started.md`, `docs/troubleshooting.md`, `package-lock.json`, `package.json`, `tests/engines.test.ts`.
- **scope partition:** `check-regress.mjs scope` exited **0**, `escaped: []`. `.pharn/` (hook scratch) and this
feature's own stage artifacts are not build output.
- **outside gates:** the stdlib `*.test.mjs` / `*.test.cjs` files + 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.

## 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.
55 changes: 55 additions & 0 deletions .dev/features/engines-node-floor/REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# REVIEW — engines-node-floor

Floor first: `node .dev/floor/validate.mjs .` → exit 0 (GREEN). Everything below is **advisory**.

## Floor-gate findings (blocking)

None at the final state. The FIRST `/pharn-dev-regress` run returned `regressions` (`tests` 0 → 1) and
`/pharn-dev-verify` returned `FAIL` (`lint:md`), and both were acted on rather than waved through:

- `check-run-pins.mjs` flagged `npx --no-install pharn` as an unpinned package exec → the workflow now
runs `./node_modules/.bin/pharn` directly (no package resolution at all).
- `check-run-pins.test.mjs` pins the exact count of lockfile/path installs in workflows (7); the new
workflow's `npm ci` + local-tarball install are deliberate, so the count is now 9 with the reason
recorded, and the file was added to the plan's `## Files`.
- Two doc tables were realigned for MD060.

## Lenses

- **L-floor (P0):** "declared floor ≥ every runtime dependency's declared floor" reduces to a numeric
version compare in `tests/engines.test.ts` (fails on the base: `@clack/prompts needs node >= 20.12.0,
package.json says >=20`). "The CLI starts on 20.12.0" rests on a non-required CI job — advisory,
stated in `docs/contributing.md`.
- **L-eval (P1):** 3 cases in `tests/engines.test.ts`; manual smoke of the packed CLI on local Node
20.20.2 printed `0.5.0` (no 20.12.0 binary offline — the CI job covers the exact floor).
- **L-trust (P2):** no runtime input changes; the new workflow has `contents: read` only and no secrets.
- **L-axis (P3):** one new workflow with one job; `ci.yml`'s six contexts untouched (comment only).

## Advisory findings

```yaml
- type: FINDING
rule_id: 'P0'
severity: important
file: '.github/workflows/node-floor.yml'
problem: "`Smoke (node 20.12.0)` is not in the ruleset's required checks, so a red smoke does not block a merge unless a maintainer adds it; the unit test is the only blocking guard."
evidence: 'name: Smoke (node 20.12.0)'
- type: FINDING
rule_id: 'P7'
severity: minor
file: 'package-lock.json:35'
problem: "The lockfile root `engines` line was edited to match package.json because the only local npm (10) rewrites unrelated `libc` fields; CI's npm 11 would produce the same single-line change."
evidence: '"node": ">=20.12.0"'
- type: FINDING
rule_id: 'P4'
severity: minor
file: 'CHANGELOG.md:8'
problem: 'No CHANGELOG `[Unreleased]` entry for the raised engines floor.'
evidence: '## [Unreleased]'
```

## Verdict

**GREEN** — 0 floor-gate findings at the final state, 3 advisory findings. Proposed lesson (not
promoted — `/pharn-dev-memory-promote` is human-gated): a new workflow must be run through
`check-run-pins.mjs` and its live-count test, which are outside the vitest suite.
17 changes: 17 additions & 0 deletions .dev/features/engines-node-floor/SHIP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SHIP — engines-node-floor

Stages run, in order: `/pharn-dev-plan` → GATE 1 (human: **Approve as written**) → `/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: open a PR and merge it only if
its CI checks are green.

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.
27 changes: 27 additions & 0 deletions .dev/features/engines-node-floor/VERIFY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# VERIFY — engines-node-floor

## FLOOR layer (owns the verdict)

Gates run over the whole repo with the feature present, as a non-root user on node 22 with the session
proxy variables unset (as root with the proxy set, 5 pre-existing tests in `init.test.ts` /
`update.test.ts` fail for environmental reasons, identically at the baseline).

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

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":[]}` — floor gates only.

Residual (P0/P7): "verified" means the named gates passed — not that the feature is correct in any sense
the suite does not encode.
30 changes: 30 additions & 0 deletions .dev/features/engines-node-floor/regression-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"base": "28397171eba488bf6a31961808f9dbecba18da48",
"inside": [
".dev/floor/check-run-pins.test.mjs",
".github/workflows/ci.yml",
".github/workflows/node-floor.yml",
"CLAUDE.md",
"README.md",
"SECURITY.md",
"docs/contributing.md",
"docs/getting-started.md",
"docs/troubleshooting.md",
"package-lock.json",
"package.json",
"tests/engines.test.ts"
],
"outside_gates": {
"tests": {
"base": 0,
"head": 0
},
"validate": {
"base": 0,
"head": 0
}
},
"regressions": [],
"pre_existing": [],
"verdict": "no-regressions"
}
17 changes: 17 additions & 0 deletions .dev/features/engines-node-floor/verify-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"feature": "engines-node-floor",
"gates": {
"format:check": 0,
"lint": 0,
"lint:md": 0,
"test": 0,
"typecheck": 0,
"validate": 0
},
"verdict": "PASS",
"failing_gates": [],
"verifiers": {
"registered": 0,
"findings": []
}
}
10 changes: 6 additions & 4 deletions .dev/floor/check-run-pins.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,12 @@ test("★ the live repo has NO floating install in any workflow run: line", () =
// `npm ci` in ci.yml and publish.yml — asserted EXACTLY, so an exemption can never become a
// silent hole. If this number changes, a lockfile install was added or removed on purpose.
//
// 7 = six in ci.yml (one per gate job — each required status check installs for itself) plus one
// in publish.yml. It was 2 while ci.yml ran a single `check` job; splitting that job into six is
// the deliberate change this count now records.
assert.equal(d.skipped, 7);
// 9 = six in ci.yml (one per gate job — each required status check installs for itself), one
// in publish.yml, and two in node-floor.yml (its `npm ci` build step, and the install of the
// locally packed tarball by PATH — `../pharn-dev-pharn-*.tgz` — onto the floor Node). It was 2
// while ci.yml ran a single `check` job, 7 until the node-floor smoke job was added; each step is
// a deliberate change this count records.
assert.equal(d.skipped, 9);

// Independent recount of the enumerated workflow files, case-insensitively — exit 0 is also what
// a checker returns when it opened nothing.
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ name: ci
#
# Coverage is deliberately one platform, one Node version (see
# `.dev/features/ci-matrix-required-checks/PLAN.md`). Note that `package.json`
# declares `engines.node: ">=20"` while these gates run node 24 — the lower end
# of that published range is NOT exercised here.
# declares `engines.node: ">=20.12.0"` while these gates run node 24 — the lower
# end of that published range is exercised only by the separate, non-required
# `Smoke (node 20.12.0)` job in `node-floor.yml`, never by these gates.

on:
pull_request:
Expand Down
59 changes: 59 additions & 0 deletions .github/workflows/node-floor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: node-floor

# Starts the PACKED CLI on exactly the Node version `engines.node` declares as
# its floor. `package.json` once said ">=20" while @clack/prompts — a runtime
# dependency — needs 20.12.0 (it imports `styleText` from node:util), so on
# 20.0–20.11 even `pharn --version` died at load time and nothing caught it:
# the six gates in ci.yml run node 24 only.
#
# A SEPARATE workflow on purpose. ci.yml's six job names are a contract with the
# `main` branch ruleset (tests/ci-workflow.test.ts); matrixing one of them would
# rename its reported context and block every PR. This job's context
# (`Smoke (node 20.12.0)`) is additional and NOT required unless a maintainer
# adds it to the ruleset. Keep FLOOR below equal to package.json's lower bound
# (tests/engines.test.ts pins that bound against the runtime dependencies).
#
# Build on node 24 (the toolchain's own floor is higher), then install the
# tarball — runtime dependencies are external to the bundle (scripts/build.mjs),
# so only an install exercises what a user actually runs.

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
smoke-node-floor:
name: Smoke (node 20.12.0)
runs-on: ubuntu-latest
env:
FLOOR: 20.12.0
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- run: npm ci
- run: npm run build
- name: Pack
run: npm pack --pack-destination "$RUNNER_TEMP"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.FLOOR }}
- name: Install the packed CLI on the floor Node
working-directory: ${{ runner.temp }}
run: |
node --version
mkdir smoke && cd smoke
npm init -y > /dev/null
npm install --no-audit --no-fund ../pharn-dev-pharn-*.tgz
- name: Start it
working-directory: ${{ runner.temp }}/smoke
run: |
./node_modules/.bin/pharn --version
./node_modules/.bin/pharn --help
4 changes: 2 additions & 2 deletions .pharn/writes-scope.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"scope": [
".dev/features/add-after-withheld-update/SHIP.md"
".dev/features/engines-node-floor/SHIP.md"
],
"set_by": ".claude/commands/pharn-dev-ship.md",
"set_at": "2026-09-24T08:22:49.900Z"
"set_at": "2026-09-24T08:31:05.706Z"
}
Loading
Loading