Skip to content

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

@os-zhuang

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.mjs pins three ported files with a per-file upstreamSha256, but the pin file carries a single upstream.ref. resync() sets pin.upstream.ref unconditionally while updating only the re-synced entry's digest.

PR #7749 re-synced .claude/hooks/guard-main-checkout.selftest.sh to objectstack 70e77ec3b. The gate now prints, for all three files, "match objectstack-ai/objectstack@70e77ec3b modulo their declared divergences" — but:

ported file pinned digest is the blob at blob at 70e77ec3b
scripts/pm/check-half-states.mjs bf10debd5 (449a0aec…) 274bac47…
scripts/invoked-as.mjs bf10debd5 (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 --resync write 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.mjs is not governed; ordinary queue.

⛔ Not folded into #7749: it edits the gate, which that round was fenced from. Separately, check-half-states.mjs owes 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.

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    and removed on Sep 7, 2026
  2. added theissue type on Sep 7, 2026
  3. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    ContributorAuthor

    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 global upstream.ref, so the provenance line the gate prints for check-half-states.mjs and invoked-as.mjs is false against the digests actually pinned. Both digests are named in the card, which is the failing probe. Bug because the driver is the contradiction, not the per-file ref field 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 to p2 if the pin ever grows a consumer that reads upstream.ref as an input rather than printing it.

    ⛔ Not folded into #7749, per the card. The check-half-states.mjs re-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

  4. claude commented on Sep 7, 2026

    @claude
    Contributor

    Claim: PM loop round R46 — domain:devx @ objectui execution 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 under scripts/__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: one os-dev subagent of this session (mode:subagent, model: opus); the dispatch note with the brief follows on this card. Labels pm:queue → pm:dispatched and assignee set in one write, read back at 2026-09-07T15:09Z.


    Generated by Claude Code

  5. claude commented on Sep 7, 2026

    @claude
    Contributor

    Dispatched — domain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-07T15:10Z. Three of three slots in flight (with #8128 and #8274; disjoint surfaces).

    Dev. One os-dev subagent of this session on branch claude/issue-8288-upstream-port-pin-per-file-ref, worktree /home/user/objectui-issue-8288. Race re-read at 15:09Z: the newest Claim: on this card is 5572592613, 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, --resync writing only the re-synced entry's ref and digest, and the gate printing each file's own ref. Whether the global upstream.ref is 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 bf10debd5 must end up carrying bf10debd5 as THEIR ref, not 70e77ec3b. 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 for check-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

  6. claude commented on Sep 7, 2026

    @claude
    Contributor

    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

  7. claude commented on Sep 7, 2026

    @claude
    Contributor

    Closed by PR #8386, squash-merged into main at 2026-09-07T16:03:19Z as fadf6cd73. Verified on re-fetched origin/main, not on the report's narrative — the full probe with its control is on the PR (5573204214).

    What landed. The single global upstream.ref is gone; each of the three ported files now carries its own ref, and the three are not the same value — two at bf10debd587, the hook self-test at 70e77ec3b56. Zero upstreamSha256 lines 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, tooling and priority:p3 stay — attribution is not state.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopriority:p3tooling

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions