You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Filed by the domain:metadata PM seat. This is the follow-up the #7856 triage ruling explicitly deferred rather than rejected, and it is filed because the implementing session drafted it but could not file it (the objectstack-ai org does not have the Claude GitHub App connected, so dev containers are blocked from the GitHub API — see #7856's PR #7992). Filing it here so a drafted card does not evaporate; a "drafted" card with no number is not a filing.
Provenance and what is already settled
#7856 (landed as PR #7992, c7e7900) fixed the coherence half: canonicalize now honours toJSON exactly as JSON.stringify does, so canonicalize(x) === canonicalize(JSON.parse(JSON.stringify(x))) holds by construction and put().version === get().hash is true again.
The triage ruling on #7856 (comment 5263973459) chose option 2 (hash the serialized form) and was explicit about this card:
Option 1 (reject non-round-tripping specs at the door) is NOT rejected on the merits — it is a separate accept-set tightening with its own blast radius; if the dev's measurement of real spec corpora shows non-round-tripping values are rare-to-zero, file the option-1 tightening as its own follow-up card (contract-first, likely domain:spec-adjacent for the authorable face) rather than riding it here.
That measurement was taken and it favours the tightening.#7992 hashed this repository's entire checked-in JSON corpus under both the old and new implementations: 1973 files, 0 hashes changed. The only positions that behave differently are those carrying a callable toJSON, and the corpus contains none.
The question this card decides
put() currently accepts a spec whose in-memory form does not round-trip through JSON. After #7992 the stored bytes and the returned version agree, so nothing is incoherent any more — but the author still gets silent lossiness: a Date they wrote is a string when read back, and a class instance is whatever its toJSON yields.
Should put()refuse such a spec, loudly, naming the offending key?
For: it matches the repo's contract-first posture ("declared = enforced"), and it is the hardest shape for an AI author to get silently wrong — the failure becomes a message at the door instead of a value that quietly changes type between write and read.
Against: it is an accept-set tightening, i.e. a breaking change for any caller that does this today. The corpus measurement bounds the blast radius for checked-in specs at zero, but it says nothing about specs constructed at runtime by consumers.
⚠️Measure the runtime constructors before assuming zero. The 1973-file result covers the on-disk corpus only. put() callers that build a spec in memory — plugins, migrations, tests, SysMetadataRepository writers — are the population this tightening would actually break, and they were not measured by #7992.
⚠️ Three open questions the drafting session flagged
Recorded as questions, not answers — this card does not pick:
What is the error's shape — a declared error code (subject to check:error-code-casing), or a thrown validation error? It must name the offending key path, not just the fact of the failure.
Is Date genuinely disallowed, or normalised? A blanket refusal of toJSON-carrying values is simple but rejects Date, which is the single most likely thing an author writes. An alternative is to refuse only values whose toJSON is lossy in a way the reader cannot invert — which is harder to define and may not be worth it.
⚠️If the choice turns on product intent unreadable from the code, that is a needs-user-decision, not a coin flip — the original card said so and it still applies.
Routing note
Filed domain:metadata because the mechanism lives in packages/metadata-core / packages/metadata-fs. ⚠️If the remedy changes the authorable acceptance face, it belongs to domain:spec (#6017) — the triage ruling anticipated this ("likely domain:spec-adjacent for the authorable face"). Re-route rather than reaching across.
Precondition
⛔ Do not start before confirming #7992 (c7e7900) is on main — this card is meaningless without it, since the coherence bug would otherwise mask the accept-set question.
Filed by the
domain:metadataPM seat. This is the follow-up the #7856 triage ruling explicitly deferred rather than rejected, and it is filed because the implementing session drafted it but could not file it (theobjectstack-aiorg does not have the Claude GitHub App connected, so dev containers are blocked from the GitHub API — see #7856's PR #7992). Filing it here so a drafted card does not evaporate; a "drafted" card with no number is not a filing.Provenance and what is already settled
#7856 (landed as PR #7992,
c7e7900) fixed the coherence half:canonicalizenow honourstoJSONexactly asJSON.stringifydoes, socanonicalize(x) === canonicalize(JSON.parse(JSON.stringify(x)))holds by construction andput().version === get().hashis true again.The triage ruling on #7856 (comment
5263973459) chose option 2 (hash the serialized form) and was explicit about this card:That measurement was taken and it favours the tightening.#7992 hashed this repository's entire checked-in JSON corpus under both the old and new implementations: 1973 files, 0 hashes changed. The only positions that behave differently are those carrying a callable
toJSON, and the corpus contains none.The question this card decides
put()currently accepts a spec whose in-memory form does not round-trip through JSON. After #7992 the stored bytes and the returned version agree, so nothing is incoherent any more — but the author still gets silent lossiness: aDatethey wrote is a string when read back, and a class instance is whatever itstoJSONyields.Should
put()refuse such a spec, loudly, naming the offending key?For: it matches the repo's contract-first posture ("declared = enforced"), and it is the hardest shape for an AI author to get silently wrong — the failure becomes a message at the door instead of a value that quietly changes type between write and read.
Against: it is an accept-set tightening, i.e. a breaking change for any caller that does this today. The corpus measurement bounds the blast radius for checked-in specs at zero, but it says nothing about specs constructed at runtime by consumers.
put()callers that build a spec in memory — plugins, migrations, tests,SysMetadataRepositorywriters — are the population this tightening would actually break, and they were not measured by #7992.Recorded as questions, not answers — this card does not pick:
put()in each repository implementation, or the shared contract suite /hashSpeclayer that fix(metadata-core,metadata-fs): hash the serialized form, soput().versionidentifies the bytes actually stored (#7856) #7992 already unified? The latter is the natural chokepoint but changes the accept set for every implementation at once.check:error-code-casing), or a thrown validation error? It must name the offending key path, not just the fact of the failure.Dategenuinely disallowed, or normalised? A blanket refusal oftoJSON-carrying values is simple but rejectsDate, which is the single most likely thing an author writes. An alternative is to refuse only values whosetoJSONis lossy in a way the reader cannot invert — which is harder to define and may not be worth it.needs-user-decision, not a coin flip — the original card said so and it still applies.Routing note
Filed⚠️ If the remedy changes the authorable acceptance face, it belongs to
domain:metadatabecause the mechanism lives inpackages/metadata-core/packages/metadata-fs.domain:spec(#6017) — the triage ruling anticipated this ("likelydomain:spec-adjacent for the authorable face"). Re-route rather than reaching across.Precondition
⛔ Do not start before confirming #7992 (
c7e7900) is onmain— this card is meaningless without it, since the coherence bug would otherwise mask the accept-set question.