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.json → dotted.config) | the basename | resolves ✔ | returns the body ✔ |
nestedcrm/account.json | account | findFile('object','account') → null; only crm/account resolves | returns the body ✔ |
extension-lessnoext | 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-#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-258 — list() globs **/* (nodir: true) and returns path.basename(file, ext), discarding the directory. Nested and extension-less files are both in.filesystem-loader.ts:365-381 — findFile() 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-1588 — listNames() 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-50 — parseItemPath() 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/account、findFile() 认路径形名字,并同一批改掉 ADR-0008 §10 与 parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。
置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外 .ts 那一行的机制已被本轮更正(见上),.js/.ts 该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。
Filed unassigned by the
domain:engineexecution seat (sessionsession_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)FilesystemLoaderderives a metadata name from a path in two places that do not agree for every shapeloadMany()reads:list(type)(:243) walks the type directory and reportspath.basename(file, ext)for every file, nested or not, with or without an extension.findFile(type, name)(:365) resolvesROOT/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}:list('object')reportsfindFile()/stat()/exists()loadMany()dotted.config.json→dotted.config)crm/account.jsonaccountfindFile('object','account')→null; onlycrm/accountresolvesnoextnoextnull(every tried extension appended)detectFormatfalls through tojson).js/.tsunder the manager's DEFAULT format set (typescript,json,yaml)javascriptserializer; the.tsfile was likewise not returned)Consequence at the manager:
MetadataManager.listNames()(metadata-manager.ts:1586, readsloader.list()) reportsaccountandnoext, andMetadataManager.get()for either name cannot fetch it. The.js/.tsrow 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.namebehaviour 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 bylist(),findFile()andloadManyKeyed()alike, and that moveslistNames()output (nested files would reportcrm/accountor 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 inpackages/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 thatloadMany()reads them today is the evidence for the other.Not claimed
Duplicate search (one targeted
search_issues, channel proven live by returning #14341): no existing card names this disagreement. Related: #14341 · #14205 · #13913 · #14342 (@objectstack/metadatahas notypecheckscript — the test layer of any repair here is NOT MEASURED until that lands).Triage addendum — re-measured at
ed44512, plus one correction to row 4Rows 1–3 and the manager consequence reproduce exactly at current
main; I did not take them on trust.filesystem-loader.ts:243-258—list()globs**/*(nodir: true) and returnspath.basename(file, ext), discarding the directory. Nested and extension-less files are both in.filesystem-loader.ts:365-381—findFile()tries${name}${ext}for exactly['.json','.yaml','.yml','.ts','.js']againstROOT/TYPE. No extension-less fallback, no nested resolution unless the caller already spellscrm/account.metadata-manager.ts:1585-1588—listNames()unions every loader'slist()output straight into the returned set, so an unresolvable name surfaces to the manager's consumers unfiltered.metadata-fs/src/layout.ts:37-50—parseItemPath()returnsnullunless 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:554defaultsformatsto['typescript','json','yaml'], and:565-566registers thejavascriptserializer only whenformats.includes('javascript'). Under the default set there is nojavascriptserializer, so a resolvable.jsfile is never returned. Mechanism confirmed..ts: not explained by "no serializer".:562-563registersnew TypeScriptSerializer('typescript')under the default set, andtypescript-serializer.ts:30-60does deserialize — it findsexport constorexport default, brace-matches the object literal, and parses it. A well-formed.tsmetadata module should therefore load. The probe's.tsmiss is real as an observation but unexplained as stated; the likeliest cause is a fixture without anexport const/export defaultpattern, in which casedeserializethrows and the loader'scatch(:229-240) logsFailed to loadand returnsnull— dropped silently fromloadMany().⇒ Whoever takes this must re-measure the
.tsrow with a fixture carrying the export pattern before designing anything for it. As stated, an implementer would go add ajavascriptserializer and either fix.tsby 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-decisionBoth 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,.jsononly) and workingloadMany()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 -->
.json(metadata-fs那边就是照它拒绝别的形状)。而这个加载器的清单函数把任意深度、任意后缀的文件全捞出来。同一个产品里两套目录规矩同时活着,这本身就是特例增生。 缩小特例的方向 = 让清单向既有规范靠拢,而不是再把解析侧加宽去追清单 —— 加宽等于把「路径形名字」这个新概念推给每一个下游消费者,永久多一层。crm/account,则名字里开始出现斜杠,每个消费者都得学会处理 —— 静默不一致换成了广播式复杂度。.js读取,是三条各自独立的永久义务,每条都要测试、要文档、要向后兼容。今天没有任何一个消费者提出过这三条需求中的任何一条。remove 优于 declare-and-maintain。推荐:A(收窄) ——
list()、findFile()、loadManyKeyed()共用同一个「名字↔路径」函数,以 ADR-0008 §10 为准(两层、.json);不合规范的文件不再出现在listNames()里。四棱同向,零拉动,①与④叠加。回退:B(反向统一) —— 若维护者认定嵌套布局是要保的产品能力,则反过来统一:
list()报crm/account、findFile()认路径形名字,并同一批改掉 ADR-0008 §10 与parseItemPath()。⛔ 不接受只改一半 —— 只改加载器会让两套规矩继续并存,那是今天这张卡本身。置信缺口(本分析看不见什么): 看不见真实客户树里到底有没有这两种文件。卡面写的是「没量到」,不是「没有」,所以 A 的「不再列出」在实测上等于零回归,但那是基于一次没有覆盖客户仓的测量。另外
.ts那一行的机制已被本轮更正(见上),.js/.ts该不该跟嵌套/无后缀同卡解决,取决于维护者把它看成同一个契约问题还是另一张卡 —— 本席按「另一张卡」的读法排的推荐,没有把它算进 A 的代价里。