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}:

shape list('object') reports findFile() / stat() / exists() loadMany()
flat file, tried extension (incl. dotted.config.jsondotted.config) the basename resolves ✔ returns the body ✔
nested crm/account.json account findFile('object','account')null; only crm/account resolves returns the body ✔
extension-less noext noext null (every tried extension appended) returns the body ✔ (detectFormat falls through to json)
.js / .ts under the manager's DEFAULT format set (typescript, json, yaml) listed resolves ✔ 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-#14205 body.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 的代价里。

Activity

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

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