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

Plan: `.dev/features/engines-styletext-floor/PLAN.md`. Spec hash recomputed:
`bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e` — **matches** the plan's
`spec_content_hash`. Registered grillers: `{"registered":0,"grillers":[]}` → inline axes only.
The plan is `trust: untrusted`; nothing in it read as an instruction.

## Findings

### Guarantee audit (P0)

```yaml
- type: FINDING
rule_id: 'P0'
severity: minor
file: '.dev/features/engines-styletext-floor/PLAN.md:42'
problem: 'The scan matches the literal call name. Both clack packages import `styleText` by that name today (`import { styleText } from ''node:util''`, checked), but an aliased import (`import { styleText as s }`) would not match, and neither would whitespace between the name and `(`. Write the regex as `\bstyleText\s*\(\s*\[` and have the test comment state that aliases are outside its reach.'
evidence: 'A `KNOWN_API_FLOORS` table (`/styleText\(\s*\[/` → 20.13.0, with the reason).'
```

### Eval coverage (P1)

```yaml
- type: FINDING
rule_id: 'P1'
severity: minor
file: '.dev/features/engines-styletext-floor/PLAN.md:44'
problem: 'A hermetic case is missing for the scanner itself. Planting a dependency file that uses the array form, and one that does not, proves the scan can both fire and stay quiet. Otherwise a regex that never matches would pass once the live dependency stops using arrays.'
evidence: 'Ours must be ≥ every floor whose pattern matches.'
```

### Honest scope (P7)

```yaml
- type: FINDING
rule_id: 'P7'
severity: minor
file: '.dev/features/engines-styletext-floor/PLAN.md:59'
problem: 'Raising `engines` excludes Node 20.12.x, a range that was already broken on every prompt cancel. The CHANGELOG entry should say that, so it does not read as dropping a working Node. With default npm settings it produces an EBADENGINE warning, not an install failure.'
evidence: '`CHANGELOG.md` — `[Unreleased]` → `### Changed` (Node ≥ 20.13.0, and why)'
```

### Checked, no finding

- `package-lock.json` regeneration: the registry is reachable from this environment (`npm view`
works), so `npm install --package-lock-only` is feasible. Only the root `engines` mirror should
change.
- Renaming the smoke job: no test or floor pin reads `node-floor.yml`'s job name (only comments
in `check-run-pins.test.mjs` mention the file), and the job is not a required check.
- Trust (P2), axis (P3), determinism (P5): no concerns. The test reads dependency files as text.

## Summary

The plan is small and grounded in a measurement. The three concerns are sharpening only: make the
scan regex slightly wider and state its reach, prove the scanner with a planted fixture, and word
the CHANGELOG entry honestly.

**ADVISORY VERDICT: 3 concerns raised (0 blocking-severity, 3 minor) — for the human to weigh
before /pharn-dev-build.**
103 changes: 103 additions & 0 deletions .dev/features/engines-styletext-floor/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# PLAN — engines-styletext-floor (the Node floor is what the prompt library's code needs, not what it declares)

- spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4
- increment: raise `engines.node` from `>=20.12.0` to `>=20.13.0`. `@clack/prompts` 1.8.1 passes
an ARRAY of formats to `util.styleText`, and Node accepts that only from 20.13.0. Make
`tests/engines.test.ts` check the dependencies' CODE (a known-API table scanned over their shipped
files), not only their declared `engines`. Pin the smoke workflow's `FLOOR` to `package.json`.
Correct every doc that repeats 20.12.0.
- layer(s): package metadata, CI (`node-floor.yml`), tests, docs
- constitution_refs: [P0, P1, P6, P7]

## Discovery — verified this run (P6)

- Measured here with official binaries: `styleText(['strikethrough','dim'], 'x')` throws
`ERR_INVALID_ARG_VALUE` on Node **20.12.0** and returns the styled string on **20.13.0**.
- `node_modules/@clack/prompts/dist/index.mjs` (1.8.1) has 21 array-form `styleText([...])` calls (and `@clack/core` 1.5.1 has 2 more),
most of them in the `cancelled` render state (`['strikethrough','dim']`). So every prompt the CLI
shows crashes on 20.12.x when cancelled: init's select/confirm, update's confirm, and the
add/remove `groupMultiselect` pickers. The review reproduced this in a pty: exit 1 with a stack
trace instead of "Cancelled" and exit 0. The pickers use `required: false`, so the one array call
in the empty-submit validation message is not reached.
- `@clack/prompts` and `@clack/core` both declare `"node": ">= 20.12.0"`. That declaration is wrong,
and `tests/engines.test.ts` trusts it: it only compares declared ranges.
- The CLI's own `src/**` uses no Node API newer than 20.13. A grep for `Promise.withResolvers`,
`Object.groupBy`, Set methods, `fromAsync`, `getBuiltinModule`, `globSync`, `styleText`,
`import.meta.dirname` and similar found nothing. esbuild targets `node20` (`scripts/build.mjs:14`).
- `node-floor.yml` has `FLOOR: 20.12.0` and job name `Smoke (node 20.12.0)`. It is NOT a required
check (CLAUDE.md; ci.yml:17-21). Only a comment asks to keep `FLOOR` equal to `package.json`'s
bound, and no test pins that. No test pins the job name.
- 20.12.0 is also stated in: `package.json:30`, `README.md:20` (badge, pinned by
engines.test.ts) and `README.md:315-317`, `SECURITY.md:7`, `CLAUDE.md:28`, `ci.yml:19-21`
(comment), `node-floor.yml:3-14,30,33`, `docs/contributing.md:56`, `docs/getting-started.md:11`,
`docs/troubleshooting.md:107`, and `tests/engines.test.ts:7,13` (comments). The mentions under
`.dev/features/*` are history and stay as they are.

## Files

- `package.json` — `engines.node: ">=20.13.0"` — layer metadata
- `package-lock.json` — the root package's mirrored `engines` field, regenerated with
`npm install --package-lock-only` (never hand-edited) — layer metadata
- `tests/engines.test.ts` — layer tests. Three additions:
- A known-API table (the array form of `styleText` → 20.13.0, with the reason), scanned over
every runtime dependency's shipped `.js`/`.mjs`/`.cjs` files, found by the same transitive
walk. Ours must be ≥ every floor whose pattern matches. The scanner is also proven on planted
fixture files (grill finding 2).
- A non-vacuity check: the scan read at least one file from `@clack/prompts`.
- The smoke workflow's `FLOOR` equals `package.json`'s lower bound.
- `.github/workflows/node-floor.yml` — `FLOOR: 20.13.0`; job name per open question 1; comments
updated with the styleText-array reason — layer CI
- `.github/workflows/ci.yml` — header comment only (the floor version and the smoke job's name) —
layer CI
- `README.md` — badge and "Current scope" bullet — layer docs
- `SECURITY.md` — `engines.node >= 20.13.0` — layer docs
- `CLAUDE.md` — the CI paragraph (floor, smoke name, and that engines.test.ts also scans dependency
code) — layer docs
- `docs/contributing.md` — the engines row — layer docs
- `docs/getting-started.md` — the Node row — layer docs
- `docs/troubleshooting.md` — the prerequisites paragraph, plus the new symptom: on 20.12.x,
cancelling a prompt fails with `ERR_INVALID_ARG_VALUE` — layer docs
- `CHANGELOG.md` — `[Unreleased]` → `### Changed` (Node ≥ 20.13.0, and why) — layer docs

## Contracts satisfied

- PHARN-06's own contract ("the declared floor is never below what the runtime dependencies need").
It now holds for what they NEED, not only what they declare (cited, P4).

## Evals to write (P1)

- `engines.test.ts` "not below what runtime dependency code needs" → FAILS on the base (@clack/prompts
uses the array form, and `>=20.12.0` < 20.13.0). Passes after.
- `engines.test.ts` "smoke FLOOR equals package.json" → passes on the base (both are 20.12.0). It
guards the pair against drifting apart later.
- The existing README-badge test → FAILS if the badge is not updated.

## Guarantee audit (P0)

- "`engines` is ≥ every KNOWN code-level floor of the installed runtime dependencies" → floor: a
regex scan plus a version compare (a blocking vitest). Its reach is exactly the table, and the
plan says so. A newer API the table does not name is not caught. That residual stays with the
(advisory, non-required) smoke job.
- "the packed CLI starts on exactly the floor" → advisory. It is a non-required CI job.
- "the smoke job runs the floor `package.json` declares" → floor: the new equality test.

## Trust audit (P2)

- No untrusted input. The test reads installed dependency files as text and never executes them.

## Determinism audit (P5)

- Regex membership and integer compares only.

## Open questions (HALT)

None open. Resolved at GATE 1 (human, 2026-09-25): every question below → **(a)**, the
recommended answer. Kept for the record:

1. The smoke job's name. (a) `Smoke (node floor)`: stable across future bumps, with the version
only in `FLOOR` — recommended. (b) `Smoke (node 20.13.0)`: renamed again on every bump. The job
is not required today, so either rename is safe. Only (a) stays safe if a maintainer later
makes it required.
2. Should the smoke job also drive a real cancelled prompt? (a) No. The unit scan covers the failure
that actually happened, and driving a prompt in CI needs a pty, which is flaky — recommended.
(b) Yes: add a `script(1)`-driven Ctrl-C on a prompt.
40 changes: 40 additions & 0 deletions .dev/features/engines-styletext-floor/REGRESSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# REGRESSION — engines-styletext-floor

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

## Base and partition

- **base:** `abb274acbef59967d26068baa38676f1033ff182` (`HEAD` — `origin/main` after #218; the build is
an uncommitted working tree on top of it).
- **inside** (each declared in `PLAN.md` `## Files`): `package.json`, `package-lock.json`,
`tests/engines.test.ts`, `.github/workflows/node-floor.yml`, `.github/workflows/ci.yml`,
`README.md`, `SECURITY.md`, `CLAUDE.md`, `docs/contributing.md`, `docs/getting-started.md`,
`docs/troubleshooting.md`, `CHANGELOG.md`.
- **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 46 stdlib `*.test.mjs` / `*.test.cjs` files `scope` returned (754 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/**`
and `tests/**` 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/engines-styletext-floor/REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# REVIEW — engines-styletext-floor

Increment:

- `package.json` / `package-lock.json`: `engines.node` `>=20.12.0` → `>=20.13.0`. The lockfile was
regenerated with npm 11.20.0, and the only change is the root `engines` line.
- `tests/engines.test.ts`: a code-level floor scan (`KNOWN_API_FLOORS`) over the runtime
dependencies' shipped JS, a non-vacuity case, a smoke-`FLOOR` equality case, and three
planted-text scanner cases.
- `node-floor.yml`: `FLOOR: 20.13.0`, job `Smoke (node floor)`.
- The version is corrected across the docs, plus a `### Changed` CHANGELOG entry.

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`
passed with exit 0 (1513 tests), `/pharn-dev-regress` returned `no-regressions`, and
`/pharn-dev-verify` returned `PASS`.

## Floor-gate findings (blocking)

None.

- **L-floor (P0)**
- "`engines` is ≥ every known code-level floor of the installed runtime dependencies" → a regex
scan plus a version compare in a blocking vitest. Its reach is stated in the test comment:
literal name only, and only the rows the table names.
- "the smoke job runs the declared floor" → the equality case.
- "the packed CLI starts on the floor" → still advisory (a non-required job), labeled as such in
the workflow and the docs.
- **L-eval (P1)**
- The code-floor case FAILED on the base `package.json`, run before the fix, with the message
"…needs node >= 20.13.0), package.json says `>=20.12.0`".
- The planted-text cases prove the scanner both fires and stays quiet (grill finding 2).
- The non-vacuity case pins that `@clack/prompts` was actually read.
- **L-trust (P2)** — dependency files are read as text and never executed. No untrusted input is
involved.
- **L-axis (P3)** — the test file keeps its one axis, the declared Node floor. The workflow and doc
edits are version text only.

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

```yaml
- type: FINDING
rule_id: 'P0'
severity: minor
file: 'tests/engines.test.ts:112'
problem: 'shippedJs scans every .js/.mjs/.cjs file a dependency ships, including files that are not runtime code (minimist ships test/ and example/). A match there would raise the required floor over code pharn never runs. That errs toward a higher floor and would be visible in the failure message, so it is kept, not filtered.'
evidence: 'function shippedJs(dir: string): string[] {'
- type: FINDING
rule_id: 'P7'
severity: minor
file: 'tests/engines.test.ts:94'
problem: 'The table has one row, the failure actually measured. Future rows need the same measurement, on the boundary Node versions, before they are added, or the table turns into a guess list. Stated in the comment ("whose minimum version was measured").'
evidence: 'const KNOWN_API_FLOORS: {'
```

## Verdict

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

Stages run, in order: `/pharn-dev-plan` → GATE 1 (human: plans A–F accepted with every recommended
answer — "Approve all") → `/pharn-dev-grill` → plan `## Files` reworded so the writes-scope parser
reads every path (formatting only; intent unchanged) → `/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)
- The 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.
46 changes: 46 additions & 0 deletions .dev/features/engines-styletext-floor/VERIFY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# VERIFY — engines-styletext-floor

## FLOOR layer (owns the verdict)

The gates ran over the whole repo with the feature present, on node 22, with the session proxy
variables unset. They ran as root **without** `CAP_DAC_OVERRIDE` / `CAP_DAC_READ_SEARCH` /
`CAP_FOWNER` (`setpriv`), the CI-equivalent of this root sandbox.

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

- `test` is vitest (1513 tests). It collects this increment's own tests: `tests/engines.test.ts`,
whose code-floor case fails on the base `>=20.12.0`.
- `test:floor` is floor.yml's `node --test` run (754 tests).
- There is no `structural:*` gate: the increment ships no eval-actual pair.

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

Outside the verdict, the packed CLI was driven in a 120×40 pty through `pharn remove`'s picker on
the official binaries. Each cell shows the exit code and what printed:

| keys | Node 20.12.0 | Node 20.13.0 |
| ------------------------------ | ------------------------------ | -------------- |
| Ctrl-C at the picker | 0, "Cancelled" | 0, "Cancelled" |
| Space, Ctrl-C (a selection) | **1, `ERR_INVALID_ARG_VALUE`** | 0, "Cancelled" |
| Space, Enter, Ctrl-C (confirm) | **1, `ERR_INVALID_ARG_VALUE`** | 0, "Cancelled" |

`--version` runs on both, which is why the smoke job alone could never catch this.

## ADVISORY layer

`node .dev/floor/count-verifiers.mjs .` → `{"registered":0,"verifiers":[]}`. No verifiers are
registered, so the verdict rests on the 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.

The code scan sees only the API usages its table names, called by their literal name. The
non-required smoke job is the only runtime check at the floor, and it does not drive a prompt.
30 changes: 30 additions & 0 deletions .dev/features/engines-styletext-floor/regression-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"base": "abb274acbef59967d26068baa38676f1033ff182",
"inside": [
".github/workflows/ci.yml",
".github/workflows/node-floor.yml",
"CHANGELOG.md",
"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"
}
18 changes: 18 additions & 0 deletions .dev/features/engines-styletext-floor/verify-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"feature": "engines-styletext-floor",
"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": []
}
}
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +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.12.0"` while these gates run node 24 — the lower
# declares `engines.node: ">=20.13.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.
# `Smoke (node floor)` job in `node-floor.yml`, never by these gates.

on:
pull_request:
Expand Down
Loading
Loading