Skip to content

Frame Spec v0.3 compliance: stop rejecting unrecognized content, then split name into title and identifier #85

Open

Description

@dcmcand

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. Parse in schema.go uses KnownFields(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. Doc needs somewhere to hold unrecognized keys, and both the YAML and .frame.md writers need to emit them.
  • Unknown section headings. framemd.go rejects any ## heading not in SlotTable. v0.3 §6.2: "A reader MUST NOT treat an unrecognized heading as an error." An unrecognized heading's content becomes guidance. 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.go rejects 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 to guidance on that account.
  • Unregistered visibility values. validate.go rejects 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 title to 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 identifier as the stable key; make title free text and drop the slug constraint.
  • Migrate frames.name and its UNIQUE (org_id, name).
  • Update everything keyed on the slug: /frames/:org/:name, nebari-frame:// URIs, CLI refs, and extends refs.
  • §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.

  • status has registered values (draft, review, approved, deprecated, revoked) under the same preserve-do-not-reject rule as visibility. Note we already have a status column on frame_versions serving a different purpose (the review-gate hook); these are not the same thing.
  • guards carries 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.
  • changelog probably maps to versionNotes, and frame_versions.digest to checksum.

Also owed

  • A conformance profile. §7 makes publishing one mandatory. It must declare whether we resolve composition transitively, and it must declare our mergeTerms replace-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: ErrParentUnreadable already does this.
  • §5.1 rule 8, cycle detection: CycleError.
  • §5.1 rule 5, elements describing the Frame itself are never inherited: resolver.go already carries Name, Description, Version, Visibility, Scope and Maintainer straight through rather than merging them from parents.
  • maintainer already 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.

Activity

  1. dcmcand commented on Sep 11, 2026

    @dcmcand
    ContributorAuthor

    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 UnmarshalMarkdown directly. 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:

    • mergeStrings dedupes 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].
    • An empty prose value is indistinguishable from an absent one, so a child cannot clear a value it would inherit. §5.1 rule 6: "An empty value is meaningful and is distinct from no value."
    • A prose section is read as one value. §6.2.2: a section yields one value per top-level block, and a profile narrowing "MUST NOT change how a document is read".
    • Only the aliased spellings name/inherits are 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 with frame, warn on another token shape.
    • A missing type is reported as a Frame in error. §3.3 requires a reader to distinguish "not a Frame" from "a Frame in error".
    • Reference syntax is grounds for rejection at publication. §5.1 rule 9: "Reference syntax is never grounds for rejecting a Frame." Requiring a pin on publication stays fine; the reader must not reject.
    • description is required and capped at 280 characters. §4.3.2 makes it SHOULD with no length limit.
    • The transitively resolved set is not reported back. §5.2: an implementation that resolves composition MUST make the Version it resolved for each reference available to the caller.
    • Our writer emits a # <name> H1 that its own reader discards, and emits repeatable maintainer as 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.

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

    Fields

    Priority

    None yet

    Start date

    None yet

    Target date

    None yet

    Size

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions