finding(metadata): FilesystemLoader.list() reports names that findFile()/load()/exists() cannot resolve — nested paths and extension-less files are listed but unreachable, and .js/.ts files are listed but never loaded #14486

Description

@os-musk

Filed unassigned by the domain:engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68) from the #14341 stop-and-report (triage 5503567681 designated this disagreement "the deliverable's first finding"). Recorded here because its repair moves consumer-visible output and is not the #14341 deliverable. Left ungraded — domain:* / type / priority are triage's.

Measured (read-only, origin/main @ 253da34c4, packages/metadata/src/loaders/filesystem-loader.ts)

FilesystemLoader derives a metadata name from a path in two places that do not agree for every shape loadMany() reads:

  • list(type) (:243) walks the type directory and reports path.basename(file, ext) for every file, nested or not, with or without an extension.
  • findFile(type, name) (:365) resolves ROOT/TYPE/NAME + one of five tried extensions (.json, .yaml, .yml, .ts, .js).

Probe fixture ROOT/object/{flat.json, nameless.json, dotted.config.json, yamlish.yaml, yamlish2.yml, crm/account.json, crm/nameless-nested.json, noext}:

shapelist('object') reportsfindFile() / stat() / exists()loadMany()
flat file, tried extension (incl. dotted.config.jsondotted.config)the basenameresolves ✔returns the body ✔
nestedcrm/account.jsonaccountfindFile('object','account')null; only crm/account resolvesreturns the body ✔
extension-lessnoextnoextnull (every tried extension appended)returns the body ✔ (detectFormat falls through to json)
.js / .ts under the manager's DEFAULT format set (typescript, json, yaml)listedresolves ✔never returned (no javascript serializer; the .ts file was likewise not returned)

Consequence at the manager: MetadataManager.listNames() (metadata-manager.ts:1586, reads loader.list()) reports account and noext, and MetadataManager.get() for either name cannot fetch it. The .js/.ts row is a membership mismatch (listed and resolvable, not loaded) rather than a key mismatch.

Why it is a card of its own

#14341 (loadManyKeyed() for this loader) was ruled to key items only where the derivation is already a bijection (flat file, tried extension) and to carry the pre-#14205body.name behaviour for the disagreeing shapes — it deliberately does not mint keys the loader cannot resolve. Repairing the disagreement itself means one shared name-to-path function used by list(), findFile() and loadManyKeyed() alike, and that moves listNames() output (nested files would report crm/account or stop being listed; extension-less files would stop being listed or become resolvable) — consumer-visible, so it needs its own ruling on which side is the contract. The canonical layout in packages/metadata-fs/src/layout.ts (ADR-0008 §10: ROOT/TYPE/NAME.json, parseItemPath() refuses other shapes) is the evidence for the "stop listing them" side; the fact that loadMany() reads them today is the evidence for the other.

Not claimed

  • No measured production tree carrying a nested or extension-less metadata file. The disagreement is real on any tree that has one.
  • No repair proposed beyond naming the shape (C above).

Duplicate search (one targeted search_issues, channel proven live by returning #14341): no existing card names this disagreement. Related: #14341 · #14205 · #13913 · #14342 (@objectstack/metadata has no typecheck script — the test layer of any repair here is NOT MEASURED until that lands).


Triage addendum — re-measured at ed44512, plus one correction to row 4

Rows 1–3 and the manager consequence reproduce exactly at current main; I did not take them on trust.

  • filesystem-loader.ts:243-258list() globs **/* (nodir: true) and returns path.basename(file, ext), discarding the directory. Nested and extension-less files are both in.
  • filesystem-loader.ts:365-381findFile() tries ${name}${ext} for exactly ['.json','.yaml','.yml','.ts','.js'] against ROOT/TYPE. No extension-less fallback, no nested resolution unless the caller already spells crm/account.
  • metadata-manager.ts:1585-1588listNames() unions every loader's list() output straight into the returned set, so an unresolvable name surfaces to the manager's consumers unfiltered.
  • metadata-fs/src/layout.ts:37-50parseItemPath() returns null unless the relative path is exactly two segments and ends .json. The card's ADR-0008 §10 evidence holds verbatim, and it is the sharper half of the story: two halves of this codebase already disagree about whether a nested metadata file is legal at all, independently of the list/find mismatch.

Correction — row 4 states one mechanism for two different shapes, and it only fits .js.

  • .js: right. metadata-manager.ts:554 defaults formats to ['typescript','json','yaml'], and :565-566 registers the javascript serializer only when formats.includes('javascript'). Under the default set there is no javascript serializer, so a resolvable .js file is never returned. Mechanism confirmed.
  • .ts: not explained by "no serializer". :562-563 registers new TypeScriptSerializer('typescript') under the default set, and typescript-serializer.ts:30-60 does deserialize — it finds export const or export default, brace-matches the object literal, and parses it. A well-formed .ts metadata module should therefore load. The probe's .ts miss is real as an observation but unexplained as stated; the likeliest cause is a fixture without an export const / export default pattern, in which case deserialize throws and the loader's catch (:229-240) logs Failed to load and returns null — dropped silently from loadMany().

⇒ Whoever takes this must re-measure the .ts row with a fixture carrying the export pattern before designing anything for it. As stated, an implementer would go add a javascript serializer and either fix .ts by accident or not at all. The silent-drop-on-parse-failure tolerance that this uncovered is a separate shape; if it survives re-measurement it earns its own card rather than riding in here.

Why this is needs-user-decision

Both repairs move listNames() output for anyone whose tree has one of these files, and the direction call has an ADR on one side of it (ADR-0008 §10 already says two segments, .json only) and working loadMany() behaviour on the other. Picking which half of the codebase is wrong is a contract direction, not a bug fix — human floor. The seat can measure both sides; it cannot choose.

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

  • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 平台已经有一份写死的目录规范:类型一层、名字一层、只认 .json(metadata-fs 那边就是照它拒绝别的形状)。而这个加载器的清单函数把任意深度、任意后缀的文件全捞出来。同一个产品里两套目录规矩同时活着,这本身就是特例增生。 缩小特例的方向 = 让清单向既有规范靠拢,而不是再把解析侧加宽去追清单 —— 加宽等于把「路径形名字」这个新概念推给每一个下游消费者,永久多一层。
  • ② 实际业务拉动 —— 今天零实测:没有量到任何一棵生产树里放着嵌套或无后缀的元数据文件(卡面自己也这么说)。没有客户在撞。按分歧推荐序,零拉动⇒荐④不扩散,与①同向,两棱都指向收窄而不是加宽。
  • ③ 防 AI 犯错 —— 出错时谁看到什么:名字明明在清单里,取详情却是空的,一声不响。作者(人或 AI)会以为是自己拼错了名字,反复重试同一个词。收窄成「不合规范的文件根本不进清单」,清单与详情从此给同一个答案;加宽成清单报 crm/account,则名字里开始出现斜杠,每个消费者都得学会处理 —— 静默不一致换成了广播式复杂度。
  • ④ 创业阶段不扩散 —— 支持嵌套目录、支持无后缀文件、支持 .js 读取,是三条各自独立的永久义务,每条都要测试、要文档、要向后兼容。今天没有任何一个消费者提出过这三条需求中的任何一条。remove 优于 declare-and-maintain。

推荐:A(收窄) —— list()findFile()loadManyKeyed() 共用同一个「名字↔路径」函数,以 ADR-0008 §10 为准(两层、.json);不合规范的文件不再出现在 listNames() 里。四棱同向,零拉动,①与④叠加。
回退:B(反向统一) —— 若维护者认定嵌套布局是要保的产品能力,则反过来统一:list()crm/accountfindFile() 认路径形名字,并同一批改掉 ADR-0008 §10 与 parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。
置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外 .ts 那一行的机制已被本轮更正(见上),.js/.ts 该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。

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

      finding(metadata): FilesystemLoader.list() reports names that findFile()/load()/exists() cannot resolve — nested paths and extension-less files are listed but unreachable, and .js/.ts files are listed but never loaded #14486

      Description

      @os-musk

      Filed unassigned by the domain:engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68) from the #14341 stop-and-report (triage 5503567681 designated this disagreement "the deliverable's first finding"). Recorded here because its repair moves consumer-visible output and is not the #14341 deliverable. Left ungraded — domain:* / type / priority are triage's.

      Measured (read-only, origin/main @ 253da34c4, packages/metadata/src/loaders/filesystem-loader.ts)

      FilesystemLoader derives a metadata name from a path in two places that do not agree for every shape loadMany() reads:

      • list(type) (:243) walks the type directory and reports path.basename(file, ext) for every file, nested or not, with or without an extension.
      • findFile(type, name) (:365) resolves ROOT/TYPE/NAME + one of five tried extensions (.json, .yaml, .yml, .ts, .js).

      Probe fixture ROOT/object/{flat.json, nameless.json, dotted.config.json, yamlish.yaml, yamlish2.yml, crm/account.json, crm/nameless-nested.json, noext}:

      shapelist('object') reportsfindFile() / stat() / exists()loadMany()
      flat file, tried extension (incl. dotted.config.jsondotted.config)the basenameresolves ✔returns the body ✔
      nestedcrm/account.jsonaccountfindFile('object','account')null; only crm/account resolvesreturns the body ✔
      extension-lessnoextnoextnull (every tried extension appended)returns the body ✔ (detectFormat falls through to json)
      .js / .ts under the manager's DEFAULT format set (typescript, json, yaml)listedresolves ✔never returned (no javascript serializer; the .ts file was likewise not returned)

      Consequence at the manager: MetadataManager.listNames() (metadata-manager.ts:1586, reads loader.list()) reports account and noext, and MetadataManager.get() for either name cannot fetch it. The .js/.ts row is a membership mismatch (listed and resolvable, not loaded) rather than a key mismatch.

      Why it is a card of its own

      #14341 (loadManyKeyed() for this loader) was ruled to key items only where the derivation is already a bijection (flat file, tried extension) and to carry the pre-#14205body.name behaviour for the disagreeing shapes — it deliberately does not mint keys the loader cannot resolve. Repairing the disagreement itself means one shared name-to-path function used by list(), findFile() and loadManyKeyed() alike, and that moves listNames() output (nested files would report crm/account or stop being listed; extension-less files would stop being listed or become resolvable) — consumer-visible, so it needs its own ruling on which side is the contract. The canonical layout in packages/metadata-fs/src/layout.ts (ADR-0008 §10: ROOT/TYPE/NAME.json, parseItemPath() refuses other shapes) is the evidence for the "stop listing them" side; the fact that loadMany() reads them today is the evidence for the other.

      Not claimed

      • No measured production tree carrying a nested or extension-less metadata file. The disagreement is real on any tree that has one.
      • No repair proposed beyond naming the shape (C above).

      Duplicate search (one targeted search_issues, channel proven live by returning #14341): no existing card names this disagreement. Related: #14341 · #14205 · #13913 · #14342 (@objectstack/metadata has no typecheck script — the test layer of any repair here is NOT MEASURED until that lands).


      Triage addendum — re-measured at ed44512, plus one correction to row 4

      Rows 1–3 and the manager consequence reproduce exactly at current main; I did not take them on trust.

      • filesystem-loader.ts:243-258list() globs **/* (nodir: true) and returns path.basename(file, ext), discarding the directory. Nested and extension-less files are both in.
      • filesystem-loader.ts:365-381findFile() tries ${name}${ext} for exactly ['.json','.yaml','.yml','.ts','.js'] against ROOT/TYPE. No extension-less fallback, no nested resolution unless the caller already spells crm/account.
      • metadata-manager.ts:1585-1588listNames() unions every loader's list() output straight into the returned set, so an unresolvable name surfaces to the manager's consumers unfiltered.
      • metadata-fs/src/layout.ts:37-50parseItemPath() returns null unless the relative path is exactly two segments and ends .json. The card's ADR-0008 §10 evidence holds verbatim, and it is the sharper half of the story: two halves of this codebase already disagree about whether a nested metadata file is legal at all, independently of the list/find mismatch.

      Correction — row 4 states one mechanism for two different shapes, and it only fits .js.

      • .js: right. metadata-manager.ts:554 defaults formats to ['typescript','json','yaml'], and :565-566 registers the javascript serializer only when formats.includes('javascript'). Under the default set there is no javascript serializer, so a resolvable .js file is never returned. Mechanism confirmed.
      • .ts: not explained by "no serializer". :562-563 registers new TypeScriptSerializer('typescript') under the default set, and typescript-serializer.ts:30-60 does deserialize — it finds export const or export default, brace-matches the object literal, and parses it. A well-formed .ts metadata module should therefore load. The probe's .ts miss is real as an observation but unexplained as stated; the likeliest cause is a fixture without an export const / export default pattern, in which case deserialize throws and the loader's catch (:229-240) logs Failed to load and returns null — dropped silently from loadMany().

      ⇒ Whoever takes this must re-measure the .ts row with a fixture carrying the export pattern before designing anything for it. As stated, an implementer would go add a javascript serializer and either fix .ts by accident or not at all. The silent-drop-on-parse-failure tolerance that this uncovered is a separate shape; if it survives re-measurement it earns its own card rather than riding in here.

      Why this is needs-user-decision

      Both repairs move listNames() output for anyone whose tree has one of these files, and the direction call has an ADR on one side of it (ADR-0008 §10 already says two segments, .json only) and working loadMany() behaviour on the other. Picking which half of the codebase is wrong is a contract direction, not a bug fix — human floor. The seat can measure both sides; it cannot choose.

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

      • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 平台已经有一份写死的目录规范:类型一层、名字一层、只认 .json(metadata-fs 那边就是照它拒绝别的形状)。而这个加载器的清单函数把任意深度、任意后缀的文件全捞出来。同一个产品里两套目录规矩同时活着,这本身就是特例增生。 缩小特例的方向 = 让清单向既有规范靠拢,而不是再把解析侧加宽去追清单 —— 加宽等于把「路径形名字」这个新概念推给每一个下游消费者,永久多一层。
      • ② 实际业务拉动 —— 今天零实测:没有量到任何一棵生产树里放着嵌套或无后缀的元数据文件(卡面自己也这么说)。没有客户在撞。按分歧推荐序,零拉动⇒荐④不扩散,与①同向,两棱都指向收窄而不是加宽。
      • ③ 防 AI 犯错 —— 出错时谁看到什么:名字明明在清单里,取详情却是空的,一声不响。作者(人或 AI)会以为是自己拼错了名字,反复重试同一个词。收窄成「不合规范的文件根本不进清单」,清单与详情从此给同一个答案;加宽成清单报 crm/account,则名字里开始出现斜杠,每个消费者都得学会处理 —— 静默不一致换成了广播式复杂度。
      • ④ 创业阶段不扩散 —— 支持嵌套目录、支持无后缀文件、支持 .js 读取,是三条各自独立的永久义务,每条都要测试、要文档、要向后兼容。今天没有任何一个消费者提出过这三条需求中的任何一条。remove 优于 declare-and-maintain。

      推荐:A(收窄) —— list()findFile()loadManyKeyed() 共用同一个「名字↔路径」函数,以 ADR-0008 §10 为准(两层、.json);不合规范的文件不再出现在 listNames() 里。四棱同向,零拉动,①与④叠加。
      回退:B(反向统一) —— 若维护者认定嵌套布局是要保的产品能力,则反过来统一:list()crm/accountfindFile() 认路径形名字,并同一批改掉 ADR-0008 §10 与 parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。
      置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外 .ts 那一行的机制已被本轮更正(见上),.js/.ts 该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。

      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

          finding(metadata): FilesystemLoader.list() reports names that findFile()/load()/exists() cannot resolve — nested paths and extension-less files are listed but unreachable, and .js/.ts files are listed but never loaded #14486

          Description

          @os-musk

          Filed unassigned by the domain:engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68) from the #14341 stop-and-report (triage 5503567681 designated this disagreement "the deliverable's first finding"). Recorded here because its repair moves consumer-visible output and is not the #14341 deliverable. Left ungraded — domain:* / type / priority are triage's.

          Measured (read-only, origin/main @ 253da34c4, packages/metadata/src/loaders/filesystem-loader.ts)

          FilesystemLoader derives a metadata name from a path in two places that do not agree for every shape loadMany() reads:

          • list(type) (:243) walks the type directory and reports path.basename(file, ext) for every file, nested or not, with or without an extension.
          • findFile(type, name) (:365) resolves ROOT/TYPE/NAME + one of five tried extensions (.json, .yaml, .yml, .ts, .js).

          Probe fixture ROOT/object/{flat.json, nameless.json, dotted.config.json, yamlish.yaml, yamlish2.yml, crm/account.json, crm/nameless-nested.json, noext}:

          shapelist('object') reportsfindFile() / stat() / exists()loadMany()
          flat file, tried extension (incl. dotted.config.jsondotted.config)the basenameresolves ✔returns the body ✔
          nestedcrm/account.jsonaccountfindFile('object','account')null; only crm/account resolvesreturns the body ✔
          extension-lessnoextnoextnull (every tried extension appended)returns the body ✔ (detectFormat falls through to json)
          .js / .ts under the manager's DEFAULT format set (typescript, json, yaml)listedresolves ✔never returned (no javascript serializer; the .ts file was likewise not returned)

          Consequence at the manager: MetadataManager.listNames() (metadata-manager.ts:1586, reads loader.list()) reports account and noext, and MetadataManager.get() for either name cannot fetch it. The .js/.ts row is a membership mismatch (listed and resolvable, not loaded) rather than a key mismatch.

          Why it is a card of its own

          #14341 (loadManyKeyed() for this loader) was ruled to key items only where the derivation is already a bijection (flat file, tried extension) and to carry the pre-#14205body.name behaviour for the disagreeing shapes — it deliberately does not mint keys the loader cannot resolve. Repairing the disagreement itself means one shared name-to-path function used by list(), findFile() and loadManyKeyed() alike, and that moves listNames() output (nested files would report crm/account or stop being listed; extension-less files would stop being listed or become resolvable) — consumer-visible, so it needs its own ruling on which side is the contract. The canonical layout in packages/metadata-fs/src/layout.ts (ADR-0008 §10: ROOT/TYPE/NAME.json, parseItemPath() refuses other shapes) is the evidence for the "stop listing them" side; the fact that loadMany() reads them today is the evidence for the other.

          Not claimed

          • No measured production tree carrying a nested or extension-less metadata file. The disagreement is real on any tree that has one.
          • No repair proposed beyond naming the shape (C above).

          Duplicate search (one targeted search_issues, channel proven live by returning #14341): no existing card names this disagreement. Related: #14341 · #14205 · #13913 · #14342 (@objectstack/metadata has no typecheck script — the test layer of any repair here is NOT MEASURED until that lands).


          Triage addendum — re-measured at ed44512, plus one correction to row 4

          Rows 1–3 and the manager consequence reproduce exactly at current main; I did not take them on trust.

          • filesystem-loader.ts:243-258list() globs **/* (nodir: true) and returns path.basename(file, ext), discarding the directory. Nested and extension-less files are both in.
          • filesystem-loader.ts:365-381findFile() tries ${name}${ext} for exactly ['.json','.yaml','.yml','.ts','.js'] against ROOT/TYPE. No extension-less fallback, no nested resolution unless the caller already spells crm/account.
          • metadata-manager.ts:1585-1588listNames() unions every loader's list() output straight into the returned set, so an unresolvable name surfaces to the manager's consumers unfiltered.
          • metadata-fs/src/layout.ts:37-50parseItemPath() returns null unless the relative path is exactly two segments and ends .json. The card's ADR-0008 §10 evidence holds verbatim, and it is the sharper half of the story: two halves of this codebase already disagree about whether a nested metadata file is legal at all, independently of the list/find mismatch.

          Correction — row 4 states one mechanism for two different shapes, and it only fits .js.

          • .js: right. metadata-manager.ts:554 defaults formats to ['typescript','json','yaml'], and :565-566 registers the javascript serializer only when formats.includes('javascript'). Under the default set there is no javascript serializer, so a resolvable .js file is never returned. Mechanism confirmed.
          • .ts: not explained by "no serializer". :562-563 registers new TypeScriptSerializer('typescript') under the default set, and typescript-serializer.ts:30-60 does deserialize — it finds export const or export default, brace-matches the object literal, and parses it. A well-formed .ts metadata module should therefore load. The probe's .ts miss is real as an observation but unexplained as stated; the likeliest cause is a fixture without an export const / export default pattern, in which case deserialize throws and the loader's catch (:229-240) logs Failed to load and returns null — dropped silently from loadMany().

          ⇒ Whoever takes this must re-measure the .ts row with a fixture carrying the export pattern before designing anything for it. As stated, an implementer would go add a javascript serializer and either fix .ts by accident or not at all. The silent-drop-on-parse-failure tolerance that this uncovered is a separate shape; if it survives re-measurement it earns its own card rather than riding in here.

          Why this is needs-user-decision

          Both repairs move listNames() output for anyone whose tree has one of these files, and the direction call has an ADR on one side of it (ADR-0008 §10 already says two segments, .json only) and working loadMany() behaviour on the other. Picking which half of the codebase is wrong is a contract direction, not a bug fix — human floor. The seat can measure both sides; it cannot choose.

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

          • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 平台已经有一份写死的目录规范:类型一层、名字一层、只认 .json(metadata-fs 那边就是照它拒绝别的形状)。而这个加载器的清单函数把任意深度、任意后缀的文件全捞出来。同一个产品里两套目录规矩同时活着,这本身就是特例增生。 缩小特例的方向 = 让清单向既有规范靠拢,而不是再把解析侧加宽去追清单 —— 加宽等于把「路径形名字」这个新概念推给每一个下游消费者,永久多一层。
          • ② 实际业务拉动 —— 今天零实测:没有量到任何一棵生产树里放着嵌套或无后缀的元数据文件(卡面自己也这么说)。没有客户在撞。按分歧推荐序,零拉动⇒荐④不扩散,与①同向,两棱都指向收窄而不是加宽。
          • ③ 防 AI 犯错 —— 出错时谁看到什么:名字明明在清单里,取详情却是空的,一声不响。作者(人或 AI)会以为是自己拼错了名字,反复重试同一个词。收窄成「不合规范的文件根本不进清单」,清单与详情从此给同一个答案;加宽成清单报 crm/account,则名字里开始出现斜杠,每个消费者都得学会处理 —— 静默不一致换成了广播式复杂度。
          • ④ 创业阶段不扩散 —— 支持嵌套目录、支持无后缀文件、支持 .js 读取,是三条各自独立的永久义务,每条都要测试、要文档、要向后兼容。今天没有任何一个消费者提出过这三条需求中的任何一条。remove 优于 declare-and-maintain。

          推荐:A(收窄) —— list()findFile()loadManyKeyed() 共用同一个「名字↔路径」函数,以 ADR-0008 §10 为准(两层、.json);不合规范的文件不再出现在 listNames() 里。四棱同向,零拉动,①与④叠加。
          回退:B(反向统一) —— 若维护者认定嵌套布局是要保的产品能力,则反过来统一:list()crm/accountfindFile() 认路径形名字,并同一批改掉 ADR-0008 §10 与 parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。
          置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外 .ts 那一行的机制已被本轮更正(见上),.js/.ts 该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。

          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

              finding(metadata): FilesystemLoader.list() reports names that findFile()/load()/exists() cannot resolve — nested paths and extension-less files are listed but unreachable, and .js/.ts files are listed but never loaded #14486

              Description

              @os-musk

              Filed unassigned by the domain:engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68) from the #14341 stop-and-report (triage 5503567681 designated this disagreement "the deliverable's first finding"). Recorded here because its repair moves consumer-visible output and is not the #14341 deliverable. Left ungraded — domain:* / type / priority are triage's.

              Measured (read-only, origin/main @ 253da34c4, packages/metadata/src/loaders/filesystem-loader.ts)

              FilesystemLoader derives a metadata name from a path in two places that do not agree for every shape loadMany() reads:

              • list(type) (:243) walks the type directory and reports path.basename(file, ext) for every file, nested or not, with or without an extension.
              • findFile(type, name) (:365) resolves ROOT/TYPE/NAME + one of five tried extensions (.json, .yaml, .yml, .ts, .js).

              Probe fixture ROOT/object/{flat.json, nameless.json, dotted.config.json, yamlish.yaml, yamlish2.yml, crm/account.json, crm/nameless-nested.json, noext}:

              shapelist('object') reportsfindFile() / stat() / exists()loadMany()
              flat file, tried extension (incl. dotted.config.jsondotted.config)the basenameresolves ✔returns the body ✔
              nestedcrm/account.jsonaccountfindFile('object','account')null; only crm/account resolvesreturns the body ✔
              extension-lessnoextnoextnull (every tried extension appended)returns the body ✔ (detectFormat falls through to json)
              .js / .ts under the manager's DEFAULT format set (typescript, json, yaml)listedresolves ✔never returned (no javascript serializer; the .ts file was likewise not returned)

              Consequence at the manager: MetadataManager.listNames() (metadata-manager.ts:1586, reads loader.list()) reports account and noext, and MetadataManager.get() for either name cannot fetch it. The .js/.ts row is a membership mismatch (listed and resolvable, not loaded) rather than a key mismatch.

              Why it is a card of its own

              #14341 (loadManyKeyed() for this loader) was ruled to key items only where the derivation is already a bijection (flat file, tried extension) and to carry the pre-#14205body.name behaviour for the disagreeing shapes — it deliberately does not mint keys the loader cannot resolve. Repairing the disagreement itself means one shared name-to-path function used by list(), findFile() and loadManyKeyed() alike, and that moves listNames() output (nested files would report crm/account or stop being listed; extension-less files would stop being listed or become resolvable) — consumer-visible, so it needs its own ruling on which side is the contract. The canonical layout in packages/metadata-fs/src/layout.ts (ADR-0008 §10: ROOT/TYPE/NAME.json, parseItemPath() refuses other shapes) is the evidence for the "stop listing them" side; the fact that loadMany() reads them today is the evidence for the other.

              Not claimed

              • No measured production tree carrying a nested or extension-less metadata file. The disagreement is real on any tree that has one.
              • No repair proposed beyond naming the shape (C above).

              Duplicate search (one targeted search_issues, channel proven live by returning #14341): no existing card names this disagreement. Related: #14341 · #14205 · #13913 · #14342 (@objectstack/metadata has no typecheck script — the test layer of any repair here is NOT MEASURED until that lands).


              Triage addendum — re-measured at ed44512, plus one correction to row 4

              Rows 1–3 and the manager consequence reproduce exactly at current main; I did not take them on trust.

              • filesystem-loader.ts:243-258list() globs **/* (nodir: true) and returns path.basename(file, ext), discarding the directory. Nested and extension-less files are both in.
              • filesystem-loader.ts:365-381findFile() tries ${name}${ext} for exactly ['.json','.yaml','.yml','.ts','.js'] against ROOT/TYPE. No extension-less fallback, no nested resolution unless the caller already spells crm/account.
              • metadata-manager.ts:1585-1588listNames() unions every loader's list() output straight into the returned set, so an unresolvable name surfaces to the manager's consumers unfiltered.
              • metadata-fs/src/layout.ts:37-50parseItemPath() returns null unless the relative path is exactly two segments and ends .json. The card's ADR-0008 §10 evidence holds verbatim, and it is the sharper half of the story: two halves of this codebase already disagree about whether a nested metadata file is legal at all, independently of the list/find mismatch.

              Correction — row 4 states one mechanism for two different shapes, and it only fits .js.

              • .js: right. metadata-manager.ts:554 defaults formats to ['typescript','json','yaml'], and :565-566 registers the javascript serializer only when formats.includes('javascript'). Under the default set there is no javascript serializer, so a resolvable .js file is never returned. Mechanism confirmed.
              • .ts: not explained by "no serializer". :562-563 registers new TypeScriptSerializer('typescript') under the default set, and typescript-serializer.ts:30-60 does deserialize — it finds export const or export default, brace-matches the object literal, and parses it. A well-formed .ts metadata module should therefore load. The probe's .ts miss is real as an observation but unexplained as stated; the likeliest cause is a fixture without an export const / export default pattern, in which case deserialize throws and the loader's catch (:229-240) logs Failed to load and returns null — dropped silently from loadMany().

              ⇒ Whoever takes this must re-measure the .ts row with a fixture carrying the export pattern before designing anything for it. As stated, an implementer would go add a javascript serializer and either fix .ts by accident or not at all. The silent-drop-on-parse-failure tolerance that this uncovered is a separate shape; if it survives re-measurement it earns its own card rather than riding in here.

              Why this is needs-user-decision

              Both repairs move listNames() output for anyone whose tree has one of these files, and the direction call has an ADR on one side of it (ADR-0008 §10 already says two segments, .json only) and working loadMany() behaviour on the other. Picking which half of the codebase is wrong is a contract direction, not a bug fix — human floor. The seat can measure both sides; it cannot choose.

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

              • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 平台已经有一份写死的目录规范:类型一层、名字一层、只认 .json(metadata-fs 那边就是照它拒绝别的形状)。而这个加载器的清单函数把任意深度、任意后缀的文件全捞出来。同一个产品里两套目录规矩同时活着,这本身就是特例增生。 缩小特例的方向 = 让清单向既有规范靠拢,而不是再把解析侧加宽去追清单 —— 加宽等于把「路径形名字」这个新概念推给每一个下游消费者,永久多一层。
              • ② 实际业务拉动 —— 今天零实测:没有量到任何一棵生产树里放着嵌套或无后缀的元数据文件(卡面自己也这么说)。没有客户在撞。按分歧推荐序,零拉动⇒荐④不扩散,与①同向,两棱都指向收窄而不是加宽。
              • ③ 防 AI 犯错 —— 出错时谁看到什么:名字明明在清单里,取详情却是空的,一声不响。作者(人或 AI)会以为是自己拼错了名字,反复重试同一个词。收窄成「不合规范的文件根本不进清单」,清单与详情从此给同一个答案;加宽成清单报 crm/account,则名字里开始出现斜杠,每个消费者都得学会处理 —— 静默不一致换成了广播式复杂度。
              • ④ 创业阶段不扩散 —— 支持嵌套目录、支持无后缀文件、支持 .js 读取,是三条各自独立的永久义务,每条都要测试、要文档、要向后兼容。今天没有任何一个消费者提出过这三条需求中的任何一条。remove 优于 declare-and-maintain。

              推荐:A(收窄) —— list()findFile()loadManyKeyed() 共用同一个「名字↔路径」函数,以 ADR-0008 §10 为准(两层、.json);不合规范的文件不再出现在 listNames() 里。四棱同向,零拉动,①与④叠加。
              回退:B(反向统一) —— 若维护者认定嵌套布局是要保的产品能力,则反过来统一:list()crm/accountfindFile() 认路径形名字,并同一批改掉 ADR-0008 §10 与 parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。
              置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外 .ts 那一行的机制已被本轮更正(见上),.js/.ts 该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。

              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

                  finding(metadata): FilesystemLoader.list() reports names that findFile()/load()/exists() cannot resolve — nested paths and extension-less files are listed but unreachable, and .js/.ts files are listed but never loaded #14486

                  Description

                  @os-musk

                  Filed unassigned by the domain:engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68) from the #14341 stop-and-report (triage 5503567681 designated this disagreement "the deliverable's first finding"). Recorded here because its repair moves consumer-visible output and is not the #14341 deliverable. Left ungraded — domain:* / type / priority are triage's.

                  Measured (read-only, origin/main @ 253da34c4, packages/metadata/src/loaders/filesystem-loader.ts)

                  FilesystemLoader derives a metadata name from a path in two places that do not agree for every shape loadMany() reads:

                  • list(type) (:243) walks the type directory and reports path.basename(file, ext) for every file, nested or not, with or without an extension.
                  • findFile(type, name) (:365) resolves ROOT/TYPE/NAME + one of five tried extensions (.json, .yaml, .yml, .ts, .js).

                  Probe fixture ROOT/object/{flat.json, nameless.json, dotted.config.json, yamlish.yaml, yamlish2.yml, crm/account.json, crm/nameless-nested.json, noext}:

                  shapelist('object') reportsfindFile() / stat() / exists()loadMany()
                  flat file, tried extension (incl. dotted.config.jsondotted.config)the basenameresolves ✔returns the body ✔
                  nestedcrm/account.jsonaccountfindFile('object','account')null; only crm/account resolvesreturns the body ✔
                  extension-lessnoextnoextnull (every tried extension appended)returns the body ✔ (detectFormat falls through to json)
                  .js / .ts under the manager's DEFAULT format set (typescript, json, yaml)listedresolves ✔never returned (no javascript serializer; the .ts file was likewise not returned)

                  Consequence at the manager: MetadataManager.listNames() (metadata-manager.ts:1586, reads loader.list()) reports account and noext, and MetadataManager.get() for either name cannot fetch it. The .js/.ts row is a membership mismatch (listed and resolvable, not loaded) rather than a key mismatch.

                  Why it is a card of its own

                  #14341 (loadManyKeyed() for this loader) was ruled to key items only where the derivation is already a bijection (flat file, tried extension) and to carry the pre-#14205body.name behaviour for the disagreeing shapes — it deliberately does not mint keys the loader cannot resolve. Repairing the disagreement itself means one shared name-to-path function used by list(), findFile() and loadManyKeyed() alike, and that moves listNames() output (nested files would report crm/account or stop being listed; extension-less files would stop being listed or become resolvable) — consumer-visible, so it needs its own ruling on which side is the contract. The canonical layout in packages/metadata-fs/src/layout.ts (ADR-0008 §10: ROOT/TYPE/NAME.json, parseItemPath() refuses other shapes) is the evidence for the "stop listing them" side; the fact that loadMany() reads them today is the evidence for the other.

                  Not claimed

                  • No measured production tree carrying a nested or extension-less metadata file. The disagreement is real on any tree that has one.
                  • No repair proposed beyond naming the shape (C above).

                  Duplicate search (one targeted search_issues, channel proven live by returning #14341): no existing card names this disagreement. Related: #14341 · #14205 · #13913 · #14342 (@objectstack/metadata has no typecheck script — the test layer of any repair here is NOT MEASURED until that lands).


                  Triage addendum — re-measured at ed44512, plus one correction to row 4

                  Rows 1–3 and the manager consequence reproduce exactly at current main; I did not take them on trust.

                  • filesystem-loader.ts:243-258list() globs **/* (nodir: true) and returns path.basename(file, ext), discarding the directory. Nested and extension-less files are both in.
                  • filesystem-loader.ts:365-381findFile() tries ${name}${ext} for exactly ['.json','.yaml','.yml','.ts','.js'] against ROOT/TYPE. No extension-less fallback, no nested resolution unless the caller already spells crm/account.
                  • metadata-manager.ts:1585-1588listNames() unions every loader's list() output straight into the returned set, so an unresolvable name surfaces to the manager's consumers unfiltered.
                  • metadata-fs/src/layout.ts:37-50parseItemPath() returns null unless the relative path is exactly two segments and ends .json. The card's ADR-0008 §10 evidence holds verbatim, and it is the sharper half of the story: two halves of this codebase already disagree about whether a nested metadata file is legal at all, independently of the list/find mismatch.

                  Correction — row 4 states one mechanism for two different shapes, and it only fits .js.

                  • .js: right. metadata-manager.ts:554 defaults formats to ['typescript','json','yaml'], and :565-566 registers the javascript serializer only when formats.includes('javascript'). Under the default set there is no javascript serializer, so a resolvable .js file is never returned. Mechanism confirmed.
                  • .ts: not explained by "no serializer". :562-563 registers new TypeScriptSerializer('typescript') under the default set, and typescript-serializer.ts:30-60 does deserialize — it finds export const or export default, brace-matches the object literal, and parses it. A well-formed .ts metadata module should therefore load. The probe's .ts miss is real as an observation but unexplained as stated; the likeliest cause is a fixture without an export const / export default pattern, in which case deserialize throws and the loader's catch (:229-240) logs Failed to load and returns null — dropped silently from loadMany().

                  ⇒ Whoever takes this must re-measure the .ts row with a fixture carrying the export pattern before designing anything for it. As stated, an implementer would go add a javascript serializer and either fix .ts by accident or not at all. The silent-drop-on-parse-failure tolerance that this uncovered is a separate shape; if it survives re-measurement it earns its own card rather than riding in here.

                  Why this is needs-user-decision

                  Both repairs move listNames() output for anyone whose tree has one of these files, and the direction call has an ADR on one side of it (ADR-0008 §10 already says two segments, .json only) and working loadMany() behaviour on the other. Picking which half of the codebase is wrong is a contract direction, not a bug fix — human floor. The seat can measure both sides; it cannot choose.

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

                  • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 平台已经有一份写死的目录规范:类型一层、名字一层、只认 .json(metadata-fs 那边就是照它拒绝别的形状)。而这个加载器的清单函数把任意深度、任意后缀的文件全捞出来。同一个产品里两套目录规矩同时活着,这本身就是特例增生。 缩小特例的方向 = 让清单向既有规范靠拢,而不是再把解析侧加宽去追清单 —— 加宽等于把「路径形名字」这个新概念推给每一个下游消费者,永久多一层。
                  • ② 实际业务拉动 —— 今天零实测:没有量到任何一棵生产树里放着嵌套或无后缀的元数据文件(卡面自己也这么说)。没有客户在撞。按分歧推荐序,零拉动⇒荐④不扩散,与①同向,两棱都指向收窄而不是加宽。
                  • ③ 防 AI 犯错 —— 出错时谁看到什么:名字明明在清单里,取详情却是空的,一声不响。作者(人或 AI)会以为是自己拼错了名字,反复重试同一个词。收窄成「不合规范的文件根本不进清单」,清单与详情从此给同一个答案;加宽成清单报 crm/account,则名字里开始出现斜杠,每个消费者都得学会处理 —— 静默不一致换成了广播式复杂度。
                  • ④ 创业阶段不扩散 —— 支持嵌套目录、支持无后缀文件、支持 .js 读取,是三条各自独立的永久义务,每条都要测试、要文档、要向后兼容。今天没有任何一个消费者提出过这三条需求中的任何一条。remove 优于 declare-and-maintain。

                  推荐:A(收窄) —— list()findFile()loadManyKeyed() 共用同一个「名字↔路径」函数,以 ADR-0008 §10 为准(两层、.json);不合规范的文件不再出现在 listNames() 里。四棱同向,零拉动,①与④叠加。
                  回退:B(反向统一) —— 若维护者认定嵌套布局是要保的产品能力,则反过来统一:list()crm/accountfindFile() 认路径形名字,并同一批改掉 ADR-0008 §10 与 parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。
                  置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外 .ts 那一行的机制已被本轮更正(见上),.js/.ts 该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。

                  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

                      finding(metadata): FilesystemLoader.list() reports names that findFile()/load()/exists() cannot resolve — nested paths and extension-less files are listed but unreachable, and .js/.ts files are listed but never loaded #14486

                      Description

                      @os-musk

                      Filed unassigned by the domain:engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68) from the #14341 stop-and-report (triage 5503567681 designated this disagreement "the deliverable's first finding"). Recorded here because its repair moves consumer-visible output and is not the #14341 deliverable. Left ungraded — domain:* / type / priority are triage's.

                      Measured (read-only, origin/main @ 253da34c4, packages/metadata/src/loaders/filesystem-loader.ts)

                      FilesystemLoader derives a metadata name from a path in two places that do not agree for every shape loadMany() reads:

                      • list(type) (:243) walks the type directory and reports path.basename(file, ext) for every file, nested or not, with or without an extension.
                      • findFile(type, name) (:365) resolves ROOT/TYPE/NAME + one of five tried extensions (.json, .yaml, .yml, .ts, .js).

                      Probe fixture ROOT/object/{flat.json, nameless.json, dotted.config.json, yamlish.yaml, yamlish2.yml, crm/account.json, crm/nameless-nested.json, noext}:

                      shapelist('object') reportsfindFile() / stat() / exists()loadMany()
                      flat file, tried extension (incl. dotted.config.jsondotted.config)the basenameresolves ✔returns the body ✔
                      nestedcrm/account.jsonaccountfindFile('object','account')null; only crm/account resolvesreturns the body ✔
                      extension-lessnoextnoextnull (every tried extension appended)returns the body ✔ (detectFormat falls through to json)
                      .js / .ts under the manager's DEFAULT format set (typescript, json, yaml)listedresolves ✔never returned (no javascript serializer; the .ts file was likewise not returned)

                      Consequence at the manager: MetadataManager.listNames() (metadata-manager.ts:1586, reads loader.list()) reports account and noext, and MetadataManager.get() for either name cannot fetch it. The .js/.ts row is a membership mismatch (listed and resolvable, not loaded) rather than a key mismatch.

                      Why it is a card of its own

                      #14341 (loadManyKeyed() for this loader) was ruled to key items only where the derivation is already a bijection (flat file, tried extension) and to carry the pre-#14205body.name behaviour for the disagreeing shapes — it deliberately does not mint keys the loader cannot resolve. Repairing the disagreement itself means one shared name-to-path function used by list(), findFile() and loadManyKeyed() alike, and that moves listNames() output (nested files would report crm/account or stop being listed; extension-less files would stop being listed or become resolvable) — consumer-visible, so it needs its own ruling on which side is the contract. The canonical layout in packages/metadata-fs/src/layout.ts (ADR-0008 §10: ROOT/TYPE/NAME.json, parseItemPath() refuses other shapes) is the evidence for the "stop listing them" side; the fact that loadMany() reads them today is the evidence for the other.

                      Not claimed

                      • No measured production tree carrying a nested or extension-less metadata file. The disagreement is real on any tree that has one.
                      • No repair proposed beyond naming the shape (C above).

                      Duplicate search (one targeted search_issues, channel proven live by returning #14341): no existing card names this disagreement. Related: #14341 · #14205 · #13913 · #14342 (@objectstack/metadata has no typecheck script — the test layer of any repair here is NOT MEASURED until that lands).


                      Triage addendum — re-measured at ed44512, plus one correction to row 4

                      Rows 1–3 and the manager consequence reproduce exactly at current main; I did not take them on trust.

                      • filesystem-loader.ts:243-258list() globs **/* (nodir: true) and returns path.basename(file, ext), discarding the directory. Nested and extension-less files are both in.
                      • filesystem-loader.ts:365-381findFile() tries ${name}${ext} for exactly ['.json','.yaml','.yml','.ts','.js'] against ROOT/TYPE. No extension-less fallback, no nested resolution unless the caller already spells crm/account.
                      • metadata-manager.ts:1585-1588listNames() unions every loader's list() output straight into the returned set, so an unresolvable name surfaces to the manager's consumers unfiltered.
                      • metadata-fs/src/layout.ts:37-50parseItemPath() returns null unless the relative path is exactly two segments and ends .json. The card's ADR-0008 §10 evidence holds verbatim, and it is the sharper half of the story: two halves of this codebase already disagree about whether a nested metadata file is legal at all, independently of the list/find mismatch.

                      Correction — row 4 states one mechanism for two different shapes, and it only fits .js.

                      • .js: right. metadata-manager.ts:554 defaults formats to ['typescript','json','yaml'], and :565-566 registers the javascript serializer only when formats.includes('javascript'). Under the default set there is no javascript serializer, so a resolvable .js file is never returned. Mechanism confirmed.
                      • .ts: not explained by "no serializer". :562-563 registers new TypeScriptSerializer('typescript') under the default set, and typescript-serializer.ts:30-60 does deserialize — it finds export const or export default, brace-matches the object literal, and parses it. A well-formed .ts metadata module should therefore load. The probe's .ts miss is real as an observation but unexplained as stated; the likeliest cause is a fixture without an export const / export default pattern, in which case deserialize throws and the loader's catch (:229-240) logs Failed to load and returns null — dropped silently from loadMany().

                      ⇒ Whoever takes this must re-measure the .ts row with a fixture carrying the export pattern before designing anything for it. As stated, an implementer would go add a javascript serializer and either fix .ts by accident or not at all. The silent-drop-on-parse-failure tolerance that this uncovered is a separate shape; if it survives re-measurement it earns its own card rather than riding in here.

                      Why this is needs-user-decision

                      Both repairs move listNames() output for anyone whose tree has one of these files, and the direction call has an ADR on one side of it (ADR-0008 §10 already says two segments, .json only) and working loadMany() behaviour on the other. Picking which half of the codebase is wrong is a contract direction, not a bug fix — human floor. The seat can measure both sides; it cannot choose.

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

                      • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 平台已经有一份写死的目录规范:类型一层、名字一层、只认 .json(metadata-fs 那边就是照它拒绝别的形状)。而这个加载器的清单函数把任意深度、任意后缀的文件全捞出来。同一个产品里两套目录规矩同时活着,这本身就是特例增生。 缩小特例的方向 = 让清单向既有规范靠拢,而不是再把解析侧加宽去追清单 —— 加宽等于把「路径形名字」这个新概念推给每一个下游消费者,永久多一层。
                      • ② 实际业务拉动 —— 今天零实测:没有量到任何一棵生产树里放着嵌套或无后缀的元数据文件(卡面自己也这么说)。没有客户在撞。按分歧推荐序,零拉动⇒荐④不扩散,与①同向,两棱都指向收窄而不是加宽。
                      • ③ 防 AI 犯错 —— 出错时谁看到什么:名字明明在清单里,取详情却是空的,一声不响。作者(人或 AI)会以为是自己拼错了名字,反复重试同一个词。收窄成「不合规范的文件根本不进清单」,清单与详情从此给同一个答案;加宽成清单报 crm/account,则名字里开始出现斜杠,每个消费者都得学会处理 —— 静默不一致换成了广播式复杂度。
                      • ④ 创业阶段不扩散 —— 支持嵌套目录、支持无后缀文件、支持 .js 读取,是三条各自独立的永久义务,每条都要测试、要文档、要向后兼容。今天没有任何一个消费者提出过这三条需求中的任何一条。remove 优于 declare-and-maintain。

                      推荐:A(收窄) —— list()findFile()loadManyKeyed() 共用同一个「名字↔路径」函数,以 ADR-0008 §10 为准(两层、.json);不合规范的文件不再出现在 listNames() 里。四棱同向,零拉动,①与④叠加。
                      回退:B(反向统一) —— 若维护者认定嵌套布局是要保的产品能力,则反过来统一:list()crm/accountfindFile() 认路径形名字,并同一批改掉 ADR-0008 §10 与 parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。
                      置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外 .ts 那一行的机制已被本轮更正(见上),.js/.ts 该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。

                      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

                          finding(metadata): FilesystemLoader.list() reports names that findFile()/load()/exists() cannot resolve — nested paths and extension-less files are listed but unreachable, and .js/.ts files are listed but never loaded #14486

                          Description

                          @os-musk

                          Filed unassigned by the domain:engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68) from the #14341 stop-and-report (triage 5503567681 designated this disagreement "the deliverable's first finding"). Recorded here because its repair moves consumer-visible output and is not the #14341 deliverable. Left ungraded — domain:* / type / priority are triage's.

                          Measured (read-only, origin/main @ 253da34c4, packages/metadata/src/loaders/filesystem-loader.ts)

                          FilesystemLoader derives a metadata name from a path in two places that do not agree for every shape loadMany() reads:

                          • list(type) (:243) walks the type directory and reports path.basename(file, ext) for every file, nested or not, with or without an extension.
                          • findFile(type, name) (:365) resolves ROOT/TYPE/NAME + one of five tried extensions (.json, .yaml, .yml, .ts, .js).

                          Probe fixture ROOT/object/{flat.json, nameless.json, dotted.config.json, yamlish.yaml, yamlish2.yml, crm/account.json, crm/nameless-nested.json, noext}:

                          shapelist('object') reportsfindFile() / stat() / exists()loadMany()
                          flat file, tried extension (incl. dotted.config.jsondotted.config)the basenameresolves ✔returns the body ✔
                          nestedcrm/account.jsonaccountfindFile('object','account')null; only crm/account resolvesreturns the body ✔
                          extension-lessnoextnoextnull (every tried extension appended)returns the body ✔ (detectFormat falls through to json)
                          .js / .ts under the manager's DEFAULT format set (typescript, json, yaml)listedresolves ✔never returned (no javascript serializer; the .ts file was likewise not returned)

                          Consequence at the manager: MetadataManager.listNames() (metadata-manager.ts:1586, reads loader.list()) reports account and noext, and MetadataManager.get() for either name cannot fetch it. The .js/.ts row is a membership mismatch (listed and resolvable, not loaded) rather than a key mismatch.

                          Why it is a card of its own

                          #14341 (loadManyKeyed() for this loader) was ruled to key items only where the derivation is already a bijection (flat file, tried extension) and to carry the pre-#14205body.name behaviour for the disagreeing shapes — it deliberately does not mint keys the loader cannot resolve. Repairing the disagreement itself means one shared name-to-path function used by list(), findFile() and loadManyKeyed() alike, and that moves listNames() output (nested files would report crm/account or stop being listed; extension-less files would stop being listed or become resolvable) — consumer-visible, so it needs its own ruling on which side is the contract. The canonical layout in packages/metadata-fs/src/layout.ts (ADR-0008 §10: ROOT/TYPE/NAME.json, parseItemPath() refuses other shapes) is the evidence for the "stop listing them" side; the fact that loadMany() reads them today is the evidence for the other.

                          Not claimed

                          • No measured production tree carrying a nested or extension-less metadata file. The disagreement is real on any tree that has one.
                          • No repair proposed beyond naming the shape (C above).

                          Duplicate search (one targeted search_issues, channel proven live by returning #14341): no existing card names this disagreement. Related: #14341 · #14205 · #13913 · #14342 (@objectstack/metadata has no typecheck script — the test layer of any repair here is NOT MEASURED until that lands).


                          Triage addendum — re-measured at ed44512, plus one correction to row 4

                          Rows 1–3 and the manager consequence reproduce exactly at current main; I did not take them on trust.

                          • filesystem-loader.ts:243-258list() globs **/* (nodir: true) and returns path.basename(file, ext), discarding the directory. Nested and extension-less files are both in.
                          • filesystem-loader.ts:365-381findFile() tries ${name}${ext} for exactly ['.json','.yaml','.yml','.ts','.js'] against ROOT/TYPE. No extension-less fallback, no nested resolution unless the caller already spells crm/account.
                          • metadata-manager.ts:1585-1588listNames() unions every loader's list() output straight into the returned set, so an unresolvable name surfaces to the manager's consumers unfiltered.
                          • metadata-fs/src/layout.ts:37-50parseItemPath() returns null unless the relative path is exactly two segments and ends .json. The card's ADR-0008 §10 evidence holds verbatim, and it is the sharper half of the story: two halves of this codebase already disagree about whether a nested metadata file is legal at all, independently of the list/find mismatch.

                          Correction — row 4 states one mechanism for two different shapes, and it only fits .js.

                          • .js: right. metadata-manager.ts:554 defaults formats to ['typescript','json','yaml'], and :565-566 registers the javascript serializer only when formats.includes('javascript'). Under the default set there is no javascript serializer, so a resolvable .js file is never returned. Mechanism confirmed.
                          • .ts: not explained by "no serializer". :562-563 registers new TypeScriptSerializer('typescript') under the default set, and typescript-serializer.ts:30-60 does deserialize — it finds export const or export default, brace-matches the object literal, and parses it. A well-formed .ts metadata module should therefore load. The probe's .ts miss is real as an observation but unexplained as stated; the likeliest cause is a fixture without an export const / export default pattern, in which case deserialize throws and the loader's catch (:229-240) logs Failed to load and returns null — dropped silently from loadMany().

                          ⇒ Whoever takes this must re-measure the .ts row with a fixture carrying the export pattern before designing anything for it. As stated, an implementer would go add a javascript serializer and either fix .ts by accident or not at all. The silent-drop-on-parse-failure tolerance that this uncovered is a separate shape; if it survives re-measurement it earns its own card rather than riding in here.

                          Why this is needs-user-decision

                          Both repairs move listNames() output for anyone whose tree has one of these files, and the direction call has an ADR on one side of it (ADR-0008 §10 already says two segments, .json only) and working loadMany() behaviour on the other. Picking which half of the codebase is wrong is a contract direction, not a bug fix — human floor. The seat can measure both sides; it cannot choose.

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

                          • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 平台已经有一份写死的目录规范:类型一层、名字一层、只认 .json(metadata-fs 那边就是照它拒绝别的形状)。而这个加载器的清单函数把任意深度、任意后缀的文件全捞出来。同一个产品里两套目录规矩同时活着,这本身就是特例增生。 缩小特例的方向 = 让清单向既有规范靠拢,而不是再把解析侧加宽去追清单 —— 加宽等于把「路径形名字」这个新概念推给每一个下游消费者,永久多一层。
                          • ② 实际业务拉动 —— 今天零实测:没有量到任何一棵生产树里放着嵌套或无后缀的元数据文件(卡面自己也这么说)。没有客户在撞。按分歧推荐序,零拉动⇒荐④不扩散,与①同向,两棱都指向收窄而不是加宽。
                          • ③ 防 AI 犯错 —— 出错时谁看到什么:名字明明在清单里,取详情却是空的,一声不响。作者(人或 AI)会以为是自己拼错了名字,反复重试同一个词。收窄成「不合规范的文件根本不进清单」,清单与详情从此给同一个答案;加宽成清单报 crm/account,则名字里开始出现斜杠,每个消费者都得学会处理 —— 静默不一致换成了广播式复杂度。
                          • ④ 创业阶段不扩散 —— 支持嵌套目录、支持无后缀文件、支持 .js 读取,是三条各自独立的永久义务,每条都要测试、要文档、要向后兼容。今天没有任何一个消费者提出过这三条需求中的任何一条。remove 优于 declare-and-maintain。

                          推荐:A(收窄) —— list()findFile()loadManyKeyed() 共用同一个「名字↔路径」函数,以 ADR-0008 §10 为准(两层、.json);不合规范的文件不再出现在 listNames() 里。四棱同向,零拉动,①与④叠加。
                          回退:B(反向统一) —— 若维护者认定嵌套布局是要保的产品能力,则反过来统一:list()crm/accountfindFile() 认路径形名字,并同一批改掉 ADR-0008 §10 与 parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。
                          置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外 .ts 那一行的机制已被本轮更正(见上),.js/.ts 该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。

                          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

                              finding(metadata): FilesystemLoader.list() reports names that findFile()/load()/exists() cannot resolve — nested paths and extension-less files are listed but unreachable, and .js/.ts files are listed but never loaded #14486

                              Description

                              @os-musk

                              Filed unassigned by the domain:engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68) from the #14341 stop-and-report (triage 5503567681 designated this disagreement "the deliverable's first finding"). Recorded here because its repair moves consumer-visible output and is not the #14341 deliverable. Left ungraded — domain:* / type / priority are triage's.

                              Measured (read-only, origin/main @ 253da34c4, packages/metadata/src/loaders/filesystem-loader.ts)

                              FilesystemLoader derives a metadata name from a path in two places that do not agree for every shape loadMany() reads:

                              • list(type) (:243) walks the type directory and reports path.basename(file, ext) for every file, nested or not, with or without an extension.
                              • findFile(type, name) (:365) resolves ROOT/TYPE/NAME + one of five tried extensions (.json, .yaml, .yml, .ts, .js).

                              Probe fixture ROOT/object/{flat.json, nameless.json, dotted.config.json, yamlish.yaml, yamlish2.yml, crm/account.json, crm/nameless-nested.json, noext}:

                              shapelist('object') reportsfindFile() / stat() / exists()loadMany()
                              flat file, tried extension (incl. dotted.config.jsondotted.config)the basenameresolves ✔returns the body ✔
                              nestedcrm/account.jsonaccountfindFile('object','account')null; only crm/account resolvesreturns the body ✔
                              extension-lessnoextnoextnull (every tried extension appended)returns the body ✔ (detectFormat falls through to json)
                              .js / .ts under the manager's DEFAULT format set (typescript, json, yaml)listedresolves ✔never returned (no javascript serializer; the .ts file was likewise not returned)

                              Consequence at the manager: MetadataManager.listNames() (metadata-manager.ts:1586, reads loader.list()) reports account and noext, and MetadataManager.get() for either name cannot fetch it. The .js/.ts row is a membership mismatch (listed and resolvable, not loaded) rather than a key mismatch.

                              Why it is a card of its own

                              #14341 (loadManyKeyed() for this loader) was ruled to key items only where the derivation is already a bijection (flat file, tried extension) and to carry the pre-#14205body.name behaviour for the disagreeing shapes — it deliberately does not mint keys the loader cannot resolve. Repairing the disagreement itself means one shared name-to-path function used by list(), findFile() and loadManyKeyed() alike, and that moves listNames() output (nested files would report crm/account or stop being listed; extension-less files would stop being listed or become resolvable) — consumer-visible, so it needs its own ruling on which side is the contract. The canonical layout in packages/metadata-fs/src/layout.ts (ADR-0008 §10: ROOT/TYPE/NAME.json, parseItemPath() refuses other shapes) is the evidence for the "stop listing them" side; the fact that loadMany() reads them today is the evidence for the other.

                              Not claimed

                              • No measured production tree carrying a nested or extension-less metadata file. The disagreement is real on any tree that has one.
                              • No repair proposed beyond naming the shape (C above).

                              Duplicate search (one targeted search_issues, channel proven live by returning #14341): no existing card names this disagreement. Related: #14341 · #14205 · #13913 · #14342 (@objectstack/metadata has no typecheck script — the test layer of any repair here is NOT MEASURED until that lands).


                              Triage addendum — re-measured at ed44512, plus one correction to row 4

                              Rows 1–3 and the manager consequence reproduce exactly at current main; I did not take them on trust.

                              • filesystem-loader.ts:243-258list() globs **/* (nodir: true) and returns path.basename(file, ext), discarding the directory. Nested and extension-less files are both in.
                              • filesystem-loader.ts:365-381findFile() tries ${name}${ext} for exactly ['.json','.yaml','.yml','.ts','.js'] against ROOT/TYPE. No extension-less fallback, no nested resolution unless the caller already spells crm/account.
                              • metadata-manager.ts:1585-1588listNames() unions every loader's list() output straight into the returned set, so an unresolvable name surfaces to the manager's consumers unfiltered.
                              • metadata-fs/src/layout.ts:37-50parseItemPath() returns null unless the relative path is exactly two segments and ends .json. The card's ADR-0008 §10 evidence holds verbatim, and it is the sharper half of the story: two halves of this codebase already disagree about whether a nested metadata file is legal at all, independently of the list/find mismatch.

                              Correction — row 4 states one mechanism for two different shapes, and it only fits .js.

                              • .js: right. metadata-manager.ts:554 defaults formats to ['typescript','json','yaml'], and :565-566 registers the javascript serializer only when formats.includes('javascript'). Under the default set there is no javascript serializer, so a resolvable .js file is never returned. Mechanism confirmed.
                              • .ts: not explained by "no serializer". :562-563 registers new TypeScriptSerializer('typescript') under the default set, and typescript-serializer.ts:30-60 does deserialize — it finds export const or export default, brace-matches the object literal, and parses it. A well-formed .ts metadata module should therefore load. The probe's .ts miss is real as an observation but unexplained as stated; the likeliest cause is a fixture without an export const / export default pattern, in which case deserialize throws and the loader's catch (:229-240) logs Failed to load and returns null — dropped silently from loadMany().

                              ⇒ Whoever takes this must re-measure the .ts row with a fixture carrying the export pattern before designing anything for it. As stated, an implementer would go add a javascript serializer and either fix .ts by accident or not at all. The silent-drop-on-parse-failure tolerance that this uncovered is a separate shape; if it survives re-measurement it earns its own card rather than riding in here.

                              Why this is needs-user-decision

                              Both repairs move listNames() output for anyone whose tree has one of these files, and the direction call has an ADR on one side of it (ADR-0008 §10 already says two segments, .json only) and working loadMany() behaviour on the other. Picking which half of the codebase is wrong is a contract direction, not a bug fix — human floor. The seat can measure both sides; it cannot choose.

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

                              • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 平台已经有一份写死的目录规范:类型一层、名字一层、只认 .json(metadata-fs 那边就是照它拒绝别的形状)。而这个加载器的清单函数把任意深度、任意后缀的文件全捞出来。同一个产品里两套目录规矩同时活着,这本身就是特例增生。 缩小特例的方向 = 让清单向既有规范靠拢,而不是再把解析侧加宽去追清单 —— 加宽等于把「路径形名字」这个新概念推给每一个下游消费者,永久多一层。
                              • ② 实际业务拉动 —— 今天零实测:没有量到任何一棵生产树里放着嵌套或无后缀的元数据文件(卡面自己也这么说)。没有客户在撞。按分歧推荐序,零拉动⇒荐④不扩散,与①同向,两棱都指向收窄而不是加宽。
                              • ③ 防 AI 犯错 —— 出错时谁看到什么:名字明明在清单里,取详情却是空的,一声不响。作者(人或 AI)会以为是自己拼错了名字,反复重试同一个词。收窄成「不合规范的文件根本不进清单」,清单与详情从此给同一个答案;加宽成清单报 crm/account,则名字里开始出现斜杠,每个消费者都得学会处理 —— 静默不一致换成了广播式复杂度。
                              • ④ 创业阶段不扩散 —— 支持嵌套目录、支持无后缀文件、支持 .js 读取,是三条各自独立的永久义务,每条都要测试、要文档、要向后兼容。今天没有任何一个消费者提出过这三条需求中的任何一条。remove 优于 declare-and-maintain。

                              推荐:A(收窄) —— list()findFile()loadManyKeyed() 共用同一个「名字↔路径」函数,以 ADR-0008 §10 为准(两层、.json);不合规范的文件不再出现在 listNames() 里。四棱同向,零拉动,①与④叠加。
                              回退:B(反向统一) —— 若维护者认定嵌套布局是要保的产品能力,则反过来统一:list()crm/accountfindFile() 认路径形名字,并同一批改掉 ADR-0008 §10 与 parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。
                              置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外 .ts 那一行的机制已被本轮更正(见上),.js/.ts 该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。

                              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