diff --git a/12-operations.md b/12-operations.md index 3fca2bd..bcbf677 100644 --- a/12-operations.md +++ b/12-operations.md @@ -65,6 +65,31 @@ Static JSON Schema defaults MAY be used by editor and create interfaces. Validation-time mutation occurs when the create operation explicitly copies a default into the draft. +### Selected types and membership representation + +A request-level type selector identifies the type the caller intends to create; +it is not itself a persisted frontmatter declaration. Contract selection MUST +resolve and validate its designated implementing type before writing. + +When explicit declaration keys are configured, implementations MUST persist a +selected type using those keys, preserving existing valid memberships. They +MUST NOT hard-code `type` or `types` when those keys are not configured. A field +outside the configured declaration keys remains ordinary application data. + +When `settings.explicit_type_keys` is empty, implementations MUST NOT add a type +declaration field and MUST NOT reject a create solely because the list is empty. +The selected type guides schema, lifecycle, and path policy. Membership is +inferred from persisted frontmatter and the final canonical path. The selected +(or contract-designated) type MUST be included in the final inferred membership; +otherwise the operation MUST fail without writing a record. All applicable +types participate in validation, not only the requested type. Matching errors +MUST NOT be silently treated as non-matches. + +The pre-lifecycle membership freeze and final membership check still apply. +Read defaults and projections are not persisted evidence of membership. A +successful create guarantees membership for the written record under the +current collection rules, not permanent membership after later user edits. + ## Update Update modifies an existing record. diff --git a/CHANGELOG.md b/CHANGELOG.md index a120cfa..f0d3c54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this specification and conformance suite are documented here. +## Unreleased + +- Clarify that selected-type creation does not require explicit membership keys. + With inference-only configuration, final persisted fields and path must match + the selected type, and all applicable types remain subject to validation. +- Add shared creation fixtures for inferred membership, ordinary `type` metadata, + auxiliary schemas, derived paths, and serialization-policy membership loss. + ## 2026-07-28 (standard field references) ### Added diff --git a/tests/level-5/references.yaml b/tests/level-5/references.yaml index a53eae1..143dcfe 100644 --- a/tests/level-5/references.yaml +++ b/tests/level-5/references.yaml @@ -55,13 +55,11 @@ groups: tasks/task-b.md: | --- type: task - id: "task-b" title: "Task B" --- tasks/task-c.md: | --- type: task - id: "task-c" title: "Task C" parent: "[[task-b]]" --- @@ -202,7 +200,6 @@ groups: notes/target.md: | --- type: note - id: "target" title: "Target Note" --- @@ -284,7 +281,6 @@ groups: notes/target-note.md: | --- type: note - id: "target-note" title: "Target Note" --- Some body content here. @@ -554,10 +550,10 @@ groups: link: "[Detail](../archive/detail.md)" # ============================================================================= - # Group 6: Ambiguous link handling during rename + # Group 6: Deterministic duplicate-filename handling during rename # ============================================================================= - - name: "ambiguous link not updated during rename" + - name: "deterministically selected duplicate filename updated during rename" spec_ref: "ยง12.5" setup: @@ -594,7 +590,7 @@ groups: --- tests: - - name: "ambiguous link emits warning and is not updated" + - name: "deterministically selected duplicate filename is updated" operation: rename input: from: "folder-a/shared-name.md" @@ -603,17 +599,18 @@ groups: expect: from: "folder-a/shared-name.md" to: "folder-a/renamed.md" - warnings: + references_updated: - path: "notes/source.md" - message_contains: "ambiguous" - # The ambiguous link is left unchanged + field: "ref" + # Equal-depth duplicate filenames use lexical normalized path order, + # so the link identifies folder-a/shared-name.md before the rename. verify_after: operation: read input: path: "notes/source.md" expect: frontmatter: - ref: "[[shared-name]]" + ref: "[[renamed]]" # ============================================================================= # Group 7: rename_ref_update_failed error code diff --git a/tests/v0.3/core/optional-membership.yaml b/tests/v0.3/core/optional-membership.yaml new file mode 100644 index 0000000..3eb8c77 --- /dev/null +++ b/tests/v0.3/core/optional-membership.yaml @@ -0,0 +1,169 @@ +name: Selected create with inferred membership +spec_version: "0.3.0" +fixture_set: core_collection +category: core_collection +spec_ref: "v0.3/07, v0.3/12" +groups: + - name: No explicit membership keys + setup: + config: | + spec_version: "0.3.0" + settings: + explicit_type_keys: [] + default_validation: error + types: + task.md: | + --- + kind: mdbase.type + name: task + match: + where: + tags: {contains: task} + schema: + dialect: json-schema-2020-12 + value: + type: object + required: [title] + properties: + title: {type: string} + type: {type: string} + tags: {type: array, items: {type: string}} + --- + review.md: | + --- + kind: mdbase.type + name: review + match: + fields_present: [reviewed] + schema: + dialect: json-schema-2020-12 + value: + type: object + required: [reviewer] + --- + tests: + - name: selected create succeeds through tag matching without adding keys + operation: create + input: + path: tasks/new.md + type: task + frontmatter: {title: Buy milk, tags: [task]} + expect: + valid: true + types: [task] + frontmatter: {title: Buy milk, tags: [task]} + frontmatter_not_contains: [type, types, mdbase_type] + - name: domain type is preserved rather than replaced with selected name + operation: create + input: + path: tasks/paper.md + type: task + frontmatter: {title: Read paper, tags: [task], type: article-journal} + expect: + valid: true + types: [task] + frontmatter: {type: article-journal} + frontmatter_not_contains: [types, mdbase_type] + - name: selected type must match the persisted document + operation: create + input: + path: tasks/not-matching.md + type: task + frontmatter: {title: Missing tag} + expect: + valid: false + - name: all inferred types validate alongside selected type + operation: create + input: + path: tasks/review.md + type: task + frontmatter: {title: Review paper, tags: [task], reviewed: true} + expect: + valid: false + - name: compatible auxiliary membership is retained + operation: create + input: + path: tasks/review-valid.md + type: task + frontmatter: {title: Review paper, tags: [task], reviewed: true, reviewer: Alice} + expect: + valid: true + types: [review, task] + frontmatter_not_contains: [type, types, mdbase_type] + - name: Final path establishes membership + setup: + config: | + spec_version: "0.3.0" + settings: + explicit_type_keys: [] + default_validation: error + types: + task.md: | + --- + kind: mdbase.type + name: task + match: + path_glob: "tasks/**/*.md" + schema: + dialect: json-schema-2020-12 + value: {type: object, required: [title], properties: {title: {type: string}}} + collection: + path: + pattern: "tasks/{title}.md" + --- + tests: + - name: selected type can derive its matching path + operation: create + input: + type: task + frontmatter: {title: Milk} + expect: + valid: true + path: tasks/Milk.md + types: [task] + frontmatter_not_contains: [type, types, mdbase_type] + - name: supplied matching path succeeds + operation: create + input: + type: task + path: tasks/Milk.md + frontmatter: {title: Milk} + expect: + valid: true + types: [task] + - name: supplied nonmatching path cannot satisfy selection + operation: create + input: + type: task + path: other/Milk.md + frontmatter: {title: Milk} + expect: + valid: false + - name: Serialization policy cannot erase membership + setup: + config: | + spec_version: "0.3.0" + settings: + explicit_type_keys: [] + write_empty_lists: false + default_validation: error + types: + task.md: | + --- + kind: mdbase.type + name: task + match: + fields_present: [markers] + schema: + dialect: json-schema-2020-12 + value: {type: object} + --- + tests: + - name: omitted empty list cannot establish persisted membership + operation: create + input: + type: task + path: no.md + frontmatter: {markers: []} + expect: + valid: false diff --git a/tests/v0.3/manifest.yaml b/tests/v0.3/manifest.yaml index b44a843..0f23e2d 100644 --- a/tests/v0.3/manifest.yaml +++ b/tests/v0.3/manifest.yaml @@ -160,6 +160,7 @@ fixture_sets: coverage_targets: [core_read, collection_semantics, links, core_write] files: - core/core-collection.yaml + - core/optional-membership.yaml - id: data_contracts description: Data contracts, type implementations, projections, stable digests, and conflicts. coverage_targets: [core_read]