[Decision] Two SOURCE registrars disagree on a view container whose data.name differs from the derived key — one silently rewrites the author's field, the other hard-fails the whole artifact load #14666

Description

@os-musk

Found while implementing #14399 (PR #14665), which moved the ObjectQL boot loop onto the shared deriveViewContainerObject. Filed unassigned; not fixed there — it is a different site, and picking a direction is a decision rather than a mechanical repair. Re-measured by the triage seat at origin/main75adf11 (2026-09-02, R+105), after PR #14665 landed.

The reading

Both SOURCE registrars derive the same binding for an aggregated view container. They do not agree on what to do when the container's own name field disagrees with that derived key.

  • packages/objectql/src/engine.ts:5149registerMetadataCollections reconciles it, silently:
    const toRegister = item.name === itemName ? item : { ...item, name: itemName };
    The stored document's name is overwritten with the derived object key. The author's field is discarded without a diagnostic.

  • packages/metadata/src/plugin.ts:1127_parseAndRegisterArtifact does not: it calls this.manager.register('view', viewObject, item, { notify: false }) with the item unchanged, so assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts:170-172, MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1) refuses:

    IMetadataService.register('view', 'crm_lead'): data.name is 'lead_views', which disagrees with
    the name argument 'crm_lead'. ... (#7378 row 1: refuse loudly, locate the mismatch).
    

    VALIDATION_ERROR / 400, thrown out of _parseAndRegisterArtifact — the entry the boot artifact load and the HMR reload share, so the refusal is a hard failure of the whole artifact, not of the one container.

Reachable divergence

For { name: 'lead_views', object: 'crm_lead', list: { … } } — schema-legal at both doors, since ViewSchema declares an optional name (packages/spec/src/ui/view.zod.ts:3603):

  • through the boot loop: registers under crm_lead, with the document's name silently rewritten from lead_views to crm_lead;
  • through the artifact/HMR door: nothing registers and the artifact load throws.

Same document, two SOURCE registrars, and the outcomes are not merely different — one is a silent authorial rewrite and the other is a boot failure.

The candidate directions

  1. Make the artifact door reconcile too (mirror the boot loop's toRegister line). Cheap and makes both registrars accept the shape — but it spreads a SILENT rewrite of an author-written field, which is what MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1 exists to refuse.
  2. Make the boot loop refuse too, matching MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1. Consistent with the standing ruling and the safer default for AI-authored metadata, but it turns a currently-silent path into a loud one, and the boot loop's toRegister line predates the ruling and serves the ordinary no-name container as well — narrowing it correctly is not mechanical.
  3. Refuse ViewSchema.name outright for object-scoped containers, since its own .describe() says "for an object-scoped container it is the object name" — a convention no door enforces. This one moves a published packages/spec contract.

✅ Now pinned on main (R+104's caveat is discharged)

packages/objectql/src/view-container-divergent-name-registrars.test.tshas landed with PR #14665. It pins the current behaviour of both sides, including the refusal's code/status, so neither door can move without a red. Nothing asserts which direction is right — that is still what this card asks.

Nothing shipped moves today

Of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to. The latent form is what is bad — an author who writes one gets a silent rewrite or a boot failure depending on how their package is loaded.

<!-- os-decision-facets -->

四棱分析(分诊席出具,供裁决)

① 长期健全性(权重 ≥50%,主导本建议)—— 指向 2。
最深的缺陷不是「哪个方向对」,而是两个 SOURCE 注册器对同一份文档给出相反结局。无论裁哪个方向,两扇门必须收敛 —— 否则「这份元数据合不合法」取决于它是被 boot 还是被 artifact/HMR 加载的,而作者看不见这个区别。方向 1 尤其要小心:#7378 row 1 已是既有裁决,其原话就是「resolving it silently in either direction can file the item under a key the caller never wrote」。方向 1 把那条裁决明令拒绝的东西复制到第二扇门,是在既有裁决上开倒车。

② 真实业务拉力 —— 零,但潜伏形状是真的。
54 个非测试来源里 0 个声明了发散的 name。没有用户在等这个。⭐ 但也正因为零,方向 2 今天不会弄坏任何东西 —— 这是「把静默路径改响」通常要付而这里恰好不必付的代价。真正的成本在未来:第一个写出发散 name 的作者,拿到的是静默改写还是启动失败,取决于加载路径,从他的视角看是不确定的。

③ 防 AI 编码错误 —— 强烈指向 2。
name: 'lead_views' 恰恰是 AI 作者会写出来的东西:一个像样的、人类风格的容器名。静默改写意味着他永远学不到这条约定;响亮拒绝会指名不匹配的两个值并定位它 —— 那正是 #7378 row 1 的设计意图。⚠️ 附带一条:当前的发散还意味着穿一扇门的测试证明不了另一扇门的行为;PR #14665 的钉子已落地,两边现在都钉住了当前行为,但没有任何断言说哪个方向是对的

④ 创业阶段不增殖 —— 指向 2,反对 3。
方向 2 不新增概念,只是把一条已有裁决贯彻到第二处。方向 3 要在 packages/spec 加一条新的 schema 规则和一类新的拒绝,并动已发布契约 —— 那是本轴要避免的增殖,且触人类底线(published contract)。方向 1 不增殖但会固化「发散 name 会怎样」的两种拼法。

建议:2 —— 让 boot 循环也按 #7378 row 1 拒绝;⛔ 收敛必须只针对发散的情形,不得波及普通的无 name 容器(那正是卡面说「narrowing it correctly is not mechanical」的地方,也是实施的主要风险点)。

回退: 若维护者判断 boot 循环的静默对某条普通路径是承重的,则退为「两扇门都接受,但发散时记一条 warn 并指名两个值」—— 保留可加载性,同时终结无声改写。⛔ 三者之中唯一不可接受的是维持现状:两扇门对同一份文档给出相反结局,是本卡真正要消灭的东西。

⚠️ 置信缺口(必录): 我只测了本仓的 54 个非测试来源。没有测下游 —— objectui / hotcrm / 各 examples 是否有作者写出发散 name,我读不到(跨仓)。若下游存在这样的文档,方向 2 会把它们从「静默通过」变成「启动失败」,那就不再是零代价。裁决前应由 repo:objectui 与 hotcrm 侧各跑一次同样的计数。方向 3 是否可行同样取决于这个数字。

Related: #14399 · PR #14665(已落地)· #7378 · #13912

Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      [Decision] Two SOURCE registrars disagree on a view container whose data.name differs from the derived key — one silently rewrites the author's field, the other hard-fails the whole artifact load #14666

      Description

      @os-musk

      Found while implementing #14399 (PR #14665), which moved the ObjectQL boot loop onto the shared deriveViewContainerObject. Filed unassigned; not fixed there — it is a different site, and picking a direction is a decision rather than a mechanical repair. Re-measured by the triage seat at origin/main75adf11 (2026-09-02, R+105), after PR #14665 landed.

      The reading

      Both SOURCE registrars derive the same binding for an aggregated view container. They do not agree on what to do when the container's own name field disagrees with that derived key.

      • packages/objectql/src/engine.ts:5149registerMetadataCollections reconciles it, silently:
        const toRegister = item.name === itemName ? item : { ...item, name: itemName };
        The stored document's name is overwritten with the derived object key. The author's field is discarded without a diagnostic.

      • packages/metadata/src/plugin.ts:1127_parseAndRegisterArtifact does not: it calls this.manager.register('view', viewObject, item, { notify: false }) with the item unchanged, so assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts:170-172, MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1) refuses:

        IMetadataService.register('view', 'crm_lead'): data.name is 'lead_views', which disagrees with
        the name argument 'crm_lead'. ... (#7378 row 1: refuse loudly, locate the mismatch).
        

        VALIDATION_ERROR / 400, thrown out of _parseAndRegisterArtifact — the entry the boot artifact load and the HMR reload share, so the refusal is a hard failure of the whole artifact, not of the one container.

      Reachable divergence

      For { name: 'lead_views', object: 'crm_lead', list: { … } } — schema-legal at both doors, since ViewSchema declares an optional name (packages/spec/src/ui/view.zod.ts:3603):

      • through the boot loop: registers under crm_lead, with the document's name silently rewritten from lead_views to crm_lead;
      • through the artifact/HMR door: nothing registers and the artifact load throws.

      Same document, two SOURCE registrars, and the outcomes are not merely different — one is a silent authorial rewrite and the other is a boot failure.

      The candidate directions

      1. Make the artifact door reconcile too (mirror the boot loop's toRegister line). Cheap and makes both registrars accept the shape — but it spreads a SILENT rewrite of an author-written field, which is what MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1 exists to refuse.
      2. Make the boot loop refuse too, matching MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1. Consistent with the standing ruling and the safer default for AI-authored metadata, but it turns a currently-silent path into a loud one, and the boot loop's toRegister line predates the ruling and serves the ordinary no-name container as well — narrowing it correctly is not mechanical.
      3. Refuse ViewSchema.name outright for object-scoped containers, since its own .describe() says "for an object-scoped container it is the object name" — a convention no door enforces. This one moves a published packages/spec contract.

      ✅ Now pinned on main (R+104's caveat is discharged)

      packages/objectql/src/view-container-divergent-name-registrars.test.tshas landed with PR #14665. It pins the current behaviour of both sides, including the refusal's code/status, so neither door can move without a red. Nothing asserts which direction is right — that is still what this card asks.

      Nothing shipped moves today

      Of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to. The latent form is what is bad — an author who writes one gets a silent rewrite or a boot failure depending on how their package is loaded.

      <!-- os-decision-facets -->

      四棱分析(分诊席出具,供裁决)

      ① 长期健全性(权重 ≥50%,主导本建议)—— 指向 2。
      最深的缺陷不是「哪个方向对」,而是两个 SOURCE 注册器对同一份文档给出相反结局。无论裁哪个方向,两扇门必须收敛 —— 否则「这份元数据合不合法」取决于它是被 boot 还是被 artifact/HMR 加载的,而作者看不见这个区别。方向 1 尤其要小心:#7378 row 1 已是既有裁决,其原话就是「resolving it silently in either direction can file the item under a key the caller never wrote」。方向 1 把那条裁决明令拒绝的东西复制到第二扇门,是在既有裁决上开倒车。

      ② 真实业务拉力 —— 零,但潜伏形状是真的。
      54 个非测试来源里 0 个声明了发散的 name。没有用户在等这个。⭐ 但也正因为零,方向 2 今天不会弄坏任何东西 —— 这是「把静默路径改响」通常要付而这里恰好不必付的代价。真正的成本在未来:第一个写出发散 name 的作者,拿到的是静默改写还是启动失败,取决于加载路径,从他的视角看是不确定的。

      ③ 防 AI 编码错误 —— 强烈指向 2。
      name: 'lead_views' 恰恰是 AI 作者会写出来的东西:一个像样的、人类风格的容器名。静默改写意味着他永远学不到这条约定;响亮拒绝会指名不匹配的两个值并定位它 —— 那正是 #7378 row 1 的设计意图。⚠️ 附带一条:当前的发散还意味着穿一扇门的测试证明不了另一扇门的行为;PR #14665 的钉子已落地,两边现在都钉住了当前行为,但没有任何断言说哪个方向是对的

      ④ 创业阶段不增殖 —— 指向 2,反对 3。
      方向 2 不新增概念,只是把一条已有裁决贯彻到第二处。方向 3 要在 packages/spec 加一条新的 schema 规则和一类新的拒绝,并动已发布契约 —— 那是本轴要避免的增殖,且触人类底线(published contract)。方向 1 不增殖但会固化「发散 name 会怎样」的两种拼法。

      建议:2 —— 让 boot 循环也按 #7378 row 1 拒绝;⛔ 收敛必须只针对发散的情形,不得波及普通的无 name 容器(那正是卡面说「narrowing it correctly is not mechanical」的地方,也是实施的主要风险点)。

      回退: 若维护者判断 boot 循环的静默对某条普通路径是承重的,则退为「两扇门都接受,但发散时记一条 warn 并指名两个值」—— 保留可加载性,同时终结无声改写。⛔ 三者之中唯一不可接受的是维持现状:两扇门对同一份文档给出相反结局,是本卡真正要消灭的东西。

      ⚠️ 置信缺口(必录): 我只测了本仓的 54 个非测试来源。没有测下游 —— objectui / hotcrm / 各 examples 是否有作者写出发散 name,我读不到(跨仓)。若下游存在这样的文档,方向 2 会把它们从「静默通过」变成「启动失败」,那就不再是零代价。裁决前应由 repo:objectui 与 hotcrm 侧各跑一次同样的计数。方向 3 是否可行同样取决于这个数字。

      Related: #14399 · PR #14665(已落地)· #7378 · #13912

      Generated by Claude Code

      Metadata

      Metadata

      Assignees

      No one assigned

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          [Decision] Two SOURCE registrars disagree on a view container whose data.name differs from the derived key — one silently rewrites the author's field, the other hard-fails the whole artifact load #14666

          Description

          @os-musk

          Found while implementing #14399 (PR #14665), which moved the ObjectQL boot loop onto the shared deriveViewContainerObject. Filed unassigned; not fixed there — it is a different site, and picking a direction is a decision rather than a mechanical repair. Re-measured by the triage seat at origin/main75adf11 (2026-09-02, R+105), after PR #14665 landed.

          The reading

          Both SOURCE registrars derive the same binding for an aggregated view container. They do not agree on what to do when the container's own name field disagrees with that derived key.

          • packages/objectql/src/engine.ts:5149registerMetadataCollections reconciles it, silently:
            const toRegister = item.name === itemName ? item : { ...item, name: itemName };
            The stored document's name is overwritten with the derived object key. The author's field is discarded without a diagnostic.

          • packages/metadata/src/plugin.ts:1127_parseAndRegisterArtifact does not: it calls this.manager.register('view', viewObject, item, { notify: false }) with the item unchanged, so assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts:170-172, MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1) refuses:

            IMetadataService.register('view', 'crm_lead'): data.name is 'lead_views', which disagrees with
            the name argument 'crm_lead'. ... (#7378 row 1: refuse loudly, locate the mismatch).
            

            VALIDATION_ERROR / 400, thrown out of _parseAndRegisterArtifact — the entry the boot artifact load and the HMR reload share, so the refusal is a hard failure of the whole artifact, not of the one container.

          Reachable divergence

          For { name: 'lead_views', object: 'crm_lead', list: { … } } — schema-legal at both doors, since ViewSchema declares an optional name (packages/spec/src/ui/view.zod.ts:3603):

          • through the boot loop: registers under crm_lead, with the document's name silently rewritten from lead_views to crm_lead;
          • through the artifact/HMR door: nothing registers and the artifact load throws.

          Same document, two SOURCE registrars, and the outcomes are not merely different — one is a silent authorial rewrite and the other is a boot failure.

          The candidate directions

          1. Make the artifact door reconcile too (mirror the boot loop's toRegister line). Cheap and makes both registrars accept the shape — but it spreads a SILENT rewrite of an author-written field, which is what MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1 exists to refuse.
          2. Make the boot loop refuse too, matching MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1. Consistent with the standing ruling and the safer default for AI-authored metadata, but it turns a currently-silent path into a loud one, and the boot loop's toRegister line predates the ruling and serves the ordinary no-name container as well — narrowing it correctly is not mechanical.
          3. Refuse ViewSchema.name outright for object-scoped containers, since its own .describe() says "for an object-scoped container it is the object name" — a convention no door enforces. This one moves a published packages/spec contract.

          ✅ Now pinned on main (R+104's caveat is discharged)

          packages/objectql/src/view-container-divergent-name-registrars.test.tshas landed with PR #14665. It pins the current behaviour of both sides, including the refusal's code/status, so neither door can move without a red. Nothing asserts which direction is right — that is still what this card asks.

          Nothing shipped moves today

          Of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to. The latent form is what is bad — an author who writes one gets a silent rewrite or a boot failure depending on how their package is loaded.

          <!-- os-decision-facets -->

          四棱分析(分诊席出具,供裁决)

          ① 长期健全性(权重 ≥50%,主导本建议)—— 指向 2。
          最深的缺陷不是「哪个方向对」,而是两个 SOURCE 注册器对同一份文档给出相反结局。无论裁哪个方向,两扇门必须收敛 —— 否则「这份元数据合不合法」取决于它是被 boot 还是被 artifact/HMR 加载的,而作者看不见这个区别。方向 1 尤其要小心:#7378 row 1 已是既有裁决,其原话就是「resolving it silently in either direction can file the item under a key the caller never wrote」。方向 1 把那条裁决明令拒绝的东西复制到第二扇门,是在既有裁决上开倒车。

          ② 真实业务拉力 —— 零,但潜伏形状是真的。
          54 个非测试来源里 0 个声明了发散的 name。没有用户在等这个。⭐ 但也正因为零,方向 2 今天不会弄坏任何东西 —— 这是「把静默路径改响」通常要付而这里恰好不必付的代价。真正的成本在未来:第一个写出发散 name 的作者,拿到的是静默改写还是启动失败,取决于加载路径,从他的视角看是不确定的。

          ③ 防 AI 编码错误 —— 强烈指向 2。
          name: 'lead_views' 恰恰是 AI 作者会写出来的东西:一个像样的、人类风格的容器名。静默改写意味着他永远学不到这条约定;响亮拒绝会指名不匹配的两个值并定位它 —— 那正是 #7378 row 1 的设计意图。⚠️ 附带一条:当前的发散还意味着穿一扇门的测试证明不了另一扇门的行为;PR #14665 的钉子已落地,两边现在都钉住了当前行为,但没有任何断言说哪个方向是对的

          ④ 创业阶段不增殖 —— 指向 2,反对 3。
          方向 2 不新增概念,只是把一条已有裁决贯彻到第二处。方向 3 要在 packages/spec 加一条新的 schema 规则和一类新的拒绝,并动已发布契约 —— 那是本轴要避免的增殖,且触人类底线(published contract)。方向 1 不增殖但会固化「发散 name 会怎样」的两种拼法。

          建议:2 —— 让 boot 循环也按 #7378 row 1 拒绝;⛔ 收敛必须只针对发散的情形,不得波及普通的无 name 容器(那正是卡面说「narrowing it correctly is not mechanical」的地方,也是实施的主要风险点)。

          回退: 若维护者判断 boot 循环的静默对某条普通路径是承重的,则退为「两扇门都接受,但发散时记一条 warn 并指名两个值」—— 保留可加载性,同时终结无声改写。⛔ 三者之中唯一不可接受的是维持现状:两扇门对同一份文档给出相反结局,是本卡真正要消灭的东西。

          ⚠️ 置信缺口(必录): 我只测了本仓的 54 个非测试来源。没有测下游 —— objectui / hotcrm / 各 examples 是否有作者写出发散 name,我读不到(跨仓)。若下游存在这样的文档,方向 2 会把它们从「静默通过」变成「启动失败」,那就不再是零代价。裁决前应由 repo:objectui 与 hotcrm 侧各跑一次同样的计数。方向 3 是否可行同样取决于这个数字。

          Related: #14399 · PR #14665(已落地)· #7378 · #13912

          Generated by Claude Code

          Metadata

          Metadata

          Assignees

          No one assigned

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [Decision] Two SOURCE registrars disagree on a view container whose data.name differs from the derived key — one silently rewrites the author's field, the other hard-fails the whole artifact load #14666

              Description

              @os-musk

              Found while implementing #14399 (PR #14665), which moved the ObjectQL boot loop onto the shared deriveViewContainerObject. Filed unassigned; not fixed there — it is a different site, and picking a direction is a decision rather than a mechanical repair. Re-measured by the triage seat at origin/main75adf11 (2026-09-02, R+105), after PR #14665 landed.

              The reading

              Both SOURCE registrars derive the same binding for an aggregated view container. They do not agree on what to do when the container's own name field disagrees with that derived key.

              • packages/objectql/src/engine.ts:5149registerMetadataCollections reconciles it, silently:
                const toRegister = item.name === itemName ? item : { ...item, name: itemName };
                The stored document's name is overwritten with the derived object key. The author's field is discarded without a diagnostic.

              • packages/metadata/src/plugin.ts:1127_parseAndRegisterArtifact does not: it calls this.manager.register('view', viewObject, item, { notify: false }) with the item unchanged, so assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts:170-172, MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1) refuses:

                IMetadataService.register('view', 'crm_lead'): data.name is 'lead_views', which disagrees with
                the name argument 'crm_lead'. ... (#7378 row 1: refuse loudly, locate the mismatch).
                

                VALIDATION_ERROR / 400, thrown out of _parseAndRegisterArtifact — the entry the boot artifact load and the HMR reload share, so the refusal is a hard failure of the whole artifact, not of the one container.

              Reachable divergence

              For { name: 'lead_views', object: 'crm_lead', list: { … } } — schema-legal at both doors, since ViewSchema declares an optional name (packages/spec/src/ui/view.zod.ts:3603):

              • through the boot loop: registers under crm_lead, with the document's name silently rewritten from lead_views to crm_lead;
              • through the artifact/HMR door: nothing registers and the artifact load throws.

              Same document, two SOURCE registrars, and the outcomes are not merely different — one is a silent authorial rewrite and the other is a boot failure.

              The candidate directions

              1. Make the artifact door reconcile too (mirror the boot loop's toRegister line). Cheap and makes both registrars accept the shape — but it spreads a SILENT rewrite of an author-written field, which is what MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1 exists to refuse.
              2. Make the boot loop refuse too, matching MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1. Consistent with the standing ruling and the safer default for AI-authored metadata, but it turns a currently-silent path into a loud one, and the boot loop's toRegister line predates the ruling and serves the ordinary no-name container as well — narrowing it correctly is not mechanical.
              3. Refuse ViewSchema.name outright for object-scoped containers, since its own .describe() says "for an object-scoped container it is the object name" — a convention no door enforces. This one moves a published packages/spec contract.

              ✅ Now pinned on main (R+104's caveat is discharged)

              packages/objectql/src/view-container-divergent-name-registrars.test.tshas landed with PR #14665. It pins the current behaviour of both sides, including the refusal's code/status, so neither door can move without a red. Nothing asserts which direction is right — that is still what this card asks.

              Nothing shipped moves today

              Of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to. The latent form is what is bad — an author who writes one gets a silent rewrite or a boot failure depending on how their package is loaded.

              <!-- os-decision-facets -->

              四棱分析(分诊席出具,供裁决)

              ① 长期健全性(权重 ≥50%,主导本建议)—— 指向 2。
              最深的缺陷不是「哪个方向对」,而是两个 SOURCE 注册器对同一份文档给出相反结局。无论裁哪个方向,两扇门必须收敛 —— 否则「这份元数据合不合法」取决于它是被 boot 还是被 artifact/HMR 加载的,而作者看不见这个区别。方向 1 尤其要小心:#7378 row 1 已是既有裁决,其原话就是「resolving it silently in either direction can file the item under a key the caller never wrote」。方向 1 把那条裁决明令拒绝的东西复制到第二扇门,是在既有裁决上开倒车。

              ② 真实业务拉力 —— 零,但潜伏形状是真的。
              54 个非测试来源里 0 个声明了发散的 name。没有用户在等这个。⭐ 但也正因为零,方向 2 今天不会弄坏任何东西 —— 这是「把静默路径改响」通常要付而这里恰好不必付的代价。真正的成本在未来:第一个写出发散 name 的作者,拿到的是静默改写还是启动失败,取决于加载路径,从他的视角看是不确定的。

              ③ 防 AI 编码错误 —— 强烈指向 2。
              name: 'lead_views' 恰恰是 AI 作者会写出来的东西:一个像样的、人类风格的容器名。静默改写意味着他永远学不到这条约定;响亮拒绝会指名不匹配的两个值并定位它 —— 那正是 #7378 row 1 的设计意图。⚠️ 附带一条:当前的发散还意味着穿一扇门的测试证明不了另一扇门的行为;PR #14665 的钉子已落地,两边现在都钉住了当前行为,但没有任何断言说哪个方向是对的

              ④ 创业阶段不增殖 —— 指向 2,反对 3。
              方向 2 不新增概念,只是把一条已有裁决贯彻到第二处。方向 3 要在 packages/spec 加一条新的 schema 规则和一类新的拒绝,并动已发布契约 —— 那是本轴要避免的增殖,且触人类底线(published contract)。方向 1 不增殖但会固化「发散 name 会怎样」的两种拼法。

              建议:2 —— 让 boot 循环也按 #7378 row 1 拒绝;⛔ 收敛必须只针对发散的情形,不得波及普通的无 name 容器(那正是卡面说「narrowing it correctly is not mechanical」的地方,也是实施的主要风险点)。

              回退: 若维护者判断 boot 循环的静默对某条普通路径是承重的,则退为「两扇门都接受,但发散时记一条 warn 并指名两个值」—— 保留可加载性,同时终结无声改写。⛔ 三者之中唯一不可接受的是维持现状:两扇门对同一份文档给出相反结局,是本卡真正要消灭的东西。

              ⚠️ 置信缺口(必录): 我只测了本仓的 54 个非测试来源。没有测下游 —— objectui / hotcrm / 各 examples 是否有作者写出发散 name,我读不到(跨仓)。若下游存在这样的文档,方向 2 会把它们从「静默通过」变成「启动失败」,那就不再是零代价。裁决前应由 repo:objectui 与 hotcrm 侧各跑一次同样的计数。方向 3 是否可行同样取决于这个数字。

              Related: #14399 · PR #14665(已落地)· #7378 · #13912

              Generated by Claude Code

              Metadata

              Metadata

              Assignees

              No one assigned

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  [Decision] Two SOURCE registrars disagree on a view container whose data.name differs from the derived key — one silently rewrites the author's field, the other hard-fails the whole artifact load #14666

                  Description

                  @os-musk

                  Found while implementing #14399 (PR #14665), which moved the ObjectQL boot loop onto the shared deriveViewContainerObject. Filed unassigned; not fixed there — it is a different site, and picking a direction is a decision rather than a mechanical repair. Re-measured by the triage seat at origin/main75adf11 (2026-09-02, R+105), after PR #14665 landed.

                  The reading

                  Both SOURCE registrars derive the same binding for an aggregated view container. They do not agree on what to do when the container's own name field disagrees with that derived key.

                  • packages/objectql/src/engine.ts:5149registerMetadataCollections reconciles it, silently:
                    const toRegister = item.name === itemName ? item : { ...item, name: itemName };
                    The stored document's name is overwritten with the derived object key. The author's field is discarded without a diagnostic.

                  • packages/metadata/src/plugin.ts:1127_parseAndRegisterArtifact does not: it calls this.manager.register('view', viewObject, item, { notify: false }) with the item unchanged, so assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts:170-172, MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1) refuses:

                    IMetadataService.register('view', 'crm_lead'): data.name is 'lead_views', which disagrees with
                    the name argument 'crm_lead'. ... (#7378 row 1: refuse loudly, locate the mismatch).
                    

                    VALIDATION_ERROR / 400, thrown out of _parseAndRegisterArtifact — the entry the boot artifact load and the HMR reload share, so the refusal is a hard failure of the whole artifact, not of the one container.

                  Reachable divergence

                  For { name: 'lead_views', object: 'crm_lead', list: { … } } — schema-legal at both doors, since ViewSchema declares an optional name (packages/spec/src/ui/view.zod.ts:3603):

                  • through the boot loop: registers under crm_lead, with the document's name silently rewritten from lead_views to crm_lead;
                  • through the artifact/HMR door: nothing registers and the artifact load throws.

                  Same document, two SOURCE registrars, and the outcomes are not merely different — one is a silent authorial rewrite and the other is a boot failure.

                  The candidate directions

                  1. Make the artifact door reconcile too (mirror the boot loop's toRegister line). Cheap and makes both registrars accept the shape — but it spreads a SILENT rewrite of an author-written field, which is what MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1 exists to refuse.
                  2. Make the boot loop refuse too, matching MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1. Consistent with the standing ruling and the safer default for AI-authored metadata, but it turns a currently-silent path into a loud one, and the boot loop's toRegister line predates the ruling and serves the ordinary no-name container as well — narrowing it correctly is not mechanical.
                  3. Refuse ViewSchema.name outright for object-scoped containers, since its own .describe() says "for an object-scoped container it is the object name" — a convention no door enforces. This one moves a published packages/spec contract.

                  ✅ Now pinned on main (R+104's caveat is discharged)

                  packages/objectql/src/view-container-divergent-name-registrars.test.tshas landed with PR #14665. It pins the current behaviour of both sides, including the refusal's code/status, so neither door can move without a red. Nothing asserts which direction is right — that is still what this card asks.

                  Nothing shipped moves today

                  Of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to. The latent form is what is bad — an author who writes one gets a silent rewrite or a boot failure depending on how their package is loaded.

                  <!-- os-decision-facets -->

                  四棱分析(分诊席出具,供裁决)

                  ① 长期健全性(权重 ≥50%,主导本建议)—— 指向 2。
                  最深的缺陷不是「哪个方向对」,而是两个 SOURCE 注册器对同一份文档给出相反结局。无论裁哪个方向,两扇门必须收敛 —— 否则「这份元数据合不合法」取决于它是被 boot 还是被 artifact/HMR 加载的,而作者看不见这个区别。方向 1 尤其要小心:#7378 row 1 已是既有裁决,其原话就是「resolving it silently in either direction can file the item under a key the caller never wrote」。方向 1 把那条裁决明令拒绝的东西复制到第二扇门,是在既有裁决上开倒车。

                  ② 真实业务拉力 —— 零,但潜伏形状是真的。
                  54 个非测试来源里 0 个声明了发散的 name。没有用户在等这个。⭐ 但也正因为零,方向 2 今天不会弄坏任何东西 —— 这是「把静默路径改响」通常要付而这里恰好不必付的代价。真正的成本在未来:第一个写出发散 name 的作者,拿到的是静默改写还是启动失败,取决于加载路径,从他的视角看是不确定的。

                  ③ 防 AI 编码错误 —— 强烈指向 2。
                  name: 'lead_views' 恰恰是 AI 作者会写出来的东西:一个像样的、人类风格的容器名。静默改写意味着他永远学不到这条约定;响亮拒绝会指名不匹配的两个值并定位它 —— 那正是 #7378 row 1 的设计意图。⚠️ 附带一条:当前的发散还意味着穿一扇门的测试证明不了另一扇门的行为;PR #14665 的钉子已落地,两边现在都钉住了当前行为,但没有任何断言说哪个方向是对的

                  ④ 创业阶段不增殖 —— 指向 2,反对 3。
                  方向 2 不新增概念,只是把一条已有裁决贯彻到第二处。方向 3 要在 packages/spec 加一条新的 schema 规则和一类新的拒绝,并动已发布契约 —— 那是本轴要避免的增殖,且触人类底线(published contract)。方向 1 不增殖但会固化「发散 name 会怎样」的两种拼法。

                  建议:2 —— 让 boot 循环也按 #7378 row 1 拒绝;⛔ 收敛必须只针对发散的情形,不得波及普通的无 name 容器(那正是卡面说「narrowing it correctly is not mechanical」的地方,也是实施的主要风险点)。

                  回退: 若维护者判断 boot 循环的静默对某条普通路径是承重的,则退为「两扇门都接受,但发散时记一条 warn 并指名两个值」—— 保留可加载性,同时终结无声改写。⛔ 三者之中唯一不可接受的是维持现状:两扇门对同一份文档给出相反结局,是本卡真正要消灭的东西。

                  ⚠️ 置信缺口(必录): 我只测了本仓的 54 个非测试来源。没有测下游 —— objectui / hotcrm / 各 examples 是否有作者写出发散 name,我读不到(跨仓)。若下游存在这样的文档,方向 2 会把它们从「静默通过」变成「启动失败」,那就不再是零代价。裁决前应由 repo:objectui 与 hotcrm 侧各跑一次同样的计数。方向 3 是否可行同样取决于这个数字。

                  Related: #14399 · PR #14665(已落地)· #7378 · #13912

                  Generated by Claude Code

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      [Decision] Two SOURCE registrars disagree on a view container whose data.name differs from the derived key — one silently rewrites the author's field, the other hard-fails the whole artifact load #14666

                      Description

                      @os-musk

                      Found while implementing #14399 (PR #14665), which moved the ObjectQL boot loop onto the shared deriveViewContainerObject. Filed unassigned; not fixed there — it is a different site, and picking a direction is a decision rather than a mechanical repair. Re-measured by the triage seat at origin/main75adf11 (2026-09-02, R+105), after PR #14665 landed.

                      The reading

                      Both SOURCE registrars derive the same binding for an aggregated view container. They do not agree on what to do when the container's own name field disagrees with that derived key.

                      • packages/objectql/src/engine.ts:5149registerMetadataCollections reconciles it, silently:
                        const toRegister = item.name === itemName ? item : { ...item, name: itemName };
                        The stored document's name is overwritten with the derived object key. The author's field is discarded without a diagnostic.

                      • packages/metadata/src/plugin.ts:1127_parseAndRegisterArtifact does not: it calls this.manager.register('view', viewObject, item, { notify: false }) with the item unchanged, so assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts:170-172, MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1) refuses:

                        IMetadataService.register('view', 'crm_lead'): data.name is 'lead_views', which disagrees with
                        the name argument 'crm_lead'. ... (#7378 row 1: refuse loudly, locate the mismatch).
                        

                        VALIDATION_ERROR / 400, thrown out of _parseAndRegisterArtifact — the entry the boot artifact load and the HMR reload share, so the refusal is a hard failure of the whole artifact, not of the one container.

                      Reachable divergence

                      For { name: 'lead_views', object: 'crm_lead', list: { … } } — schema-legal at both doors, since ViewSchema declares an optional name (packages/spec/src/ui/view.zod.ts:3603):

                      • through the boot loop: registers under crm_lead, with the document's name silently rewritten from lead_views to crm_lead;
                      • through the artifact/HMR door: nothing registers and the artifact load throws.

                      Same document, two SOURCE registrars, and the outcomes are not merely different — one is a silent authorial rewrite and the other is a boot failure.

                      The candidate directions

                      1. Make the artifact door reconcile too (mirror the boot loop's toRegister line). Cheap and makes both registrars accept the shape — but it spreads a SILENT rewrite of an author-written field, which is what MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1 exists to refuse.
                      2. Make the boot loop refuse too, matching MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1. Consistent with the standing ruling and the safer default for AI-authored metadata, but it turns a currently-silent path into a loud one, and the boot loop's toRegister line predates the ruling and serves the ordinary no-name container as well — narrowing it correctly is not mechanical.
                      3. Refuse ViewSchema.name outright for object-scoped containers, since its own .describe() says "for an object-scoped container it is the object name" — a convention no door enforces. This one moves a published packages/spec contract.

                      ✅ Now pinned on main (R+104's caveat is discharged)

                      packages/objectql/src/view-container-divergent-name-registrars.test.tshas landed with PR #14665. It pins the current behaviour of both sides, including the refusal's code/status, so neither door can move without a red. Nothing asserts which direction is right — that is still what this card asks.

                      Nothing shipped moves today

                      Of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to. The latent form is what is bad — an author who writes one gets a silent rewrite or a boot failure depending on how their package is loaded.

                      <!-- os-decision-facets -->

                      四棱分析(分诊席出具,供裁决)

                      ① 长期健全性(权重 ≥50%,主导本建议)—— 指向 2。
                      最深的缺陷不是「哪个方向对」,而是两个 SOURCE 注册器对同一份文档给出相反结局。无论裁哪个方向,两扇门必须收敛 —— 否则「这份元数据合不合法」取决于它是被 boot 还是被 artifact/HMR 加载的,而作者看不见这个区别。方向 1 尤其要小心:#7378 row 1 已是既有裁决,其原话就是「resolving it silently in either direction can file the item under a key the caller never wrote」。方向 1 把那条裁决明令拒绝的东西复制到第二扇门,是在既有裁决上开倒车。

                      ② 真实业务拉力 —— 零,但潜伏形状是真的。
                      54 个非测试来源里 0 个声明了发散的 name。没有用户在等这个。⭐ 但也正因为零,方向 2 今天不会弄坏任何东西 —— 这是「把静默路径改响」通常要付而这里恰好不必付的代价。真正的成本在未来:第一个写出发散 name 的作者,拿到的是静默改写还是启动失败,取决于加载路径,从他的视角看是不确定的。

                      ③ 防 AI 编码错误 —— 强烈指向 2。
                      name: 'lead_views' 恰恰是 AI 作者会写出来的东西:一个像样的、人类风格的容器名。静默改写意味着他永远学不到这条约定;响亮拒绝会指名不匹配的两个值并定位它 —— 那正是 #7378 row 1 的设计意图。⚠️ 附带一条:当前的发散还意味着穿一扇门的测试证明不了另一扇门的行为;PR #14665 的钉子已落地,两边现在都钉住了当前行为,但没有任何断言说哪个方向是对的

                      ④ 创业阶段不增殖 —— 指向 2,反对 3。
                      方向 2 不新增概念,只是把一条已有裁决贯彻到第二处。方向 3 要在 packages/spec 加一条新的 schema 规则和一类新的拒绝,并动已发布契约 —— 那是本轴要避免的增殖,且触人类底线(published contract)。方向 1 不增殖但会固化「发散 name 会怎样」的两种拼法。

                      建议:2 —— 让 boot 循环也按 #7378 row 1 拒绝;⛔ 收敛必须只针对发散的情形,不得波及普通的无 name 容器(那正是卡面说「narrowing it correctly is not mechanical」的地方,也是实施的主要风险点)。

                      回退: 若维护者判断 boot 循环的静默对某条普通路径是承重的,则退为「两扇门都接受,但发散时记一条 warn 并指名两个值」—— 保留可加载性,同时终结无声改写。⛔ 三者之中唯一不可接受的是维持现状:两扇门对同一份文档给出相反结局,是本卡真正要消灭的东西。

                      ⚠️ 置信缺口(必录): 我只测了本仓的 54 个非测试来源。没有测下游 —— objectui / hotcrm / 各 examples 是否有作者写出发散 name,我读不到(跨仓)。若下游存在这样的文档,方向 2 会把它们从「静默通过」变成「启动失败」,那就不再是零代价。裁决前应由 repo:objectui 与 hotcrm 侧各跑一次同样的计数。方向 3 是否可行同样取决于这个数字。

                      Related: #14399 · PR #14665(已落地)· #7378 · #13912

                      Generated by Claude Code

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          [Decision] Two SOURCE registrars disagree on a view container whose data.name differs from the derived key — one silently rewrites the author's field, the other hard-fails the whole artifact load #14666

                          Description

                          @os-musk

                          Found while implementing #14399 (PR #14665), which moved the ObjectQL boot loop onto the shared deriveViewContainerObject. Filed unassigned; not fixed there — it is a different site, and picking a direction is a decision rather than a mechanical repair. Re-measured by the triage seat at origin/main75adf11 (2026-09-02, R+105), after PR #14665 landed.

                          The reading

                          Both SOURCE registrars derive the same binding for an aggregated view container. They do not agree on what to do when the container's own name field disagrees with that derived key.

                          • packages/objectql/src/engine.ts:5149registerMetadataCollections reconciles it, silently:
                            const toRegister = item.name === itemName ? item : { ...item, name: itemName };
                            The stored document's name is overwritten with the derived object key. The author's field is discarded without a diagnostic.

                          • packages/metadata/src/plugin.ts:1127_parseAndRegisterArtifact does not: it calls this.manager.register('view', viewObject, item, { notify: false }) with the item unchanged, so assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts:170-172, MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1) refuses:

                            IMetadataService.register('view', 'crm_lead'): data.name is 'lead_views', which disagrees with
                            the name argument 'crm_lead'. ... (#7378 row 1: refuse loudly, locate the mismatch).
                            

                            VALIDATION_ERROR / 400, thrown out of _parseAndRegisterArtifact — the entry the boot artifact load and the HMR reload share, so the refusal is a hard failure of the whole artifact, not of the one container.

                          Reachable divergence

                          For { name: 'lead_views', object: 'crm_lead', list: { … } } — schema-legal at both doors, since ViewSchema declares an optional name (packages/spec/src/ui/view.zod.ts:3603):

                          • through the boot loop: registers under crm_lead, with the document's name silently rewritten from lead_views to crm_lead;
                          • through the artifact/HMR door: nothing registers and the artifact load throws.

                          Same document, two SOURCE registrars, and the outcomes are not merely different — one is a silent authorial rewrite and the other is a boot failure.

                          The candidate directions

                          1. Make the artifact door reconcile too (mirror the boot loop's toRegister line). Cheap and makes both registrars accept the shape — but it spreads a SILENT rewrite of an author-written field, which is what MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1 exists to refuse.
                          2. Make the boot loop refuse too, matching MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1. Consistent with the standing ruling and the safer default for AI-authored metadata, but it turns a currently-silent path into a loud one, and the boot loop's toRegister line predates the ruling and serves the ordinary no-name container as well — narrowing it correctly is not mechanical.
                          3. Refuse ViewSchema.name outright for object-scoped containers, since its own .describe() says "for an object-scoped container it is the object name" — a convention no door enforces. This one moves a published packages/spec contract.

                          ✅ Now pinned on main (R+104's caveat is discharged)

                          packages/objectql/src/view-container-divergent-name-registrars.test.tshas landed with PR #14665. It pins the current behaviour of both sides, including the refusal's code/status, so neither door can move without a red. Nothing asserts which direction is right — that is still what this card asks.

                          Nothing shipped moves today

                          Of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to. The latent form is what is bad — an author who writes one gets a silent rewrite or a boot failure depending on how their package is loaded.

                          <!-- os-decision-facets -->

                          四棱分析(分诊席出具,供裁决)

                          ① 长期健全性(权重 ≥50%,主导本建议)—— 指向 2。
                          最深的缺陷不是「哪个方向对」,而是两个 SOURCE 注册器对同一份文档给出相反结局。无论裁哪个方向,两扇门必须收敛 —— 否则「这份元数据合不合法」取决于它是被 boot 还是被 artifact/HMR 加载的,而作者看不见这个区别。方向 1 尤其要小心:#7378 row 1 已是既有裁决,其原话就是「resolving it silently in either direction can file the item under a key the caller never wrote」。方向 1 把那条裁决明令拒绝的东西复制到第二扇门,是在既有裁决上开倒车。

                          ② 真实业务拉力 —— 零,但潜伏形状是真的。
                          54 个非测试来源里 0 个声明了发散的 name。没有用户在等这个。⭐ 但也正因为零,方向 2 今天不会弄坏任何东西 —— 这是「把静默路径改响」通常要付而这里恰好不必付的代价。真正的成本在未来:第一个写出发散 name 的作者,拿到的是静默改写还是启动失败,取决于加载路径,从他的视角看是不确定的。

                          ③ 防 AI 编码错误 —— 强烈指向 2。
                          name: 'lead_views' 恰恰是 AI 作者会写出来的东西:一个像样的、人类风格的容器名。静默改写意味着他永远学不到这条约定;响亮拒绝会指名不匹配的两个值并定位它 —— 那正是 #7378 row 1 的设计意图。⚠️ 附带一条:当前的发散还意味着穿一扇门的测试证明不了另一扇门的行为;PR #14665 的钉子已落地,两边现在都钉住了当前行为,但没有任何断言说哪个方向是对的

                          ④ 创业阶段不增殖 —— 指向 2,反对 3。
                          方向 2 不新增概念,只是把一条已有裁决贯彻到第二处。方向 3 要在 packages/spec 加一条新的 schema 规则和一类新的拒绝,并动已发布契约 —— 那是本轴要避免的增殖,且触人类底线(published contract)。方向 1 不增殖但会固化「发散 name 会怎样」的两种拼法。

                          建议:2 —— 让 boot 循环也按 #7378 row 1 拒绝;⛔ 收敛必须只针对发散的情形,不得波及普通的无 name 容器(那正是卡面说「narrowing it correctly is not mechanical」的地方,也是实施的主要风险点)。

                          回退: 若维护者判断 boot 循环的静默对某条普通路径是承重的,则退为「两扇门都接受,但发散时记一条 warn 并指名两个值」—— 保留可加载性,同时终结无声改写。⛔ 三者之中唯一不可接受的是维持现状:两扇门对同一份文档给出相反结局,是本卡真正要消灭的东西。

                          ⚠️ 置信缺口(必录): 我只测了本仓的 54 个非测试来源。没有测下游 —— objectui / hotcrm / 各 examples 是否有作者写出发散 name,我读不到(跨仓)。若下游存在这样的文档,方向 2 会把它们从「静默通过」变成「启动失败」,那就不再是零代价。裁决前应由 repo:objectui 与 hotcrm 侧各跑一次同样的计数。方向 3 是否可行同样取决于这个数字。

                          Related: #14399 · PR #14665(已落地)· #7378 · #13912

                          Generated by Claude Code

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              [Decision] Two SOURCE registrars disagree on a view container whose data.name differs from the derived key — one silently rewrites the author's field, the other hard-fails the whole artifact load #14666

                              Description

                              @os-musk

                              Found while implementing #14399 (PR #14665), which moved the ObjectQL boot loop onto the shared deriveViewContainerObject. Filed unassigned; not fixed there — it is a different site, and picking a direction is a decision rather than a mechanical repair. Re-measured by the triage seat at origin/main75adf11 (2026-09-02, R+105), after PR #14665 landed.

                              The reading

                              Both SOURCE registrars derive the same binding for an aggregated view container. They do not agree on what to do when the container's own name field disagrees with that derived key.

                              • packages/objectql/src/engine.ts:5149registerMetadataCollections reconciles it, silently:
                                const toRegister = item.name === itemName ? item : { ...item, name: itemName };
                                The stored document's name is overwritten with the derived object key. The author's field is discarded without a diagnostic.

                              • packages/metadata/src/plugin.ts:1127_parseAndRegisterArtifact does not: it calls this.manager.register('view', viewObject, item, { notify: false }) with the item unchanged, so assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts:170-172, MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1) refuses:

                                IMetadataService.register('view', 'crm_lead'): data.name is 'lead_views', which disagrees with
                                the name argument 'crm_lead'. ... (#7378 row 1: refuse loudly, locate the mismatch).
                                

                                VALIDATION_ERROR / 400, thrown out of _parseAndRegisterArtifact — the entry the boot artifact load and the HMR reload share, so the refusal is a hard failure of the whole artifact, not of the one container.

                              Reachable divergence

                              For { name: 'lead_views', object: 'crm_lead', list: { … } } — schema-legal at both doors, since ViewSchema declares an optional name (packages/spec/src/ui/view.zod.ts:3603):

                              • through the boot loop: registers under crm_lead, with the document's name silently rewritten from lead_views to crm_lead;
                              • through the artifact/HMR door: nothing registers and the artifact load throws.

                              Same document, two SOURCE registrars, and the outcomes are not merely different — one is a silent authorial rewrite and the other is a boot failure.

                              The candidate directions

                              1. Make the artifact door reconcile too (mirror the boot loop's toRegister line). Cheap and makes both registrars accept the shape — but it spreads a SILENT rewrite of an author-written field, which is what MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1 exists to refuse.
                              2. Make the boot loop refuse too, matching MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row 1. Consistent with the standing ruling and the safer default for AI-authored metadata, but it turns a currently-silent path into a loud one, and the boot loop's toRegister line predates the ruling and serves the ordinary no-name container as well — narrowing it correctly is not mechanical.
                              3. Refuse ViewSchema.name outright for object-scoped containers, since its own .describe() says "for an object-scoped container it is the object name" — a convention no door enforces. This one moves a published packages/spec contract.

                              ✅ Now pinned on main (R+104's caveat is discharged)

                              packages/objectql/src/view-container-divergent-name-registrars.test.tshas landed with PR #14665. It pins the current behaviour of both sides, including the refusal's code/status, so neither door can move without a red. Nothing asserts which direction is right — that is still what this card asks.

                              Nothing shipped moves today

                              Of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to. The latent form is what is bad — an author who writes one gets a silent rewrite or a boot failure depending on how their package is loaded.

                              <!-- os-decision-facets -->

                              四棱分析(分诊席出具,供裁决)

                              ① 长期健全性(权重 ≥50%,主导本建议)—— 指向 2。
                              最深的缺陷不是「哪个方向对」,而是两个 SOURCE 注册器对同一份文档给出相反结局。无论裁哪个方向,两扇门必须收敛 —— 否则「这份元数据合不合法」取决于它是被 boot 还是被 artifact/HMR 加载的,而作者看不见这个区别。方向 1 尤其要小心:#7378 row 1 已是既有裁决,其原话就是「resolving it silently in either direction can file the item under a key the caller never wrote」。方向 1 把那条裁决明令拒绝的东西复制到第二扇门,是在既有裁决上开倒车。

                              ② 真实业务拉力 —— 零,但潜伏形状是真的。
                              54 个非测试来源里 0 个声明了发散的 name。没有用户在等这个。⭐ 但也正因为零,方向 2 今天不会弄坏任何东西 —— 这是「把静默路径改响」通常要付而这里恰好不必付的代价。真正的成本在未来:第一个写出发散 name 的作者,拿到的是静默改写还是启动失败,取决于加载路径,从他的视角看是不确定的。

                              ③ 防 AI 编码错误 —— 强烈指向 2。
                              name: 'lead_views' 恰恰是 AI 作者会写出来的东西:一个像样的、人类风格的容器名。静默改写意味着他永远学不到这条约定;响亮拒绝会指名不匹配的两个值并定位它 —— 那正是 #7378 row 1 的设计意图。⚠️ 附带一条:当前的发散还意味着穿一扇门的测试证明不了另一扇门的行为;PR #14665 的钉子已落地,两边现在都钉住了当前行为,但没有任何断言说哪个方向是对的

                              ④ 创业阶段不增殖 —— 指向 2,反对 3。
                              方向 2 不新增概念,只是把一条已有裁决贯彻到第二处。方向 3 要在 packages/spec 加一条新的 schema 规则和一类新的拒绝,并动已发布契约 —— 那是本轴要避免的增殖,且触人类底线(published contract)。方向 1 不增殖但会固化「发散 name 会怎样」的两种拼法。

                              建议:2 —— 让 boot 循环也按 #7378 row 1 拒绝;⛔ 收敛必须只针对发散的情形,不得波及普通的无 name 容器(那正是卡面说「narrowing it correctly is not mechanical」的地方,也是实施的主要风险点)。

                              回退: 若维护者判断 boot 循环的静默对某条普通路径是承重的,则退为「两扇门都接受,但发散时记一条 warn 并指名两个值」—— 保留可加载性,同时终结无声改写。⛔ 三者之中唯一不可接受的是维持现状:两扇门对同一份文档给出相反结局,是本卡真正要消灭的东西。

                              ⚠️ 置信缺口(必录): 我只测了本仓的 54 个非测试来源。没有测下游 —— objectui / hotcrm / 各 examples 是否有作者写出发散 name,我读不到(跨仓)。若下游存在这样的文档,方向 2 会把它们从「静默通过」变成「启动失败」,那就不再是零代价。裁决前应由 repo:objectui 与 hotcrm 侧各跑一次同样的计数。方向 3 是否可行同样取决于这个数字。

                              Related: #14399 · PR #14665(已落地)· #7378 · #13912

                              Generated by Claude Code

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions