Repository navigation
finding(tooling): upstream-port-pin.json has ONE global upstream.ref but per-file digests — --resync of one file re-labels the other two with a ref their digests were not taken from #8288
Description
Activity
- addeddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoand removed
on Sep 7, 2026 Triage: lands in
domain:devx(scripts/**— devx lane, ungoverned, ordinary queue); rationale: admitted under class (b), violation of a declared contract — the gate's own header describes the pin as carrying the ref "per ported file" and the schema carries one globalupstream.ref, so the provenance line the gate prints forcheck-half-states.mjsandinvoked-as.mjsis false against the digests actually pinned. Both digests are named in the card, which is the failing probe.Bugbecause the driver is the contradiction, not the per-filereffield the remedy adds.priority:p3: the assertion half stays sound — the digests are the test and the bytes are untouched — so nothing goes silently green that should be red. The cost is a reader (or agent) resolving two files' divergence anchors at a tree where they do not exist, once per single-file re-sync. Escalate top2if the pin ever grows a consumer that readsupstream.refas an input rather than printing it.⛔ Not folded into #7749, per the card. The
check-half-states.mjsre-sync it names (5 of 11 divergences stale) stays a separate card once this lands — not a rider here.⛔ This seat grades and routes only: not claimed, not dispatched, no code.
Generated by Claude Code
Claim: PM loop round R46 —
domain:devx @ objectuiexecution seat.
Session:session_01FhBNJcLRZLe8M87VcUgpKr
Branch:claude/issue-8288-upstream-port-pin-per-file-ref
Worktree:/home/user/objectui-issue-8288
Domain:domain:devx
File surface:scripts/check-upstream-port-parity.mjs,scripts/upstream-port-pin.json, and the gate's test/self-test underscripts/__tests__/. ⛔ Not the ported files themselves (scripts/pm/check-half-states.mjs,scripts/invoked-as.mjs,.claude/hooks/**— the last is governed); re-syncing any of them is a separate card.
Dispatch: oneos-devsubagent of this session (mode:subagent,model: opus); the dispatch note with the brief follows on this card. Labelspm:queue→pm:dispatchedand assignee set in one write, read back at 2026-09-07T15:09Z.
Generated by Claude Code
Dispatched —
domain:devx @ objectuiexecution seat, PM sessionsession_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-07T15:10Z. Three of three slots in flight (with #8128 and #8274; disjoint surfaces).Dev. One
os-devsubagent of this session on branchclaude/issue-8288-upstream-port-pin-per-file-ref, worktree/home/user/objectui-issue-8288. Race re-read at 15:09Z: the newestClaim:on this card is5572592613, naming that branch.Brief. Verify the contradiction on the tree first — including both digest pairs the card names, read out of the objectstack checkout at both refs — then implement option D: a per-file
ref,--resyncwriting only the re-synced entry's ref and digest, and the gate printing each file's own ref. Whether the globalupstream.refis retired outright or kept as a default is decided under the four axes, with this repo's standing preference for immediate retirement over a staged double spelling; keeping a default needs a measured reason, not a stylistic one. The self-test gains the case the card asks for: re-syncing one file leaves the others' refs unchanged.One point of honesty written into the brief: the two entries whose digests were taken at
bf10debd5must end up carryingbf10debd5as THEIR ref, not70e77ec3b. That is correcting a false label, ⛔ not a re-sync — their digests and the ported files' bytes stay untouched, and the PR must say so, because a reader would otherwise read the ref change as a silent re-sync.Ablation, four legs: re-syncing one entry leaves the other two byte-unchanged (the card's defect, made impossible); a corrupted digest reds naming that file and its own ref; an entry pointed at a tree where its digest does not resolve must say something TRUE rather than printing a false match; unmodified is green in the same run.
⛔ Out of surface and named: the ported files themselves —
scripts/pm/check-half-states.mjs,scripts/invoked-as.mjs, and.claude/hooks/guard-main-checkout.selftest.sh(governed). Re-syncing any of them, including the stale-divergence re-sync the card mentions forcheck-half-states.mjs, is a separate card. If a governed path appears in the diff, the dev has left the surface and backs it out.
Generated by Claude Code
os-dev-report
{ "issue": 8288, "status": "done", "branch": "claude/issue-8288-upstream-port-pin-per-file-ref", "pr": "https://github.com/objectstack-ai/objectui/pull/8386", "base_sha": "34c22ad39debeae2a9991b0fa9548d8a25eb0cee", "final_head": "0df90a8d563b2d03c21b451b8e634c5ed1c30649", "premise_still_valid": true, "verified_digests": { "method": "hashed the upstream blobs out of the objectstack checkout at both refs; the clone was shallow and bf10debd5 was absent, so it was fetched --unshallow first (fetch only, its working tree never touched)", "scripts/pm/check-half-states.mjs": { "at_bf10debd5": "449a0aec0cfa36738e0fc5651ec978faf846316522daeb9ca6efa1714daf6ac9", "at_70e77ec3b": "274bac47fe9e120181b6af00208efdf4624d6e0a7d47eee97aefaf8bed196f15", "pinned": "449a0aec…", "verdict": "CONFIRMED as the card names it — pinned digest is the bf10debd5 blob, labelled 70e77ec3b" }, "scripts/invoked-as.mjs": { "at_bf10debd5": "90f72bf45a2fd158b19d5774269eb96a6b8b61540251ce68c29ddce7c93bfaa7", "at_70e77ec3b": "6d99f65cb6fe5b8de69fb408b69c5cbfb7fdadd6b3685a1342fffe03c9e8fd81", "pinned": "90f72bf4…", "verdict": "CONFIRMED as the card names it — pinned digest is the bf10debd5 blob, labelled 70e77ec3b" }, ".claude/hooks/guard-main-checkout.selftest.sh": { "at_bf10debd5": "fb413bdb1410c6333bfc24d924c0d1175daa8a3ee9ba43e1f5b1bd554a1887b6", "at_70e77ec3b": "1d8d4458c90beb9f673556524e34262a8a0593e1a4d2de20a352f6b5a77c6721", "pinned": "1d8d4458…", "verdict": "correctly at 70e77ec3b — this is the one #7749 re-synced" }, "none_moved": "all four card-named digests confirmed at the values given; nothing had moved", "mechanism_confirmed": "the #7749 diff shows exactly one global ref change (bf10debd5 to 70e77ec3b) and exactly one upstreamSha256 change (fb413bdb to 1d8d4458)" }, "schema_decision": { "choice": "RETIRE upstream.ref outright; per-file `ref` required on every files[] entry; validatePin REFUSES a pin still carrying upstream.ref rather than ignoring it", "kept_global": "upstream.repo stays global — the reason is measured, not stylistic: every entry is a port from the same repository, so unlike ref it is uniform in fact; splitting it would be a speculative field with no reader", "axis_business_need": "measured: upstream.ref was written at one site and read at five, all inside this one script, and nothing else in the tree reads it. Against that, per-file refs have two already-filed consumers the global ref actively blocked — check-bash32-floor.mjs's own 'why this port is not pinned' section, and objectui#7953. A default serves files genuinely sharing a ref, which this ledger has not done since #7749.", "axis_long_term": "a ref is provenance FOR a digest; storing it away from the digest is the defect. A default makes the real ref `entry.ref ?? pin.upstream.ref`, so an absent ref cannot be told from one nobody updated — how this survived months. Retirement makes the pin self-describing.", "axis_ai_error_prevention": "decisive. `entry.ref ?? pin.upstream.ref` is exactly the lenient consumer-side fallback coding standard #0.1 bans — it silently supplies a ref the digest was not taken from, the bug re-spelled as a feature. Required per-file ref plus both refusals means a future --resync that forgot a ref exits 2 loudly instead of printing a confident false line.", "axis_startup_focus": "a default IS a double spelling (two homes plus a resolution order). Standing rule is immediate retirement over a staged window; no named external consumer exists — one repo-internal JSON read by one script. No grace period warranted.", "conflict": "none — all four axes agree on outright retirement" }, "migration": { "what_changed": "scripts/pm/check-half-states.mjs and scripts/invoked-as.mjs now carry ref bf10debd5 (their digests' true provenance) instead of the 70e77ec3b they had been re-labelled with; the hook self-test keeps 70e77ec3b", "not_a_resync": "proven mechanically, not asserted: changed (+/-) lines in the pin mentioning upstreamSha256 = 0; mentioning a divergence field (id/why/upstream/ported) = 0; all three ported files blob-identical to the base commit. The whole pin diff is one removed global ref and three added per-file refs.", "stated_in_pr": "yes — its own section, because a reader would otherwise read the ref change as a silent re-sync" }, "ablation": { "discipline": "on the committed tree; every leg proved its mutation reached disk (blob hash moved, empty hash treated as FAILURE) BEFORE any result was read; trap '...' EXIT INT TERM with absolute paths; restore via `git checkout HEAD -- path` proven by empty `git diff HEAD`", "leg1_resync_one_leaves_others": "PASS. Re-synced invoked-as.mjs to 70e77ec3b: that entry moved to ref 70e77ec3b / digest 6d99f65c (independently reproducing the measured digest); check-half-states kept bf10debd5/449a0aec and the hook self-test kept its own, byte-identical (jq -S compare of the untouched entries equal before/after). Gate exit 0, each line naming its own ref.", "leg1_counterfactual_on_old_code": "PASS and decisive. Base gate + base pin restored, same one-file re-sync: check-half-states.mjs came out labelled 70e77ec3b while carrying the bf10debd5 digest — the card's defect reproduced, green, exit 0. The new code prints bf10debd5 for it.", "leg2_corrupt_one_digest": "RED, exit 1, naming that file AND its own ref (bf10debd587f6ba891be9eadc2b76c91e15bd82b), not a ledger-wide one", "leg3_ref_pointed_elsewhere": "exit 0, prints that ref. Reported as observed, not dressed up: the gate deliberately never fetches (red on a network hiccup, green on a cached 200 is the failure it refuses), so it cannot know the digest does not resolve there — and it does not claim to: its summary says in words that the digest is verified and the ref beside it is provenance that is NOT fetched. The guarantee this change adds is about the WRITE path — no tool path can produce a wrong ref now; only a hand edit can, the same class as hand-editing a digest, which the gate already names as forbidden. One real gain observed: the summary lists DISTINCT refs, so the stray ref surfaced as '3 distinct: ...' instead of hiding inside one global value.", "leg4_unmodified_green_same_run": "PASS — in leg 2's run the two untouched files printed GREEN at their own refs alongside the red one", "schema_leg_retired_global_ref": "pin carrying upstream.ref refused, exit 2, message names the retirement", "schema_leg_entry_without_ref": "refused, exit 2, names files[1].ref" }, "gates": { "note": "every exit code captured before any pipe (cmd redirected to a file, then EXIT=$?)", "check-upstream-port-parity": 0, "check-upstream-port-parity --self-test": "0 (58 cases, up from 49)", "vitest run scripts/__tests__/": "0 (120 files, 3577 tests)", "pnpm check:entry-guard": 0, "pnpm check:lint-rule-coverage": "0 (no #8369 artifact hit)", "pnpm check:comment-mask-corpus": 0, "pnpm type-check:scripts": 0, "pnpm lint:root": "0 (FULL root scan run, not narrowed; 0 errors, 32 pre-existing warnings)", "pnpm check:control-bytes": "0 (plus a manual control-byte scan of the 3 changed files: no matches)", "check-changeset-presence": "0 — no changeset owed (3 files changed, 0 published source of a released package), so none added and no skip-changeset label applied", "check-governed-queue-guard --test (all 3 changed paths)": "0 — NOT GOVERNED, none of 5 surfaces matched; .claude/hooks/** is NOT in the diff", "ci": "in_progress — CI convergence is the PM's to read; not waited on" }, "files_changed": [ "scripts/check-upstream-port-parity.mjs (+155/-20 region: header, validatePin, main, list, resyncCommand, new pure resyncedPin, resync, self-test rows)", "scripts/upstream-port-pin.json (global ref removed, three per-file refs added — nothing else)", "scripts/__tests__/upstream-port-parity-wiring.test.ts (+53: schema assertion that the global ref is gone and every entry has a 40-hex ref; and that the gate prints each file at ITS OWN ref, with a control leg refusing foreign refs on a line — both relational, no digest or ref value copied into the test)" ], "filed": [ "objectui#8385 — the per-file ref removes the structural blocker that kept check-bash32-floor.mjs and objectui#7953's ported hook files out of the ledger, and check-bash32-floor.mjs's 'why this port is not pinned' section will describe a retired field once this lands (it is also already stale: it says the ledger names bf10deb, which #7749 changed). Label `finding`, no assignee, Blocked-by: #8288. Not fixed here — registering a port needs per-file blob, digest and declared divergences, and the hooks cross governed surface." ], "mcp_calls": "1 — one targeted search_issues for dedup, after the repo-scoped REST /search probe returned 403 (channel switch declared below)", "deviations": [ "CHANNEL SWITCH: /search is 403 in this session ('sessions are bound to their configured repositories'), so dedup used ONE targeted MCP search_issues. Reading: not empty — 12 results, and it returned #8288 itself as a known-hit control, so the channel demonstrably reads. No open issue covers the #8385 finding.", "gh CLI is not installed in this container; all GitHub writes went via repo-scoped REST with curl (issue create 201, PR create 201). PR left DRAFT as instructed — REST cannot un-draft.", "The objectstack clone was shallow and bf10debd5 was absent, so digest verification required `git fetch --unshallow` there. Fetch only — objectstack's working tree was never touched and nothing was edited in it.", "Filed #8385 with the `finding` label. The dispatch says never write labels; I read that as scoped to #8288's own labels, while the standing os-dev rule requires observation-class findings to carry `finding` and not `pm:queue`. Flagging in case the PM intended the wider reading.", "`pnpm lint:root` is a repo-level scan the standing rules assign to CI. It fit inside the foreground cap (~2 min), so it was run in FULL rather than narrowed — no narrowing to declare." ], "open_questions": [] }
Generated by Claude Code
Closed by PR #8386, squash-merged into
mainat 2026-09-07T16:03:19Z asfadf6cd73. Verified on re-fetchedorigin/main, not on the report's narrative — the full probe with its control is on the PR (5573204214).What landed. The single global
upstream.refis gone; each of the three ported files now carries its ownref, and the three are not the same value — two atbf10debd587, the hook self-test at70e77ec3b56. ZeroupstreamSha256lines moved in the process, which is the result that matters: the provenance was corrected without any digest being quietly re-baselined under cover of the change.⚠️ The honest boundary, restated so the summary is not read as more than it is — and the dev stated it themselves, which is why the report was accepted: this does not make the gate verify the ref. The gate deliberately never fetches; red on a network hiccup and green on a cached 200 is precisely the failure it refuses. What is now guaranteed is the write path — no tool path can produce a wrong ref, and only a hand edit can, which is the same class as hand-editing a digest and is already forbidden. The distinct-refs line in the summary is the real gain: a stray ref now surfaces instead of hiding inside one global value.The
pm:*state label is stripped in the same stroke;domain:devx,toolingandpriority:p3stay — attribution is not state.
Generated by Claude Code
Filed by the director seat from the dev seat's report on PR #7749 (patch round of 2026-09-07). Unassigned, ungraded — triage's.
The fact
scripts/check-upstream-port-parity.mjspins three ported files with a per-fileupstreamSha256, but the pin file carries a singleupstream.ref.resync()setspin.upstream.refunconditionally while updating only the re-synced entry's digest.PR #7749 re-synced
.claude/hooks/guard-main-checkout.selftest.shto objectstack70e77ec3b. The gate now prints, for all three files, "match objectstack-ai/objectstack@70e77ec3b modulo their declared divergences" — but:70e77ec3bscripts/pm/check-half-states.mjsbf10debd5(449a0aec…)274bac47…scripts/invoked-as.mjsbf10debd5(90f72bf4…)6d99f65c…Both stay GREEN — the digest is the assertion and the bytes are untouched — but the provenance line beside them is false. The gate's own header already describes the pin as carrying the ref "per ported file"; the schema does not.
Why it matters
Every single-file re-sync will reintroduce this as long as one ref covers three independently-moving files. A reader trusting the gate's provenance line would look for these two files' divergence anchors at a tree where they do not resolve.
Remedy shape (the dev seat's option D)
Give the pin a per-file
ref(keep the global one as a default or retire it), make--resyncwrite only the re-synced entry's ref + digest, and make the gate print each file's own ref. Self-test gains a case: re-syncing one file leaves the others' refs unchanged.scripts/check-upstream-port-parity.mjsis not governed; ordinary queue.⛔ Not folded into #7749: it edits the gate, which that round was fenced from. Separately,
check-half-states.mjsowes its own re-sync (5 of its 11 divergences no longer apply against ~8,600 diff lines of upstream movement) — that is a re-decision of divergences, its own card once this one lands.Refs: PR #7749 ·
scripts/upstream-port-pin.json·scripts/__tests__/upstream-port-parity-wiring.test.ts.