') + ')', '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(plugin-gantt,plugin-tree): drop the undeclared `referenceTo` arm from both target chains by os-sam · Pull Request #6922 · objectstack-ai/objectui · GitHub
Skip to content

fix(plugin-gantt,plugin-tree): drop the undeclared referenceTo arm from both target chains - #6922

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6837-gantt-tree-referenceto-arms
Aug 30, 2026
Merged

fix(plugin-gantt,plugin-tree): drop the undeclared referenceTo arm from both target chains#6922
os-sam merged 1 commit into
mainfrom
claude/issue-6837-gantt-tree-referenceto-arms

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Refs #6837

Second slice of the classification table's suggested slicing, item 1: the
referenceTo third arms at ObjectGantt and ObjectTree. ⛔ Nothing else — not
Class B, not the pass-through forwarders, not reference_field, not the other
~16 readers. The card stays open.

ObjectGantt.tsx:1128 fd?.reference_to ?? fd?.reference ?? fd?.referenceTo
→ fd?.reference_to ?? fd?.reference
ObjectTree.tsx:132 def?.reference || def?.reference_to || def?.referenceTo
→ def?.reference || def?.reference_to

Both readers were re-located by text on bf97b98c8; both were still at the
line numbers the card cites (the table had already found two other readers whose
numbers had shifted, so this was not assumed).


1. The accept/reject delta, stated so it does not have to be re-derived

This is a reject-direction change reachable by third-party DataSource
implementations
— same clause-② routing as PR #6920, and for the same measured
reason: the ingestion door is not total. normalizeSchemaReferenceKeys has
two production call sites, making it a private property of two implementations,
while getObjectSchema is a required, non-optional member of the published
DataSource interface (types/src/data.ts:519) — and both components here call
it on the genericdataSource:

componenthow the def arrivesreachable raw?
ObjectGantteffectiveDataSource.getObjectSchema(resource) at :706YES
ObjectTreeuseSettledSchema(schemaKey, dataSource)dataSource.getObjectSchema(key) (react/src/hooks/useSettledSchema.ts:141)YES

What stops being accepted: a field def carrying onlyreferenceTo — and
which did not pass the ingestion choke point — no longer resolves a
relationship target at these two seams.

What that looks like, concretely, rather than abstractly:

seambeforeafter
gantt quick filterfetches the referenced object's full option domainfalls back to the distinct values present in the loaded rows
tree parent pointerfield is auto-detected as the single-parent pointernot detected; records render as a flat forest unless parentField is set explicitly

What is NOT affected:

2. Why this arm, and only this arm, is licensed

referenceTo clears the same bar the first slice (PR #6920) cleared for
RecordDetailDrawer:

  • Refused by contract, by name. Two-directional probe against this branch's
    installed @objectstack/spec 17.2.0, via @objectstack/spec/data:

    probeverdict
    reference: 'crm_account'ACCEPT
    reference_to: 'crm_account'REFUSE — unrecognized_keys, "Did you mean reference_toreference?"
    referenceTo: 'crm_account'REFUSE — unrecognized_keys, "Did you mean referenceToreference?"

    The alias entry is a rename hint attached to a refusal, not an acceptance:
    the spec names referenceTo explicitly in order to refuse it.

  • Stripped by the read door.referenceTo is a tombstone in
    RETIRED_FIELD_KEY_TOMBSTONES
    (types/src/internal/retired-field-keys.ts:146, retiredBy: 'objectui#6041',
    specEquivalent: 'reference') with all three strip sites true.

  • Zero producers in the cell, against hot controls — §3.

3. The producer census — re-derived at this base, not inherited

⛔ The table's numbers were not inherited. A separately written structure
walk was run at bf97b98c8: TypeScript compiler API over every tracked
.ts/.tsx plus parsed JSON, recording each hit's ancestor property chain,
counting emit positions only (PropertyAssignment /
ShorthandPropertyAssignment). A PropertyAccessExpression such as
fd.referenceTo is a read and is never counted as a producer; a
PropertySignature is a declaration and is bucketed separately (38 of those,
never counted as producers). Scanned 3808 TS/TSX + 754 JSON tracked files.

THE CELL = an emit inside a fields container — literally what these
components read, objectSchema.fields[name].

termrolerepo-wide emitsIN THE CELLstrict cell (field-def level)independent cell (enclosing type is reference-bearing)
referenceToSUBJECT81 / 42 files2 / 22 / 230 / 14
referenceCONTROL195 / 7692 / 3691 / 35156 / 59
reference_toCONTROL137 / 8852 / 3652 / 36101 / 65

Control discipline, both halves.

  1. The query ran. The controls are hot — 92 and 52 — in the very cells
    where the subject collapses to 2, extracted by the same pass.
  2. The question was right. Subject and control were extracted by the same
    pass, from the same cells, in the same units
    (emitted keys on a field
    definition), so a mis-posed cell would have moved subject and control
    together instead of separating 92-to-2. The cell was computed two ways
    (ancestor chain contains fields; and strictly at field-def level) and they
    agree on all three terms.

Third check, the one this key affords:referenceTo is not a term the
scanner cannot see — it is hot repo-wide at 81 emits across 42 files, and
collapses to 2 only under the cell restriction. The near-zero is produced by the
restriction, not by scanner blindness. Attributing all 27 non-test emits:
action params (resolveActionParams.ts), designer drafts (FieldDesigner.tsx,
MetadataFieldsPage.tsx), dataset inspectors, the report field schema
(reports.zod.ts:69), list-view column meta and filter-condition meta, plus 10
i18n locale strings. Not one is in the cell, and not one is in either package
this PR touches.

The 2 in-cell hits are negative fixtures of the retirement machinery, read
and confirmed rather than assumed:
app-shell/.../object-fields-io.spec-keys.test.ts:235 and
plugin-designer/src/MetadataFieldsPage.specKeyReference.test.tsx:75 both poison
a draft with the retired key precisely to assert ObjectSchema.safeParse
succeeds after the read door strips it. A fixture asserting removal is not a
producer.

⭐ Seam-local control — the half a repo-wide pass does not give you

The dispatch asked for confirmation that these two readers have no in-cell
referenceTo producer, so the census was also attributed per package:

packagereferenceTo emits (any position, any cell)surviving spellings in its own fixtures
plugin-gantt0reference_to at ObjectGantt.quickfilter.test.tsx:251, demo/main.tsx:334-335; reference at :306
plugin-tree0reference ×6, all in a fields container: ObjectTree.fieldFormatting.test.tsx:51-52, ObjectTree.settledSchemaKeying-6481.test.tsx:62,63,70,71

⇒ The corpus that actually feeds these two readers is hot on what survives and
empty on what goes
. The zero here is a local measurement with a local control,
not an extrapolation from the repo-wide one.

⛔ The hard limitation, restated so this PR is not read as more than it is

This licence rests on the key being contract-refused by name and actively
stripped by the read door
— ⛔ not on a production-only census. The
table's ceiling still stands and is not stepped over here:

the census cannot distinguish "no production object-metadata producer of
reference_to exists" from "this repo contains no production object-metadata
documents at all" — restricting the cell to production files collapses the
control too (in-cell non-test reference = 1, reference_to = 2; a
2-versus-1 comparison is not a measurement). This repo is a UI library, not
a metadata-app repo.

⇒ That is exactly why reference_to is untouched here and referenceTo is not.
The argument does not carry to reference_to, and this PR does not extend it.

4. The pins — the published form, copied not invented

ObjectGantt.referenceArms-6837.test.tsx and
ObjectTree.referenceArms-6837.test.tsx follow the five-element form PR #6916
published and PR #6920 copied: header carrying the measurement rather than
the conclusion; the live arms pinned in the same file as the dead one; a
named refusal case per deleted key; the precedence-inversion element; and the
floor restated at the pin.

Element 4 — no inversion case exists, and none was fabricated. The deleted
arm sat at the END of both chains (the tree's is already canonical-first), so
it could never preempt a contract-carrying spelling. A
{ reference: 'projects', referenceTo: 'other' } case would resolve to
'projects' both before and after and would measure nothing. Stated in prose in
both files, per the form.

Element 2 matters here and is load-bearing: each file pins the live arms
beside the refusal, plus a degenerate-pass guard — otherwise a reader that simply
stopped resolving anything would pass the refusals too. The settle signal in both
harnesses is deliberately spelling-independent: buildExpandFields decides
$expand from the field's type alone ("the reference / reference_to target
is irrelevant to the decision"), so a find carrying $expand proves the
component consumed the schema — for the refusal probe just as much as for the
live-arm ones. A refusal that passed because the schema never arrived cannot hide
behind it.

5. Verification

Module resolution, stated first: both pins import their component by
relative source path (./ObjectGantt, ./ObjectTree) and @object-ui/core
is aliased by the root vitest.config.mts to packages/core/src. Both legs
resolve to SOURCE — no package exports hop, no dist, and therefore no
rebuild leg
to get wrong.

Red first

treetestsresult
base source + the new pins123 failed / 9 passed
after the change1212 passed

The 3 failures were exactly the refusal assertions — gantt "does NOT read
referenceTo", gantt "degrades to the distinct loaded values", tree "does NOT
read referenceTo" — with all 9 live-arm, degenerate-guard and
through-the-normalizer cases green.

Ablation — the FACT mutated, never the assertion

Run from the committed tree (d812dadb8), so the restore leg has a real
reference. Predictions were written before running; all four matched.

legmutationpredictedmeasured
controlnone12 passed12 passed
Arestore the third arm in gantt2 failed / 10 passed2 failed / 10 passed
Brestore the third arm in tree1 failed / 11 passed1 failed / 11 passed
Crestore both (= the pre-change source)3 failed / 9 passed3 failed / 9 passed

Leg B is the informative one: the tree's degenerate-pass guard stayed green
under the mutation, which is what makes it a control rather than a duplicate of
the pin.

Mutation proven on disk per leg — anchored counts (deleted-text 1→0,
injected-text 0→1; an anchor miss aborts the leg with exit 7 rather than silently
no-opping) andgit hash-object diverging from the HEAD blob:
gantt dc20b2f0857b3324, tree 568c4d35beca27c1.

Restore proven both ways per leg, scoped to the mutated paths only
git diff HEAD -- path empty (measured in bytes, not by exit code) and
git hash-object equal to the HEAD blob. Restore is pinned to
git checkout HEAD -- path, never the bare form, which restores from the index
a path-scoped checkout has itself written. Whole-tree git status --short = 0
bytes
at the end. Absolute paths from git rev-parse --show-toplevel, under a
trap ... EXIT INT TERM.

Suites, types, lint, gates

  • Package suites:plugin-gantt + plugin-tree60 files / 462 tests
    passed
    . Run from the repo root against explicit paths (a package-cwd run and
    a zero-match filter are both refused by scripts/vitest-invocation-guard.mjs).
  • Type-check, both packages, exit 0 with the script echoed
    (tsc --noEmit && tsc -p tsconfig.test.json) so it is not a zero-match false
    green. The dependency closure was built first (25 packages, exit 0) —
    tsconfig.test.json sets paths: {} so it resolves workspace deps through
    built .d.ts, and an unbuilt closure would have been a false RED (TS2307),
    not a finding.
  • --listFiles, MEASURED both halves: each pin is a program input 1 time
    under tsconfig.test.json and 0 times under the build project — the build
    tsconfig.json excludes the test glob, so only the test project can cover
    it
    . Each edited component is an input 1 time in both projects.
  • Lint, compared against the BASE version of the same files — not a bare
    count. Base blobs 677650a2 / 94ba6dce vs branch blobs dc20b2f0 /
    568c4d35, counts from --format json: 0 errors / 113 warnings, identical
    in both directions
    , with a byte-identical per-rule breakdown. Blob ids
    verified in both directions and the restore proven by an empty git status.
    The two new pins have no base version and are reported standalone: 0 errors /
    9 warnings
    , all @typescript-eslint/no-explicit-any — the same rule and
    class as the drawer pin from PR fix(plugin-detail): drop the two undeclared arms from RecordDetailDrawer's relationship-target chain #6920 (6 of the same).
  • The real package lint gates, not a narrowing:
    @object-ui/plugin-gantteslint .exit 0 (0 errors, 309 warnings) and
    @object-ui/plugin-treeeslint .exit 0 (0 errors, 53 warnings).
  • Gates, each quoted by its own verdict line, all exit 0:
    check:control-bytes ("scanned 5777 tracked text file(s)"),
    check:vi-mock-specifiers, check:vi-mock-inherit, check:self-import,
    check:esm-specifiers, check:phantom-deps, check:designer-field-key-parity,
    check:element-data-source-declaration, check:side-effects-array,
    check-changeset-no-major, check-changeset-presence ("4 source file(s) of 2
    released package(s) changed, and this change declares 1 changeset(s)"),
    check-changeset-fixed, check-changeset-overwrite, check-lint-coverage
    (46/46), check-type-check-coverage (45/46 + 41/41 test projects).

NOT MEASURED — reported as neither red nor green:

  • check:sdui-registration-pins exits 2 and says so itself: "This is exit 2,
    not a pass: ... a run with nothing to read has measured nothing. Build the
    console first"
    . No console build exists in this worktree. The diff touches no
    registration array.
  • check:spec-floors exits 1 with no-artifact and prints its own remedy
    ("Build the workspace before running this gate"). This worktree built a
    25-package closure, not the whole workspace. The diff adds no spec import and
    moves no dependency range.

Declared narrowing: repo-wide eslint . and the rest of the gate farm were
not run locally; CI runs the farm once regardless. The narrowing is bounded, not
assumed: eslint.config.js contains no projectService and no project:
key
, so type-aware linting is not enabled and this diff cannot move the verdict
of any file it does not touch. Both packages it does touch were linted in full
by their own gate.

Shared verify lock: every acquisition waited 0s; longest hold 88s (the
two package suites). Reported as a measurement, per the dispatch.

All figures above are from the final commit d812dadb8, with git status
clean.

6. Scope, and the boundary that was not reached

Refs, never Fixes.#6837 has ~18 readers of deferred scope and must
stay open; #6584 lost a decision's home for four days by closing on a first
slice.

The stop-and-report boundary — "if either file's seam turns out to differ, STOP
AND REPORT rather than deleting"
was not reached. Both seams matched the
table exactly: contract (a), fed by a generic dataSource.getObjectSchema,
reachable raw, three spellings read, only reference declared by the contract
fed. One refinement worth recording rather than silently correcting: the table
lists ObjectTree's def provenance as an "objectSchema param". It is more
precisely useSettledSchema(schemaKey, dataSource), which calls
dataSource.getObjectSchema itself. That makes it more clearly reachable
raw, not less — the classification is unchanged.

⛔ Deliberately untouched, and still #6837's open scope: the reference_to arms
across Class A (blocked on a corpus this repo cannot provide), Class B rows
18-20 (both spellings have named in-repo producers), the two pass-through
forwarders at plugin-grid/ObjectGrid.tsx:465 and
plugin-dashboard/recordFields.tsx:133, and
RecordDetailDrawer.tsx:301's reference_field ?? referenceField category
error.

Generated by Claude Code


Generated by Claude Code

…from both target chains
`ObjectGantt`'s quick-filter option fetch and `ObjectTree`'s `detectParentField`
each resolved a relationship target through three spellings:
fd?.reference_to ?? fd?.reference ?? fd?.referenceTo
def?.reference || def?.reference_to || def?.referenceTo
No contract declares the third. `@objectstack/spec` 17.2.0's `FieldSchema`
refuses `referenceTo` by name with `unrecognized_keys`, carrying its own "Did
you mean `referenceTo` -> `reference`?" rename; it is also a tombstone in
`RETIRED_FIELD_KEY_TOMBSTONES` (objectui#6041) at all three strip sites, so the
designer read door removes it before a draft round-trips. So the arm was not a
redundant fallback but invented tolerance surface -- a silent absorption point
for a producer that ought to fail visibly (AGENTS.md #0.1).
A repo-wide structure-walk producer census (TypeScript compiler API over every
tracked source plus parsed JSON, recording each hit's ancestor property chain,
emit positions only) found `referenceTo` at 2 hits in the cell these components
read -- both of them negative fixtures of the retirement machinery, asserting
that the read door strips the key -- against controls `reference` at 92 and
`reference_to` at 52 in the same cells from the same pass. Neither
`plugin-gantt` nor `plugin-tree` emits `referenceTo` anywhere, while both
packages' own fixtures are hot on the surviving spellings.
The two surviving arms are deliberately untouched: choosing between
`reference_to` and `reference` per reader is objectui#6837's open scope.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BPHEWE5c.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.40KB62.44KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

CONTRACT_REVIEW_TIER: ACCEPT

Reviewed at head d812dadb8 against base bf97b98c8 (= origin/main), in a dedicated detached worktree, removed after review. Clause-② routing (reject-direction change reachable raw by third-party DataSource implementations) was settled at this tier on PR #6920; I sanity-checked its two premises rather than re-litigating — getObjectSchema is a required, non-optional member at types/src/data.ts:519, and normalizeSchemaReferenceKeys has exactly two production call sites (MetadataProvider.tsx:657, data-objectstack/src/index.ts:3753). Both hold.

The crux: the narrower licence holds, and nothing leaks toward reference_to

The licence claimed is conjunctive — referenceTo is (i) refused by name by the contract these seams are fed, (ii) stripped by the read door, (iii) a seam-local zero against hot controls. I checked each leg against the question the classification table's ceiling exists to guard: would this step equally license deleting a reference_to arm?

  • (i) alone would leak — my probe shows FieldSchema refuses reference_to by name too. But the PR never uses (i) alone.
  • (ii) does not leak: RETIRED_FIELD_KEY_TOMBSTONES contains referenceTo (retiredBy: 'objectui#6041', specEquivalent: 'reference', all three strip sites true) and does not contain reference_to, which is instead a live declared key of DetailViewFieldSchema (views.zod.ts:49).
  • (iii) actively contradicts a reference_to deletion: the same package-local query that returns zero referenceTo emits returns hot reference_to fixtures feeding these very readers — deleting that arm would flip the packages' own suites red.

The PR restates the table's hard ceiling verbatim, does not rest on a production-only census, and both pins carry the explicit "what this does NOT rest on" disclaimer. No step in the argument extends to reference_to.

Re-derived / reproduced (not taken on report)

  1. Scope — exactly 5 files (changeset, 2 sources, 2 pins). Each source diff deletes only the trailing referenceTo arm plus a comment; the surviving reference_to / reference arms and every other spelling-bearing line in both files are byte-identical to base (blob ids match the PR's: gantt 677650a23dc20b2f03, tree 94ba6dce7568c4d355). Class B, the two forwarders (ObjectGrid.tsx:465, recordFields.tsx:133), reference_field, and the other readers: untouched. Refs #6837 only; finding(objectui): ~20 more relationship-target readers still run a legacy-first reference_to chain the ingestion normalizer already made redundant #6837's closed_by_pull_requests = 0.
  2. ⭐ Seam-local control — re-derived at base with an independent query, and it is slightly stronger than stated: referenceTo appears in plugin-gantt exactly twice (one comment, the read itself) and in plugin-tree exactly once (the read itself) — zero emits at any position, not merely zero in-cell — while the same query returns the hot controls at the exact claimed lines (quickfilter.test.tsx:251, demo/main.tsx:334-335, :306; tree reference ×6, each verified inside a fields container). Both halves: the query ran, on the join.
  3. Tombstone + spec probe — tombstone entry verified at retired-field-keys.ts:146 as claimed. Two-directional probe re-run against installed @objectstack/spec 17.2.0: reference ACCEPT; reference_to REFUSE unrecognized_keys; referenceTo REFUSE unrecognized_keys with the "Did you mean referenceToreference?" rename hint. Reproduced verbatim.
  4. Ablation — control re-run: 12/12. Leg B re-run (tree arm restored): 1 failed / 11 passed, the failure being exactly the tree refusal, and the tree's degenerate-pass guard stayed GREEN under the mutation — it is a genuine control, not a duplicate of the pin. Leg C re-run (both arms restored = pre-change source): 3 failed / 9 passed, exactly the three named tests — which also corroborates the red-first 3/9 claim. Mutations proven by anchored counts (1→0 / 0→1) and blob divergence; my minimal mutations produced blobs beca27c1 / 857b3324, byte-identical to the PR's reported mutated blobs. Restores proven both ways scoped to the mutated paths (git diff HEAD -- path = 0 bytes, blob = HEAD blob), whole-tree git status = 0 bytes.
  5. Pin instrument — all five elements present in both files; the form's source (RecordDetailDrawer.referenceArms-6837.test.tsx) exists on main. Element 2's settle signal is genuinely spelling-independent: isExpandableFieldDef (core/src/utils/expand-fields.ts:127-136) reads only the field's type. Element 4: verified at base that referenceTo sat last in both chains, so no inversion case exists and none was fabricated. The through-the-normalizer case's premise verified at reference-keys.ts:29-32.
  6. --listFiles, both halves — closure built first (exit 0), then all four type-check legs exit 0, then measured: each pin is a program input under tsconfig.test.json and under the build project; each edited component in both. Only the test project can cover the pins, as claimed.
  7. Package suites — re-run from the repo root with explicit paths through the root config (per the invocation guard): 60 files / 462 tests passed, matching the PR.
  8. The recorded refinement — verified: base ObjectTree.tsx:396 is useSettledSchema<any>(schemaKey, dataSource), and the hook itself calls dataSource.getObjectSchema(settleKey) (useSettledSchema.ts:~140). More clearly reachable raw; classification unchanged; recorded rather than silently corrected. Also verified the two in-cell referenceTo census hits are the claimed negative fixtures poisoning drafts to assert the strip.

NOT MEASURED, restated as neither red nor green (not converted to a colour here either): check:sdui-registration-pins (exit 2, no console build) and check:spec-floors (exit 1 no-artifact). CI on d812dadb8 is fully green (all four test shards, Type Check, Lint, gate farm), which covers the repo-wide half neither of us ran locally.

Shared verify lock, as a measurement: six acquisitions, every wait ≤ 0.005s; longest hold ~92s (the two package suites).

Non-blocking notes (no follow-up required)

  • The pins'/changeset's headline "the arm NO CONTRACT DECLARES" is seam-scoped shorthand: report columns (reports.zod.ts:69) and designer fields (designer.ts:838) do declare referenceTo for their document kinds. The operative claim — the contract fed to these two readers declares only reference — is correct, and pin §4 states the full tier-boundary fact accurately. No action needed; noted so the headline is not quoted out of seam.
  • The PR's sweep-C (independent cell) counts differ from the classification table's by 1 (reference 156 vs 157, referenceTo 30 vs 31) while the load-bearing sweep-B cells and controls match exactly — consistent with a genuinely re-derived walker rather than inherited numbers.

The narrower licence holds; the slice stays inside it; #6837 correctly stays open.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude