Repository navigation
Description
Context
nebari-dev/frame-spec#28 proposes Frame Spec v0.3: a Frame data model with Markdown, YAML, and JSON encodings. v0.2 remains the released, normative spec and this registry is compliant with it. This issue tracks what would change here if v0.3 lands.
The proposal's own interop table is the motivation:
| Direction | Result |
|---|---|
This registry's example Frames, checked by the spec's validate_frames.py |
4 of 4 pass |
| The spec repository's 18 example Frames, read by this registry's importer | 0 of 18 accepted |
The failures are unknown section "## Purpose", unknown field status, and human-readable names like "Editorial Style Guide" where we demand a lowercase slug.
The theme
Our parser is strict-reject. v0.3 is preserve-and-degrade.
backend/internal/frames/framemd.go states our policy directly: strict about "unknown keys, unknown section headings, malformed bullets", because "guessing would silently mangle an author's content". v0.3 forbids rejecting on all three. Its answer is not that guessing is fine, but that there is a third option we did not take: do not guess, do not reject, keep the content as guidance.
Most of the work below follows from that one difference.
Phase 1: stop rejecting what we do not recognize
Independent of Phase 2, much cheaper, and it is what unblocks importing the spec repository's examples. Recommend doing this first and re-measuring the interop table.
- Unknown top-level keys.
Parseinschema.gousesKnownFields(true). v0.3 §4.7: readers MUST preserve unknown elements and MUST NOT reject a Frame for carrying them, and writers MUST re-emit them so a round trip through a reader that does not understand an element is lossless.Docneeds somewhere to hold unrecognized keys, and both the YAML and.frame.mdwriters need to emit them. - Unknown section headings.
framemd.gorejects any##heading not inSlotTable. v0.3 §6.2: "A reader MUST NOT treat an unrecognized heading as an error." An unrecognized heading's content becomesguidance. Note the sharp edge: §6.2 also says readers MUST NOT match labels by prefix, suffix, or similarity, so "Rules of the Game" is guidance, not the Rules section. - Malformed terminology bullets.
framemd.gorejects a bullet that is not- **term**: definition. v0.3 §6.2: "A reader MUST NOT fail on the shape of a list item"; non-matching content is unstructured terminology content. §4.4 generalises this: a reader that cannot extract a refinement's structured form MUST keep the value as that refinement's content, and MUST NOT demote it toguidanceon that account. - Unregistered
visibilityvalues.validate.gorejects anything outside the four values. v0.3: a reader MUST preserve an unregistered value and MUST NOT reject the Frame for carrying one; it MAY warn. - Security rule that comes with this. §4.7: unknown elements are metadata, not content, and a reader MUST NOT present an unrecognized element's value to an AI system as guidance. The MCP composer needs to exclude preserved unknowns from what it hands the model.
Phase 2: name is doing two jobs
Breaking, needs a migration, deserves its own design pass.
v0.3 separates the human-readable title from the stable identifier, and is explicit that our current practice is non-conformant:
Implementations MUST NOT constrain
titleto an identifier syntax such as a slug; constraining it to such a syntax contradicts the term's meaning rather than narrowing it.
nameRe in validate.go is ^[a-z0-9][a-z0-9-]{0,63}$, and it is why every one of the spec repository's 18 examples fails our importer.
- Add
identifieras the stable key; maketitlefree text and drop the slug constraint. - Migrate
frames.nameand itsUNIQUE (org_id, name). - Update everything keyed on the slug:
/frames/:org/:name,nebari-frame://URIs, CLI refs, andextendsrefs. - §3.2: when a registry assigns a Frame a different identifier on import, it MUST record the prior one in
derivedFrom. This lands on the import tool in Import Frame: turn unstructured context into a structured Frame with an LLM #52.
One mercy: §6.2 keeps name as an alias for title in the Markdown encoding, so existing .frame.md files do not break. It is our constraint that has to go, not the key.
New elements
identifier, status, license, canonicalSource, derivedFrom, previousVersion, issued, and guards.
statushas registered values (draft,review,approved,deprecated,revoked) under the same preserve-do-not-reject rule asvisibility. Note we already have astatuscolumn onframe_versionsserving a different purpose (the review-gate hook); these are not the same thing.guardscarries a real composition rule: values accumulate, and a Guard declared by any Frame in the composed set applies to the result, so a child cannot drop a parent's Guard by omitting it.changelogprobably maps toversionNotes, andframe_versions.digesttochecksum.
Also owed
- A conformance profile. §7 makes publishing one mandatory. It must declare whether we resolve composition transitively, and it must declare our
mergeTermsreplace-by-key behaviour: §5.1 rule 6 permits that narrowing but only if declared. - A JSON encoding (§6.4), plus the media types in §10.
Already compliant
Worth knowing how much we get for free:
- §5.1 rule 7, an unresolvable parent must not be silently ignored:
ErrParentUnreadablealready does this. - §5.1 rule 8, cycle detection:
CycleError. - §5.1 rule 5, elements describing the Frame itself are never inherited:
resolver.goalready carriesName,Description,Version,Visibility,ScopeandMaintainerstraight through rather than merging them from parents. maintaineralready matches the schema.org term the draft points at.- Our ten slots are the whitepaper's ten categories, which is where §4.4's section labels come from.
Out of scope
Frame templates. They are an authoring affordance local to this implementation and deliberately not a spec concept (see docs/adr/0001-frame-templates-are-not-frames.md), so v0.3 does not constrain the template model, its field rules, or its levels.
Caveat
v0.3 is a working draft and nebari-dev/frame-spec#28 is open. Nothing here should be built against it until it settles. The design reasoning behind the draft is in #83.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Start date
Target date
Size
Activity
dcmcand commentedon Sep 11, 2026
Re-checked this against the current draft on
spec/v0.3-working-draft. Two things worth recording.Section numbers have shifted. The draft has been revised repeatedly since this issue was written. Extensions are now §4.6 (not §4.7), the body-structure rules are §6.2.2 (not §6.2), and the dumb-down rule is §4.4.1.
Measured baseline: 0 of 18. Ran the spec repo's examples through
UnmarshalMarkdowndirectly. Confirms the interop table; every failure is one of the Phase 1 items. This is the number to re-run after each fix.Nine items this issue doesn't yet cover. Two are behavioural bugs rather than strictness:
mergeStringsdedupes keeping the last occurrence; §5.1 rule 6 requires the first, so the merged list stays a subsequence of the concatenation. Parent[X, Y]+ child[X]gives us[Y, X]; the spec says[X, Y].name/inheritsare accepted on read. §6.2.1: "A reader MUST accept either spelling."type: frame [0.3.1]is rejected. §6.2.1: accept any value beginning withframe, warn on another token shape.typeis reported as a Frame in error. §3.3 requires a reader to distinguish "not a Frame" from "a Frame in error".descriptionis required and capped at 280 characters. §4.3.2 makes it SHOULD with no length limit.# <name>H1 that its own reader discards, and emits repeatablemaintaineras a scalar. §6.2.1/§6.2.2.The full list, with what we already satisfy, is now in the published conformance profile:
docs/site/src/content/docs/frame-spec-conformance.md.