') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(devx): resolve a regen row's gen:/check: in its declared owner, not only in packages/spec by os-project-manager · Pull Request #13616 · objectstack-ai/objectstack · GitHub
Skip to content

fix(devx): resolve a regen row's gen:/check: in its declared owner, not only in packages/spec - #13616

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13585-merge-regen-root-manifest
Aug 31, 2026
Merged

fix(devx): resolve a regen row's gen:/check: in its declared owner, not only in packages/spec#13616
os-project-manager merged 1 commit into
mainfrom
claude/issue-13585-merge-regen-root-manifest

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#13585

git-merge-regen.mjs --self-test resolved every regen row's gen:/check: names in
packages/spec/package.json and nowhere else, so an artifact owned by root tooling could not be
registered for merge=os-regen at all — however exactly it matched the pathology the driver exists
for. The refusal was correct about the tree and wrong about the world: it read as "you named a
script that does not exist"
when the truth was "this artifact is not owned by packages/spec."

Rows now carry an owner, defaulting to @objectstack/spec — which is what all 13 existing rows
declared implicitly — and the refusal names the manifests it searched.

Verified on 1997df337.


必答項 — is check:merge-driver's reconciliation still two-way?

Yes, and that requirement is what chose the shape of the fix. The answer has two halves.

1. The lookup stayed exact, so nothing became merely permissive

A name is looked for in one manifest — the row's declared owner's — never in "any manifest that
happens to have it". A widened lookup would have made resolution non-deterministic the moment two
manifests defined the same gen: name, and would have accepted a row that names a root-only script
while claiming a package owner. It is pinned as a live case rather than argued:
reconcileOwnership() asserts that check:merge-driver, a root-only script name, resolves
under ROOT_OWNER and does not resolve under @objectstack/spec. A permissive lookup passes
every other assertion in that block and fails exactly that one.

Both directions of the .gitattributes ↔ table reconciliation are untouched, and an owner that
resolves to no manifest is a refusal, not a skip — an unresolvable owner means a row whose
scripts were never verified, which is the state this reconciliation exists to make impossible.

2. The half that a one-sided loosening would have left standing — measured, not reasoned

This is the "registered but unreconcilable" seam, and it was real. Two consumers need to know
which manifest owns a row, not merely that some manifest has the name:

  • git-merge-regen.mjsprints a regeneration command at merge time;
  • check-regen-pending.mjs (the pre-commit gate) spawns one.

Both were bound to packages/spec — the runner's spawn directory was packages/spec
unconditionally. Measured before the owner field existed:

pnpm -s check:sdui-lockstep in packages/spec -> exit 254 ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "check:sdui-lockstep" not found
pnpm -s check:sdui-lockstep at the repo root -> exit 0 check:sdui-lockstep: OK -- ...

So a lookup-only widening would have produced a row that reconciles green and is then
permanently unsatisfiable in the hook. Driven end to end against the real
check-regen-pending.mjs, same root-owned row, the only difference being the owner declaration:

A — root-owned row, no owner declared (what a lookup-only widening leaves behind):

HOOK_EXIT=1
✗ packages/sdui-parser/objectui-lockstep.json — stale
pnpm --filter @objectstack/spec gen:sdui-lockstep <- a dead command

Every commit refused, forever, with remedy advice that cannot work.

B — the same row declaring owner: ROOT_OWNER (this PR):

HOOK_EXIT=0
✓ packages/sdui-parser/objectui-lockstep.json — current
os-regen: all deferred artifacts are current — marker cleared.

That is why the owner is declared rather than searched for: one declaration, read by the gate,
the driver's advice and the hook's spawn, so the command the driver prints is the command the gate
runs. Registered-and-unreconcilable is a worse defect than unregisterable, and a widened lookup
alone would have manufactured it.


Non-empty control — rejected before, registered after

The artifact that produced the finding, docs/audits/2026-08-tenant-audit-write-call-sites.counts.md,
lives on PR #13584's branch and is not on main, so it could not be used. The control used instead
is packages/sdui-parser/objectui-lockstep.json with gen:sdui-lockstep / check:sdui-lockstep
— the only root-owned gen:/check: pair in the root manifest on main, i.e. a genuinely
root-owned artifact and not a packages/spec entry testing a path that already worked.

Before the fix (registration attempted on origin/main, then backed out) — the card's exact
refusal shape, reproduced on a different artifact:

✓ .gitattributes ↔ regen-artifacts.mjs agree on 14 path(s)
✗ script(s) named by the table no longer exist in @objectstack/spec:
packages/sdui-parser/objectui-lockstep.json → gen:sdui-lockstep
packages/sdui-parser/objectui-lockstep.json → check:sdui-lockstep
✗ merge driver wiring is inconsistent — see above.

After the fix, same row, still no owner — still refused, because the loosening is not blanket,
and the refusal now names what it searched and points at the right repair:

✗ script(s) named by the table do not exist in their declared owner:
packages/sdui-parser/objectui-lockstep.json → gen:sdui-lockstep [owner @objectstack/spec, packages/spec/package.json]
packages/sdui-parser/objectui-lockstep.json → check:sdui-lockstep [owner @objectstack/spec, packages/spec/package.json]
Manifests searched: @objectstack/spec (packages/spec/package.json)
A row that declares no `owner` defaults to @objectstack/spec, so this can mean the row is
in the wrong package rather than that the script is gone. If ROOT tooling owns the
artifact, declare it — `owner: ROOT_OWNER` in scripts/regen-artifacts.mjs. ⛔ Do NOT move
the scripts into a package to satisfy the lookup: that lets this tool decide code ownership.

After the fix, declaring owner: ROOT_OWNER — registers, and pnpm check:merge-driver is green
end to end (exit 0):

✓ .gitattributes ↔ regen-artifacts.mjs agree on 14 path(s)
✓ all 28 gen:/check: names resolve in their declared owner (@objectstack/spec (packages/spec/package.json), @objectstack/spec-monorepo (package.json))
✓ owner resolution: 11 case(s) pinned, root manifest read as @objectstack/spec-monorepo
...
✓ merge driver wiring is consistent (7 path(s) deliberately excluded).
✓ check-regen-pending self-test passed.

And a third leg, an owner nobody can resolve refuses rather than skipping:

✗ owner(s) named by the table resolve to no manifest:
@objectstack/not-a-package — declared by packages/sdui-parser/objectui-lockstep.json
Unresolved is a REFUSAL, not a skip: those rows' scripts were never verified.

All three registrations were temporary, applied on top of the committed implementation and backed
out with git checkout HEAD -- ...; restoration was proven by an empty git diff HEAD plus a
HEAD-blob-hash comparison per file, not by an exit code.

Why the control artifact is not registered permanently here

packages/sdui-parser/objectui-lockstep.jsoncan now be registered, and deliberately is not.
gen:sdui-lockstep re-reads objectui's side, which needs an objectui checkout the merger is not
holding — so the deferred regeneration would be unrunnable at exactly the moment pre-commit demands
it, and the driver only ever defers. Its conflict is also not the shape the driver exists for: the
record holds one side at a named revision, so two branches that each re-recorded it disagree
about which objectui revision the parity was verified against, and no regeneration answers that.
Registering it to give this PR a permanent control would be the test dictating the code, so the
durable guard is reconcileOwnership() instead — it reads the real root manifest through the same
functions the driver and the hook use, on every check:merge-driver run.


The refusal now names which manifests were searched

Per the card: a refusal that misdescribes its own cause sends the reader to the wrong repair. The new
text names the owner and manifest per dead row, lists every manifest searched, states that an
undeclared row defaults to @objectstack/spec, and explicitly refuses the repair the old text
implied — moving root tooling into a package to satisfy a lookup path.


PM mechanism assumptions — verified

  1. "All currently-registered paths are spec-owned or spec-adjacent; the single-manifest assumption
    has never been stressed."
    Confirmed, with a correction to the count. There are 13
    registered paths on main, not 14 (the card's 14 was measured with its own candidate row
    already added). Ten are under packages/spec/; the other three —
    docs/protocol-upgrade-guide.md, docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
    and content/docs/references/** — are outside packages/. All 13 name scripts defined in
    packages/spec/package.json, so the assumption held by coincidence of ownership, never by
    constraint.

  2. "docs/protocol-upgrade-guide.md is outside packages/ with only its scripts inside — the real
    boundary is the manifest, not the artifact path."
    Confirmed, and stronger than stated: it is
    one of three such rows, not one. The boundary was always the manifest.

  3. "The two-way reconciliation is the fragile half; if loosening it cleanly is impossible without a
    declared-owner field, say so."
    Confirmed, and that is the shape delivered. A widened lookup
    cannot supply what the driver's advice and the hook's spawn need — which package — so the
    declared-owner field is not one of two acceptable options here but the only one that keeps the
    reconciliation two-way. See the 必答項 section.


On #13335

Triage's grading placed #13335 as the symptom of this cause, with this card blocking it. That
blocking relationship does not hold, and the acceptance case could not be run as specified.

skills/*/references/_index.md is written by gen:skill-refs and gated by check:skill-refs, and
both of those are defined in packages/spec/package.json (lines 257-258), not in the root
manifest:

./packages/spec/package.json:257: "gen:skill-refs": "tsx scripts/build-skill-references.ts",
./packages/spec/package.json:258: "check:skill-refs": "tsx scripts/build-skill-references.ts --check",

So that artifact was never blocked by the single-manifest lookup — it is spec-owned and could
have been registered at any time. Registering it here would therefore have been the vacuous control
this lane rejects: a packages/spec entry exercising a path that already worked. It is also a
disposition question in its own right (route it, or record why it must keep text-merging), which is
the substance of that card and not of this one. #13335 stays open and untouched; nothing under
skills/** is in this diff. The card's own body hedged this correctly — "If that card's generator
is also root-owned, this one blocks it"
— and it is not.


Verification

Gate family derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
(15 families + 2 convention-triggered for editing a gate script), not guessed. All run at
1997df337:

  • check:merge-driver — green, including the new ✓ owner resolution: 11 case(s) pinned line
  • check:agent-test-spelling, check:bash32-floor, check:cli-command-ids,
    check:cross-package-test-inputs, check:entry-guard, check:parse-guard,
    check:pnpm-filter-targets, check:watch-hint-literal — green
  • check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs — green
  • bare-root-worklist.mjs --self-test, check:pm-dispatch-gates (the two gate-script conventions) — green
  • @objectstack/speccheck:generated (all 14 generated artifacts up to date) and check:docs
    (230 generated files in sync) — green, after a full spec build
  • pnpm lint — the whole repo, eslint . --no-inline-config, green. No narrowing claimed.
  • check:nul-bytes — green; the diff also self-scanned for raw control bytes, none
  • check-declaration-mirrors.mjs (the .d.mts mirror of the edited runner) — green
  • The edited gate scripts' consumer suites: dist-freshness.test.ts,
    schema-tree-freshness.test.ts, build-schemas-check-mode.test.ts — 3 files, 84 tests passed —
    plus check:scripts-typecheck — green
  • check-test-completeness.mjsNOT MEASURED, by the gate's own declaration: it grades a saved
    turbo run test log and exits 3 (PREREQUISITE NOT MET) when run without one, which is the shape
    the derived family invokes. Not a red.

No changeset: this PR releases nothing from any package — the diff is three root tooling scripts —
so it carries skip-changeset.


Generated by Claude Code

…ot only in packages/spec
`git-merge-regen.mjs --self-test` resolved every row's script names in
`packages/spec/package.json` and nowhere else, so an artifact owned by ROOT
tooling could not be registered for `merge=os-regen` at all — however exactly it
matched the pathology the driver exists for. The refusal was correct about the
tree and wrong about the world: it read as "you named a script that does not
exist" when the truth was "this artifact is not owned by packages/spec", and an
author following it literally moves root tooling into a package it does not
belong to, purely to satisfy a lookup path.
Rows now carry an `owner` (defaulting to @objectstack/spec, which is what all 13
declared implicitly), and the refusal names the manifests it searched.
The owner is DECLARED rather than searched for, because a lookup-only widening
would have left the worse half standing. Two consumers need to know WHICH
manifest owns a row, not merely that some manifest has the name: the driver
PRINTS a regeneration command and `check-regen-pending.mjs` SPAWNS one, and both
were bound to `packages/spec`. A root-owned row under a widened lookup would
have reconciled green and then been spawned in a directory that does not define
its script — measured, `pnpm -s check:sdui-lockstep` exits 254 there and 0 at
the repo root — leaving the artifact permanently stale and every commit refused.
Registered-and-unreconcilable is a worse defect than unregisterable.
`reconcileOwnership()` pins the rule against the real root manifest on every
run, including the case a permissive lookup would fail: a root-only script name
must NOT resolve under @objectstack/spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] merge=os-regen cannot be wired for a root-owned generated artifact — git-merge-regen resolves gen:/check: names in packages/spec only

2 participants

@os-project-manager@claude