Repository navigation
Root engines.node >=22 claims support for 22.0.0–22.11.x, a window yargs does not support #137
Description
Activity
Triage: escalating to decision box — the filer states plainly this is "a decision about a value" (a declared public
engines.nodecontract), and #127's ruling explicitly forbade changing declared values without one.① platform long-term coherence: option 2 (gate enforces "declared floor must satisfy every dependency range") is strictly more correct than the current max-of-minimums check and closes the exact blind spot #127 was filed to end — but it goes red on
maintoday until paired with option 1.
② measured business pull: zero today — no contributor is on Node 22.0.0–22.11.x, CI pins latest 22.x. Not urgent.
③ AI-agent error-resistance: option 2+1 turns "declaration nobody can mechanically check" into an enforced invariant; option 3 (accept as-is) leaves a claim CI cannot verify, which is the shape #127 exists to retire.
④ startup scope discipline: option 1 is a one-line tightening (>=22→>=22.12.0in two files), not scope growth; option 2 is a small gate change, not a new subsystem.Recommendation: 2 paired with 1 (the filer's own recommendation) — makes the declaration true and makes it stay true. Option 3 is coherent but re-creates a silently-unverifiable claim.
Confidence gap: this analysis has not checked whether any downstream consumer (e.g. a Docker base image, a hosting provider's Node runtime matrix) advertises 22.0.0–22.11.x support that>=22.12.0would newly exclude — worth a quick check before landing option 1.
Generated by Claude Code
Generated by Claude Code
Maintainer ruling recorded (2026-08-20, live decision-inbox session with the triage seat, session
session_01PjAP6vbcsg2yMtvySPv1Qo)Ruled: 2 paired with 1 — the gate strengthens to "the declared floor version must itself satisfy every dependency range", and the declarations tighten to
>=22.12.0(root +apps/docs) so the strengthened gate lands green. Provenance: maintainer accepted the batch, verbatim: 「其他接受你的建议。」One floor, mechanically true — option 3's "a declaration nobody can check" shape is rejected.⚠️ Fold with #138 (same files, same gate, both ruled — the five-gate family test reads foldable): one PR carries both rulings. State transition in the same stroke:needs-user-decision→pm:queue.
Generated by Claude Code
os-project-manager commented
on Aug 22, 2026 CollaboratorMore actionsFold-or-serial: FOLD with #138, dispatched as one unit
PM seat
repo:objectos(objectstack#9831), sessionsession_01V8AcCw8C1feB7b5kiaJd7b, round 1, 2026-08-22. Triage ruled the fold in both cards' rulings; this comment records the five-gate answer rather than inheriting it unexamined.- Same defect form, same fix — both are "a declared Node floor that is not mechanically true", repaired by moving declarations and widening what the gate governs. Not merely the same keyword or subsystem.
- Same region —
package.json,apps/docs/package.json,tools/ci-scripts/package.json,.github/scripts/check-node-floor.mjs. One worktree, one queue slot. - Every member adjudicated — both ruled 2026-08-20, verbatim 「其他接受你的建议。」Nothing from the decision inbox is riding along.
- Each independently verifiable — Root
engines.node>=22claims support for 22.0.0–22.11.x, a window yargs does not support #137: the gate enforces "the declared floor version must itself satisfy every dependency range", and root +apps/docsread>=22.12.0. [finding] There is a fourth Node floor declaration —tools/ci-scriptssays>=20.0.0while the repo says 22 #138:tools/ci-scriptsreads the same floor and appears inDECLARATION_FILES. Two distinct criteria; the batch cannot quietly under-deliver. - Exclusion list — things that look like family and are not: objectos#141 (version pins in
quickstart.mdxprose —pm:on-hold, CLI publish trigger, unrelated toengines.node); objectos#142 (version-shaped claims inchangelog.mdx—pm:blocked); and.node-version, which is a version-manager pin, not anengines.nodedeclaration. The ruling's phrase "governs all four declarations" must not be read as license to fold.node-versionintoDECLARATION_FILESas though it were a fourthpackage.json— the gate already treats it under its ownnode-versionrule. Whoever implements this: verify that before touching it.
Premises re-verified on
origin/mainat514527b, not quoted forward: rootpackage.json">=22"·apps/docs/package.json">=22.0.0"·tools/ci-scripts/package.json">=20.0.0"·.node-version22·DECLARATION_FILES = ['package.json', 'apps/docs/package.json']· bothyargs@18.0.0andyargs-parser@22.0.0still declare^20.19.0 || ^22.12.0 || >=23. All five hold; the card is accurate.One new interaction, discovered this round and not visible when this card was filed. Five Dependabot PRs are open against
pnpm-lock.yaml(#46, #47, #48, #49, #50), and this card's gate reads that lockfile to compute the floor. A dependency bump can move the ranges this card reasons about. Checked: #48 does not movewrangler(stays4.95.0), so theyargsranges are untouched — but the implementing dev must re-derive the lockfile maximum at implementation time rather than trusting the numbers above, since more of those PRs may land first. This is the same "never quote a measurement forward" rule that has caught this seat three times.Dispatch deferred to round 2 —
batch:3is full with #90, #102 and #132.
Generated by Claude Code
Claim: PM loop round 1 — fold chain head (#137 + #138, one dev, one PR)
Session:session_01VFwZj1a84ZxFUcWAi5H8S5
Branch:claude/issue-137-node-floor-satisfies
Worktree:objectos-issue-137
Domain:repo:objectos(seat objectstack#9831)
File surface:package.json·apps/docs/package.json(enginesblock only) ·tools/ci-scripts/package.json·.github/scripts/check-node-floor.mjs(rule + its in-file self-test fixtures) (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus—dispatch-gates.mjs --tieron this surface returns "no path-derived mandate … floor sonnet · default opus · ceiling fable". Held at default rather than floor: this changes a validator's rule set, and a strengthened rule shipped without a red fixture is a validator that cannot go red.
Clause-②: no —objectoshas nopackages/spec; this changes a repo-internal CI gate, not a contract's accept/reject behaviour and not a public surface.
Serial constraints cleared: not clean — declared, not waved through. Four open Dependabot PRs (#46, #48, #49, #50) all rewritepnpm-lock.yaml, and this card's gate reads that lockfile to compute the required floor. #49 and #46 also touchapps/docs/package.json— itsdependenciesregion, not theenginesregion this card edits. This seat is landing that relay during this same round, so the lockfile will move under this branch. Mitigation is in the dispatch: mergemainonce before finalising and re-derive the maximum from the lockfile as it then stands. No non-Dependabot PR is open; no other card is in flight on these files.Dispatch-time premise re-check — run now on
origin/mainatcb41537The predecessor verified these at
514527b;mainhas moved one commit since, so they were re-measured rather than inherited. All hold:Premise cb41537root package.json">=22"✅apps/docs/package.json">=22.0.0"✅tools/ci-scripts/package.json">=20.0.0"✅.node-version22✅DECLARATION_FILESstill the two-entry list, at line 83 ✅ yargs@18.0.0,yargs-parser@22.0.0both still ^20.19.0 || ^22.12.0 || >=23✅wranglerstill 4.95.0— the path those ranges enter by ✅check-node-floor.mjsinSELF_TESTED✅ run-self-tests.mjs:60One premise I checked because it could have retired this card, and it did not
RULESat line 86 already containsrangeandcoverage, which read at a glance like the rule this card asks for. They are not. Bothrangesites (lines 339, 360) fire only on "is not a range this parser understands" — a parseability rule.coveragereports files the gate did not look at. The floor comparison at lines ~334-370 is still strictly max-of-minimums viarangeLowerBound, and there is no "declared floor satisfies every dependency range" check anywhere in the file. Option 2 is genuinely unbuilt; the card is accurate. Had this gone the other way the card would have been closed rather than dispatched.Worth carrying into the implementation: the existing declaration-agreement check treats
>=22and>=22.0.0as the same floor semantically, which is exactly whymainis green today despite the mismatch this card fixes.
Generated by Claude Code
- added a commit that references this issue
on Aug 22, 2026 File-surface increment — one bounded in-place fix, declared before the report
Dev session
session_01VFwZj1a84ZxFUcWAi5H8S5, branch
claude/issue-137-node-floor-satisfies. The claim declared four files. A fifth
was edited under the bounded in-place-fix exemption, so the declared surface is
amended here rather than left to be discovered in the diff:.github/workflows/ci.yml— the comment on thenode-floorjob opened with
"The three Node floor declarations (rootengines.node,apps/docs
engines.node,.node-version)". That is the same stale count #138 exists to
correct (its own body: "#127 and #121 both describe the Node floor as three
declarations. There are four"), sitting in the comment on the job that runs the
gate. Left alone it would have shipped a PR whose whole subject is declarations
that say what is true, while the job's own comment miscounted them.The four conditions, checked rather than assumed:
- Same defect class — a stale enumeration of the Node floor declarations;
literally the sentence [finding] There is a fourth Node floor declaration —tools/ci-scriptssays>=20.0.0while the repo says 22 #138 was filed against. - Mechanical, form pinned by existing evidence — the correct count is the
measured file set (threepackage.json+.node-version), fixed by the
ruling. - No other claim on the file — no non-Dependabot PR open, and the four
Dependabot PRs touchpnpm-lock.yaml/apps/docsdependencies, not
.github/workflows/. - Same gate family, no new verification surface — it is a YAML comment; it
changes no step, no job, and nothing any gate evaluates.
It is its own commit (
c5ecd88,docs(ci): the node-floor job checks four declarations, not three) and is named with its evidence in the PR body, so it
is reviewable separately from the two ruled changes.
Generated by Claude Code
Generated by Claude Code
- Same defect class — a stale enumeration of the Node floor declarations;
(Superseded — this comment was accidentally overwritten by this session while repairing a stripped report marker. The report of record is #137 (comment))
Generated by Claude Code
ACCEPT — PR #156 (chain head; #138 accepted on its own criterion, see there)
repo:objectosseat (objectstack#9831), sessionsession_01VFwZj1a84ZxFUcWAi5H8S5, round 1. Reviewed againstorigin/mainand by executing the gate myself, not against the report.Checklist: draft ✓ · base
main✓ ·Fixes #137+Fixes #138, one commit each ✓ · 5 files,+331/−65✓ · no changeset (no flow here) ✓ ·.github/**is not a governed surface, so this takes the normal queue path ✓.Gates on head
592ede4— the dev mergedmainin and re-derived, so my earlier green onc5ecd883was superseded and re-read:build(required) completed/success ·Node floorcompleted/success.Ownership & freshnessdid not run, and I verified that is by design rather than a missing gate:translations.ymlis path-filtered tocontent/docs/**,apps/docs/lib/i18n.tsandcheck-translation*.mjs—check-node-floor.mjsmatches none of them.I ran the gate rather than reading its transcript
A PR that changes a validator is exactly where a quoted verdict is worth least, so I checked out
592ede4and ran it:| `package.json` | `>=22.12.0` | 22.12.0 | | `apps/docs/package.json` | `>=22.12.0` | 22.12.0 | | `tools/ci-scripts/package.json` | `>=22.12.0` | 22.12.0 | | `.node-version` | `22` | pin | ✅ Every declared floor clears what the dependency tree requires, and the declarations agree.#138's observable criterion holds in my own run: no notes section, and
tools/ci-scriptsappears as a governed declaration. Theungovernedadvisory is gone because the file is governed now, not because the rule was silenced.And the property that actually matters — the new rule can go red:
✓ a floor inside a gap in a disjunctive range fired [unsupported] ✓ an exclusive floor lands in the same gap fired [unsupported] ✓ declared floor below what the tree requires fired [lockfile unsupported] ✓ the tools/ci-scripts declaration disagrees fired [declarations] ✓ classify unsupported -> blocking ✓ self-test: 17 rule case(s), 24 satisfies case(s) and 18 range case(s)The load-bearing fixture fires
unsupportedalone, withlockfilesilent — the exact blind spot #137 was filed against — andunsupportedclassifies blocking, not advisory. A strengthened rule with no red fixture is a validator that cannot fail, which PR #74's ruling rejected; this one is demonstrated failing on the repo's own former values, so the fixture doubles as their regression test.Design decisions I checked rather than waved through
- One comparator parser feeding both rules. The stated reason is the right one: two parsers disagreeing about what counts as an understood range would put a range under one rule and outside the other. All 18 pre-existing
RANGE_CASESpass unchanged, which is the evidence the refactor preserved the reduction exactly rather than an assertion that it did. unsupportedsubsumeslockfile, andlockfileis kept anyway — correct, and for the stated reason: it names the version to bump to, which a gap cannot. The pair is pinned in a fixture, so a future change that makes them independent has to say so there.lowestClaimed()—>22.0.0claims 22.0.1, not 22.0.0. Subtle and right: testing the excluded version itself would let a>declaration walk past on a technicality. It has its own fixture..node-versioncorrectly excluded fromDECLARATION_FILES, with the reason now written next to the list — a bare22through the range parser would read as floor 22.0.0 and call the repo's own>=22.12.0a disagreement. This was the trap most likely to be got wrong and it was got right.- Unparseable ranges skipped in
unsupportedso one unread range does not produce two findings.
Surface increment: accepted
The fifth file,
.github/workflows/ci.yml, is outside the dispatched surface. Declared before the report, in its own commit, with its four conditions checked — and it is in scope on the bounded rule this seat already applies: it is a claim this PR's own change falsifies. The job comment enumerated "the three Node floor declarations", which is literally the sentence #138 was filed against, sitting on the job that runs the gate. Shipping a PR whose entire subject is declarations that say what is true, while the checking job's own comment miscounts them, would have been the contradiction. Going looking for others would not have been in scope; this was not that.The triage confidence gap on this card is closed, not carried: all three packages are
private: true, so nothing is published and no consumer can read these fields —>=22.12.0newly excludes nobody.Deviations: none beyond the declared increment.
Driving to landing now.
Generated by Claude Code
- One comparator parser feeding both rules. The stated reason is the right one: two parsers disagreeing about what counts as an understood range would put a range under one rule and outside the other. All 18 pre-existing
os-dev-report
{ "issue": 137, "status": "done", "branch": "claude/issue-137-node-floor-satisfies", "pr": "https://github.com/objectstack-ai/objectos/pull/156", "premise_still_valid": true, "summary": "Fold of #137 + #138 delivered as one draft PR, three commits. #137: added an `unsupported` rule requiring the lowest version each declaration claims to support to itself satisfy every `engines.node` range in the lockfile, and tightened root + apps/docs to `>=22.12.0` so it lands green. The comparator parser was widened from a lower-bound reduction to a full interval, with `comparatorLowerBound` derived from it so both rules share one notion of a parseable range; all 18 pre-existing RANGE_CASES still pass unchanged, which is the evidence the reduction was preserved. #138: `tools/ci-scripts/package.json` bumped to the same `>=22.12.0` and added to DECLARATION_FILES; `.node-version` deliberately NOT added, with the reason recorded next to the list. Both cards' bodies and all comments were read in full on GitHub and verified complete (each terminates in its signature footer with all sections closed; no truncation). All eight dispatch premises re-measured on the branch point and all hold, including the near-miss: `range` fires only on 'is not a range this parser understands' at lines 339/360 and `coverage` reports unlooked-at files, so no 'floor satisfies every range' logic pre-existed - #137 was genuinely unbuilt. DEVIATION 1: one bounded in-place fix outside the declared surface - `.github/workflows/ci.yml`'s node-floor job comment said 'three Node floor declarations', the same stale count #138 fixes; taken as its own commit, four exemption conditions checked, surface increment declared in a separate comment on this issue before this report. DEVIATION 2, my own error: while repairing the stripped report marker I probed the comment-edit REST endpoint with a live PATCH carrying the body 'test' instead of a read-only GET, which overwrote the first copy of this report (comment 5380966226); the follow-up restoring PATCH was blocked by the sandbox classifier, so the report was re-posted as this comment. Nothing of another actor's was touched and no content was lost, but the probe should have been a GET and that comment still needs deleting or overwriting by someone with the access. No changeset: objectos has no `.changeset` dir and no changeset gate in any workflow (verified), and no `skip-changeset` label mechanism was created.", "tests": "All gates re-run on the FINAL head 592ede4 (after merging origin/main at f0a830d), exit codes captured by redirect-then-$? with no pipe in between, verdicts quoted from each gate's own output line.\n\n[1] `node .github/scripts/check-node-floor.mjs --self-test` (CI: node-floor / Self-test) -> exit 0, 'self-test: 17 rule case(s), 24 satisfies case(s) and 18 range case(s) - every rule demonstrated able to fail, every silence-bearing rule demonstrated able to stay silent'\n[2] `node .github/scripts/check-node-floor.mjs` (CI: node-floor / Check) -> exit 0, 'Every declared floor clears what the dependency tree requires, and the declarations agree.'\n[3] `node run-self-tests.mjs` in tools/ci-scripts (CI: build / turbo run test) -> exit 0, '2 self-test(s) passed'\n\nEXPECTED-RED, observed live on the real repo before the declarations were changed (new rule + main's values): exit 1, '2 finding(s).' both `unsupported`, naming yargs-parser@22.0.0 and yargs@18.0.0 on '^20.19.0 || ^22.12.0 || >=23'. Direction as predicted: red, not a reversal.\n\n#138 UNGOVERNED NOTE, before/after (its observable acceptance criterion). BEFORE, on cb41537: '1 note(s), reported and not blocking: - **ungoverned** - tools/ci-scripts/package.json declares engines.node \">=20.0.0\", which this gate does not check - it governs package.json and apps/docs/package.json only'. AFTER: the entire notes section is absent and the file appears in the declarations table as '| `tools/ci-scripts/package.json` | `>=22.12.0` | 22.12.0 |'. Mechanical count of the string 'ungoverned' in captured gate output: before 1, after 0.\n\nABLATIONS - two, each a plain .mjs with no build step, each mutation confirmed on disk by grepping BOTH the injected and the deleted text (not an editor exit code), each restored by a `trap ... EXIT INT TERM` and the restore leg verified. (A) revert DECLARATION_FILES to two entries: injected form present=1, deleted form present=0; the dedicated fixture goes red - 'the tools/ci-scripts declaration disagrees fired [ungoverned] expected [declarations]' - and the advisory note reappears in the gate. (B) revert all three declarations to main's values keeping the new rule: three injected values present, '22.12.0' count 0 in all three files; gate exit 1 with 12 findings. After both: git status --porcelain empty, DECLARATION_FILES back to three entries.\n\nDISCRIMINATING FIXTURE (the point of the card): 'a floor inside a gap in a disjunctive range' fires [unsupported] ALONE with `lockfile` silent - proof the new rule catches what max-of-minimums structurally cannot. Its values are this repo's own former declarations. A second fixture pins the exclusive-floor path (>22.0.0 claims 22.0.1). `unsupported` joined SILENT_RULES; the direction-guard fixture now ignores both rules.\n\nSELF-TEST CAUGHT A REAL BUG IN MY OWN WORK: adding the third governed file made the `lockfile` fixture fire [declarations lockfile node-version unsupported] instead of [lockfile unsupported], because it moved only two of three declarations. Fixed by moving all three. The exact-set assertion did its job.\n\nLOCKFILE RE-DERIVED AT FINALISATION, not quoted forward: main moved cb41537 -> f0a830d while the branch was open, but that is #152, docs-only, touching no package.json and not pnpm-lock.yaml, so no part of the Dependabot relay has landed. Merged origin/main in and re-measured on the merged tree: 428 engines blocks, wrangler@4.95.0, both yargs ranges still '^20.19.0 || ^22.12.0 || >=23', 0 of 428 ranges unsatisfied at 22.12.0 and 0 unparseable.\n\nDECLARED NARROWING: apps/docs `type-check` and `build` were not run locally. The diff changes no TypeScript, no MDX, no dependency and no config those steps read. `pnpm install --frozen-lockfile` cannot be affected: .npmrc sets no engine-strict (read: only auto-install-peers and strict-peer-dependencies) and the lockfile's importers section contains zero `engines` entries (measured). CI runs the farm regardless. Control-byte scan over all five touched files: no matches. All three package.json files re-parsed as valid JSON after editing.\n\nCI status at report time: in_progress - reported at draft-PR time per the standing contract, not waited on.", "open_questions": [], "out_of_scope_findings": [] }Marker note for the PM's scanner: this comment leads with the literal text
os-dev-reportrather than the HTML-comment form. The angle-bracket marker was
posted first and the body sanitizer stripped it — read back and confirmed
missing — so the fallback spelling is in use.Superseded comment: comment
5380966226above currently readstest. That
was this session's own mistake, not another actor's edit — I probed the
comment-edit endpoint with a livePATCHinstead of aGETwhile trying to
repair the marker, and the restoring call was then blocked. It carries no
information; this comment is the report of record.Not in the JSON, for triage rather than the machine parse: the triage
comment on this card flagged a confidence gap — whether any downstream consumer
advertises 22.0.0–22.11.x support that>=22.12.0would newly exclude. Measured
and closed: all three packages areprivate: true(objectos-monorepo,
@objectos/docs,@objectos/ci-scripts), so nothing in this repo is published
and no consumer can read these fields. The tightening has no external audience.No new out-of-scope findings were filed. The one adjacent thing seen —
content/docs/resources/changelog.mdxdescribingengines.nodeas>=22.0.0
— is about the published@objectstack/*packages, not this repo's own
declarations, so it is not made stale by this PR; it is also already covered by
objectos#142 on the fold's exclusion list.
Generated by Claude Code
Generated by Claude Code
Measured while implementing #127 (the Node floor gate). Filed unassigned and not folded in: #127's ruling scoped that PR to the check only and explicitly forbade changing any declared value, and this is a decision about a value.
The finding
pnpm-lock.yamlat5fd04d8carries:Those three branches cover
[20.19.0, 21.0.0),[22.12.0, 23.0.0), and 23 upward. Node 22.0.0 satisfies none of them. The rootpackage.jsondeclaresengines.node: ">=22", which asserts that every version from 22.0.0 up is supported — so the declaration currently claims a window (22.0.0 through 22.11.x) in which a package in the tree declares itself unsupported.yargsreaches the tree throughwrangler@4.95.0(apps/docsdevDependency).Why the new gate does not catch it
The gate that landed for #127 reduces each dependency range to the lowest version satisfying it and takes the maximum across the tree. For this range that minimum is 20.19.0 — far below 22 — so it never moves the maximum and never fires. That reduction is what the #127 ruling specified, and it is correct for the drift it targets (a declared floor below what the tree demands). It is structurally blind to a gap inside a disjunctive range, which is what this is. The limitation is documented in the script header rather than left implicit.
CI does not surface it either: all three workflows pin
node-version: 22inactions/setup-node, which resolves to the latest 22.x — today well above 22.12.0 — so the unsupported window is never exercised.Options
>=22.12.0(and theapps/docssibling to match). Makes the declaration true. Costs: it is a narrower claim than the repo may want to make, and it drifts again the moment a dependency moves.maintoday, purely because of this finding — so it can only land together with option 1 or with a decision to accept the finding.>=22is a statement about the major line rather than a precise floor. If this is the answer it is worth writing down, because the next reader will re-derive the same finding.Recommendation: 2 paired with 1 — option 2 is the rule that makes the declaration mean what it says, and option 1 is the one-line change that makes it green. Option 3 is coherent but re-creates the "declaration nobody can mechanically check" shape that #127 exists to end, just one level further in.
Not urgent: no contributor on a current Node 22 is affected, and CI is unaffected.
Generated by Claude Code