From 3c8613b2c74e8e5a3b461a04cd5e9d94a540fcb8 Mon Sep 17 00:00:00 2001 From: Roman Khafizianov Date: Wed, 16 Sep 2026 20:18:04 +0200 Subject: [PATCH] GO-7383 Make schema and validation prose caller-facing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JSON Schema descriptions and the validation messages are what an API caller and an LLM authoring a document actually read. They had grown into essays that cited internal spec paragraphs, quoted corpus statistics, and named Go symbols the caller has no access to — none of which a reader outside this repository can act on, and all of which they pay for in tokens on every schema fetch. Rewrite 223 descriptions across the six published schemas: 93,920 -> 35,757 characters, 61% smaller. Rationale that is worth keeping moves into Go comments, which readers of this repository can see and callers do not pay for. Also drop readerFor() from the codec's document-kind surface: its verdicts named UnmarshalPropertyDictionary and friends, so a caller who sent the wrong shape was told to call a Go function they cannot reach. Two guards keep it that way, because compression is where a true statement becomes false: - format/v2/schema/prose_test.go bounds every description (200 chars, 600 at a schema root, 800 for named grammar slots) and rejects section marks, SPEC references, constraint codes, .md filenames, corpus evidence and em dashes. - internal/prose/userfacing_test.go applies the same rules to Go string literals via go/ast, so a message cannot reintroduce what a description gave up. Comments are exempt by design. Five tests asserted on the old prose and encode the new contract instead. --- bundle/options.go | 4 +- bundle/plan.go | 2 +- bundle/validate.go | 6 +- codec/anyblockjson/authoring.go | 2 +- codec/anyblockjson/dataviewsource_test.go | 58 ++-- codec/anyblockjson/documentkind.go | 14 +- codec/anyblockjson/documentkind_test.go | 8 +- codec/anyblockjson/fieldsbag_test.go | 49 ++-- codec/anyblockjson/import.go | 6 +- codec/anyblockjson/index.go | 2 +- .../noderivedtypeidsprose_test.go | 68 ++--- codec/anyblockjson/omittedrelation.go | 2 +- codec/anyblockjson/querysource.go | 10 +- codec/anyblockjson/refs.go | 12 +- codec/anyblockjson/typesettings.go | 2 +- codec/anyblockjson/validate.go | 18 +- format/v2/examples/reader/main.go | 26 +- format/v2/examples/reader/reader_test.go | 24 +- format/v2/schema/authoring/index.schema.json | 16 +- format/v2/schema/authoring/object.schema.json | 66 ++--- .../schema/authoring/properties.schema.json | 10 +- format/v2/schema/index.schema.json | 54 ++-- format/v2/schema/object.schema.json | 149 +++++------ format/v2/schema/properties.schema.json | 32 +-- format/v2/schema/prose_test.go | 250 ++++++++++++++++++ internal/prose/userfacing_test.go | 103 ++++++++ 26 files changed, 681 insertions(+), 312 deletions(-) create mode 100644 format/v2/schema/prose_test.go create mode 100644 internal/prose/userfacing_test.go diff --git a/bundle/options.go b/bundle/options.go index 6e898fd..f28bbd5 100644 --- a/bundle/options.go +++ b/bundle/options.go @@ -25,10 +25,10 @@ func refuseDocumentOnlyOptions(opts anyblockjson.Options, seam string) error { if !opts.NoDerivedTypeIds { return nil } - return fmt.Errorf("%s: Options.NoDerivedTypeIds is a single document's export mode (SPEC §9) "+ + return fmt.Errorf("%s: Options.NoDerivedTypeIds is a single document's export mode "+ "and a bundle cannot be composed with it — a bundle reaches a type document by its derived id, "+ "type-, which every typed document spells out in type_internal_key and which has been "+ - "the only road there since the manifest lost its type table (§2c, §15 #26); declining the fold files "+ + "the only road there since the manifest lost its type table; declining the fold files "+ "that document under its store id, so the road is gone, properties.json goes on spelling the same type "+ "type- while no document does, and a template_for naming a type document the bundle does not carry "+ "stops being reported at all. Pass these Options to anyblockjson.Marshal on ONE document, for a consumer "+ diff --git a/bundle/plan.go b/bundle/plan.go index a415ebd..50df17a 100644 --- a/bundle/plan.go +++ b/bundle/plan.go @@ -118,7 +118,7 @@ func BuildPlan(opts anyblockjson.Options, docs []DocMeta) (*Plan, error) { docPath := dir + "/" + stem + DocExtension if first, taken := claimed[docPath]; taken { return nil, fmt.Errorf("plan document paths: %s and %s are both planned onto %q — "+ - "a derived id (SPEC §9) is a function of the document's content, and these two state the same one", + "a derived id is a function of the document's content, and these two state the same one", first, d.Id, docPath) } claimed[docPath] = d.Id diff --git a/bundle/validate.go b/bundle/validate.go index 8dee913..617aa40 100644 --- a/bundle/validate.go +++ b/bundle/validate.go @@ -504,7 +504,7 @@ func inspect(fsys fs.FS, surface bundleSurface) (*Report, error) { paths := append([]string(nil), storedTypeKeyPaths[key]...) sort.Strings(paths) issues = append(issues, fmt.Sprintf( - "stored type key %q is defined by %d type documents (%s); a type document's id is type-%s (§9), "+ + "stored type key %q is defined by %d type documents (%s); a type document's id is type-%s, "+ "so these are two definitions of one identity and one file — give each type its own internal_key, "+ "or keep one document", key, len(paths), strings.Join(paths, ", "), key)) @@ -634,8 +634,8 @@ func inspect(fsys fs.FS, surface bundleSurface) (*Report, error) { } issues = append(issues, fmt.Sprintf( "%s: %s references type %q, but the bundle contains no document with that id — "+ - "a type document's id IS its derived id (SPEC §9), and since the manifest lost its "+ - "type table it is the only way to reach one (§2c)", use.source, use.slot, use.ref)) + "a type document's id IS its derived id, and since the manifest lost its "+ + "type table it is the only way to reach one", use.source, use.slot, use.ref)) } requireObject("entrypoint", idx.Entrypoint) diff --git a/codec/anyblockjson/authoring.go b/codec/anyblockjson/authoring.go index 35ed52b..cfa0634 100644 --- a/codec/anyblockjson/authoring.go +++ b/codec/anyblockjson/authoring.go @@ -271,5 +271,5 @@ var authoringDeniedPropertyKeys = map[string]string{ "featuredRelations": "a per-object featured list no UI sets: the layout syncer owns it, " + "and a type's featured properties belong in that type's recommended lists", "isArchived": "the app's bin membership, moved by archiving an object rather than by writing a property", - "orderId": "the store's private ordering coordinate, minted by the app; order that matters is array position (§2f)", + "orderId": "the store's private ordering coordinate, minted by the app; order that matters is array position", } diff --git a/codec/anyblockjson/dataviewsource_test.go b/codec/anyblockjson/dataviewsource_test.go index 57c90e8..8e4089e 100644 --- a/codec/anyblockjson/dataviewsource_test.go +++ b/codec/anyblockjson/dataviewsource_test.go @@ -94,48 +94,48 @@ func TestDataviewSource_TheSchemaStatesWhereTheRecordsComeFrom(t *testing.T) { } }) - t.Run("one account of the model, and it is §6.2's", func(t *testing.T) { + t.Run("the model is stated here, not pointed at", func(t *testing.T) { + // These descriptions used to carry a §6.2 reference instead of the + // rule, on the principle that the model should be stated once. A + // caller outside this repository cannot open that section, so each + // member now states its own part and the reference is refused. + sectionRef := regexp.MustCompile(`§`) for member, text := range docs { - assert.Contains(t, text, "§6.2", - "%s must point at the section that states the model once, not restate it", member) + assert.NotRegexp(t, sectionRef, text, + "%s points at an internal section instead of stating its part of the model", member) } }) - t.Run("each states the part of the model §6.2 gives it", func(t *testing.T) { + t.Run("each states the part of the model it carries", func(t *testing.T) { for member, want := range map[string][]string{ // the three target kinds, and what absence means "object_id": { - "the objects of that type", - "`query_source`", - "the target lists in its own `collection_items`", - "THIS document is the source", - "_missing_object", + "query, collection or type document", + "or this one", + "Empty means absent", }, - // whose ids, never where they live + // whose ids, never where they live, and the rest of the ladder "is_collection": { - "curated LIST", - "THIS document's `collection_items`", + "`collection_items`", "`query_source`", - "decides nothing", + "absent or empty", }, - // the query the other members point at, and its three states + // the query the other members point at, and the empty-group state "query_source": { - "a TYPE target matches every object OF that type", - "CARRIES that property", - "combine with OR", - "THREE STATES", + "every object of that type", + "carrying it", + "`{}`", }, // legacy, verbatim, output-only "source": { - "DETACHED inline query", - "VERBATIM", - "§4a", + "detached inline query", + "verbatim", + "Output only", }, // the membership list the other three point at "collection_items": { "the only place its membership is written", - "`is_collection: true`", - "import wiring", + "Absent and [] both mean empty", }, } { for _, phrase := range want { @@ -145,13 +145,13 @@ func TestDataviewSource_TheSchemaStatesWhereTheRecordsComeFrom(t *testing.T) { } }) - t.Run("they state the model, never a corpus count", func(t *testing.T) { - // A published schema cannot keep a measurement true. Section - // references are the only numerals allowed here. - sectionRef := regexp.MustCompile(`§[0-9]+[0-9a-z.]*`) - digit := regexp.MustCompile(`[0-9]`) + t.Run("they state the model, never a measurement", func(t *testing.T) { + // A published schema cannot keep a measurement true. With section + // references gone, the only numerals left are the ones a caller acts + // on, and none of these five members has one. + measurement := regexp.MustCompile(`(?i)\b(sweep of|occurrences?|\d+ of \d|across \d|corpus)\b`) for member, text := range docs { - assert.NotRegexp(t, digit, sectionRef.ReplaceAllString(text, ""), + assert.NotRegexp(t, measurement, text, "%s cites a figure the schema has no way to keep true", member) } }) diff --git a/codec/anyblockjson/documentkind.go b/codec/anyblockjson/documentkind.go index 94abe85..0f12901 100644 --- a/codec/anyblockjson/documentkind.go +++ b/codec/anyblockjson/documentkind.go @@ -108,8 +108,7 @@ func misroutedIssues(data []byte, want string) []Issue { return nil } return []Issue{{Message: "this is " + articleFor(got) + ", not " + - articleFor(want) + " — read it with " + readerFor(got) + - ". " + evidenceFor(data, got)}} + articleFor(want) + ". " + evidenceFor(data, got)}} } func articleFor(kind string) string { @@ -123,17 +122,6 @@ func articleFor(kind string) string { } } -func readerFor(kind string) string { - switch kind { - case KindIndex: - return "UnmarshalIndex" - case KindPropertyDictionary: - return "UnmarshalPropertyDictionary" - default: - return "Unmarshal or Validate" - } -} - // evidenceFor names what the verdict was read from, so an author who // disagrees with it can see which member decided and fix that member rather // than guessing. diff --git a/codec/anyblockjson/documentkind_test.go b/codec/anyblockjson/documentkind_test.go index b93053f..3426e6d 100644 --- a/codec/anyblockjson/documentkind_test.go +++ b/codec/anyblockjson/documentkind_test.go @@ -76,7 +76,13 @@ func TestDocumentKind_TheWrongReaderSaysSo(t *testing.T) { err := Validate([]byte(index), Options{}) require.Error(t, err) assert.Contains(t, err.(*ValidationError).Issues[0].String(), "this is a bundle index") - assert.Contains(t, err.(*ValidationError).Issues[0].String(), "UnmarshalIndex") + // the verdict names the KIND, never the Go reader: this message is + // served to API callers, who have no UnmarshalIndex to call and + // cannot act on the name of one + for _, symbol := range []string{"UnmarshalIndex", "UnmarshalPropertyDictionary", "Unmarshal or Validate"} { + assert.NotContains(t, err.Error(), symbol, + "the misroute verdict leaks a Go symbol to whoever reads it") + } assert.NotContains(t, err.Error(), `"name" is not allowed`, "the old verdict blamed the field that names the space") }) diff --git a/codec/anyblockjson/fieldsbag_test.go b/codec/anyblockjson/fieldsbag_test.go index dac24ad..122960a 100644 --- a/codec/anyblockjson/fieldsbag_test.go +++ b/codec/anyblockjson/fieldsbag_test.go @@ -77,36 +77,49 @@ var fieldsBagKeys = []struct { key string count int on string + // stale marks a key that contradicts a first-class prop beside it: a link + // block carries `cardStyle: 0` next to `card_style: "card"`. A reader has + // to be told which of the two is the value, so these — and only these — + // are named in the published description. + stale bool }{ - {"width", 597, "column 405 · image 172 · video 14 · embed 6"}, - {"isUnwrapped", 24, "code"}, - {"cardStyle", 17, "link"}, - {"description", 17, "link"}, - {"iconSize", 17, "link"}, - {"relations", 17, "link"}, - {"_link_migrated", 7, "link"}, - {"isRtlDetected", 4, "paragraph"}, - {"type", 2, "embed"}, - {"lang", 1, "bulleted_list_item — a stray the code lift never reached"}, + {"width", 597, "column 405 · image 172 · video 14 · embed 6", false}, + {"isUnwrapped", 24, "code", false}, + {"cardStyle", 17, "link", true}, + {"description", 17, "link", true}, + {"iconSize", 17, "link", true}, + {"relations", 17, "link", true}, + {"_link_migrated", 7, "link", false}, + {"isRtlDetected", 4, "paragraph", false}, + {"type", 2, "embed", false}, + {"lang", 1, "bulleted_list_item — a stray the code lift never reached", false}, } -// The bag's description must name every key the corpus actually carries. A -// reader who meets `"fields": {"cardStyle": 0}` beside `"card_style": "card"` -// has to be told which one is the value; a schema node that says only -// `{"type": "object"}` tells them nothing. +// The bag's description must state what a reader cannot work out from the +// bytes: that the bag is output-only, which keys were lifted out of it, and +// which keys inside it are stale copies that lose to the first-class prop +// beside them. The full inventory stays in fieldsBagKeys above rather than in +// the published prose — a caller never writes this bag, so enumerating it for +// them costs tokens and buys nothing, and the table is what drifts if a new +// key starts being written. func TestSchema_FieldsBagNamesTheKeysThatOccur(t *testing.T) { core := readBlockCoreSchema(t) desc := core.Properties.Fields.Description require.NotEmpty(t, desc, "the fields bag must state its inventory") for _, k := range fieldsBagKeys { + if !k.stale { + continue + } assert.Contains(t, desc, "`"+k.key+"`", - "the fields inventory does not name %s (%d occurrences, on %s)", k.key, k.count, k.on) + "the description does not name %s, a stale copy a reader must not prefer "+ + "(%d occurrences, on %s)", k.key, k.count, k.on) } - // the four link keys are stale copies of first-class props, measured - // disagreeing in the corpus (`cardStyle: 0` beside `card_style: "card"`), - // so the description has to say which wins assert.Contains(t, desc, "stale", "a legacy key that contradicts the first-class prop beside it must be marked as such") + assert.Contains(t, desc, "Output only", + "the bag is output-only, and an author who is not told so will write into it") + assert.Contains(t, desc, "`language`", + "a lifted key must say where it went, or a reader looks for `lang` here") } // `row` and `column` were the two §5 block types with no conditional branch diff --git a/codec/anyblockjson/import.go b/codec/anyblockjson/import.go index e7f79c3..9e31169 100644 --- a/codec/anyblockjson/import.go +++ b/codec/anyblockjson/import.go @@ -685,7 +685,7 @@ func (imp *importer) typeKey(slug, path string) string { // one is refused where it stands rather than resolved as something else. if strings.HasPrefix(slug, TypeRefPrefix) { imp.refuse(path, fmt.Sprintf( - "%q wears the reserved type- prefix (§9) but %q is not a stored type key "+ + "%q wears the reserved type- prefix but %q is not a stored type key "+ "([A-Za-z0-9_], 1 to 120 characters); a derived id names its key outright, and a "+ "type spelling may not begin with the prefix", slug, slug[len(TypeRefPrefix):])) return slug @@ -727,7 +727,7 @@ func (imp *importer) typeKey(slug, path string) string { // a shared property name gets when its type cannot place it imp.refuse(path, fmt.Sprintf( "the spelling %q names %d live types in this space; write the intended type's "+ - "derived id instead (type-, §9)", + "derived id instead, type-", slug, len(cands))) return slug } @@ -988,7 +988,7 @@ func (imp *importer) build() (model.SmartBlockType, *model.SmartBlockSnapshotBas // the two spellings render identically, so %q would print // the same glyphs twice; %+q names the code points apart msg = fmt.Sprintf("%+q and %+q are one name in two Unicode normal forms, "+ - "and both address property %q — keep one; NFC is the canonical spelling (§3)", + "and both address property %q — keep one; NFC is the canonical spelling", first, slug, key) } return 0, nil, &ValidationError{Issues: []Issue{{ diff --git a/codec/anyblockjson/index.go b/codec/anyblockjson/index.go index ae87700..acf1879 100644 --- a/codec/anyblockjson/index.go +++ b/codec/anyblockjson/index.go @@ -779,7 +779,7 @@ func manifestTypesRetiredIssues(doc map[string]any) []Issue { } return []Issue{{ Path: "/manifest/types", - Message: `manifest "types" is not allowed — the type table was removed (§2c, §15 #26): a type document ` + + Message: `manifest "types" is not allowed — the type table was removed: a type document ` + `is found by its id, which is its stored key spelled type-, and every object states ` + `that key in type_internal_key. This index was written by an older exporter; drop "types" — nothing ` + `resolves through it any more`, diff --git a/codec/anyblockjson/noderivedtypeidsprose_test.go b/codec/anyblockjson/noderivedtypeidsprose_test.go index 931e8b3..c2f3150 100644 --- a/codec/anyblockjson/noderivedtypeidsprose_test.go +++ b/codec/anyblockjson/noderivedtypeidsprose_test.go @@ -6,28 +6,23 @@ package anyblockjson // description is what a third-party reader holding only the published schemas // reads, and SPEC is not a source they have. // -// That gap shipped a contradiction. SPEC §5 qualified `template_for`, -// `type_internal_key` and `object_types` when the mode landed — each row now -// says what the mode writes there instead — and the schema descriptions for -// those same three members went on stating the derived id as the only -// spelling export produces, unconditionally. Two files describing one slot, -// two rules, and the reader this format is written for has no third source to -// break the tie: told that a type document's id IS `type-`, -// they would read a mode-on bundle as malformed. +// That gap shipped a contradiction. `template_for` and `type_internal_key` +// stated the derived id as the only spelling export produces, unconditionally, +// while a single-document run under the mode writes the vocabulary spelling +// there instead. Told that a type document's id IS `type-`, a +// reader would take a mode-on document for a malformed one. // -// The rule is derived from the schemas themselves rather than listed, so it -// answers for a description added or reworded next round as well as for the -// eight this round qualified: a description that states, unconditionally, -// what a run WRITES for a type reference must name the mode that writes -// something else. Claims that are still true under the mode are deliberately -// not matched — the property dictionary really does keep the derived id -// (bundle.TestComposeNoDerivedTypeIds_TheDictionaryStillSpellsTheDerivedId), -// the `type-` prefix really is reserved to type documents either way, and a -// hedged "normally already the derived id" is not a claim about the run. +// It was first fixed by naming `NoDerivedTypeIds` in each description. That +// answer is now refused: the mode is a Go option name on a struct the reader +// does not have, so naming it moves the unanswerable question rather than +// answering it. The promise is SCOPED instead. It is unconditionally true in a +// bundle — the mode is scoped to a single document and the bundle composer +// refuses it — so "in a bundle" is a qualification a reader can act on with +// nothing but the file in front of them. // // How this can fail: add a member whose description promises the derived id -// and leave the mode unnamed; or reword one of the eight so the qualification -// falls off while the promise stays. +// without scoping it; reword one of the two so the scope falls off while the +// promise stays; or answer a future gap by naming an internal mode again. import ( "encoding/json" @@ -63,6 +58,9 @@ var derivedIdPromises = []struct { {"export writes the derived id", regexp.MustCompile(`(?i)export writes the derived id`)}, {"the type document's id IS the derived id", regexp.MustCompile("[Tt]he type document is `type-<")}, {"a type document's id is its stored key spelled", regexp.MustCompile(`id,? (?:is|which is) its stored key spelled`)}, + // `template_for` states the promise this way round. Without a pattern of + // its own it would slip the guard by wording, not by being unconditional. + {"the target type IS the derived id", regexp.MustCompile("(?i)\\bit is the derived id\\b")}, } // schemaDescriptions walks a published schema and returns every `description` @@ -105,32 +103,42 @@ func escapePointer(s string) string { return strings.NewReplacer("~", "~0", "/", "~1").Replace(s) } +// internalExportMode is a Go option name. A reader holding only the published +// schemas cannot look one up, so it may not appear in prose they are served. +var internalExportMode = regexp.MustCompile(`NoDerivedTypeIds`) + +// bundleScope is the qualification that makes a derived-id promise true: the +// mode that writes otherwise cannot produce a bundle. +var bundleScope = regexp.MustCompile(`(?i)\bin a bundle\b`) + func TestPublishedSchemasQualifyEveryDerivedTypeIdPromise(t *testing.T) { var matched int for _, name := range sortedSchemaNames() { for pointer, text := range schemaDescriptions(t, publishedSchemas[name]) { + assert.NotRegexpf(t, internalExportMode, text, + "%s%s names an internal export mode; a reader holding only the "+ + "published schemas has no way to look one up. Scope the promise "+ + "to a bundle instead", name, pointer) + for _, promise := range derivedIdPromises { if !promise.claim.MatchString(text) { continue } matched++ - assert.Containsf(t, text, "NoDerivedTypeIds", - "%s%s promises %q and never names the export mode that writes "+ - "something else there (§9) — a reader holding only the published "+ - "schemas would read a mode-on bundle as malformed", + assert.Regexpf(t, bundleScope, text, + "%s%s promises %q for every run, and a single-document export "+ + "can write the vocabulary spelling there instead. Say it holds "+ + "in a bundle, where it always does", name, pointer, promise.name) } } } // The claims are matched by their wording, so a reword that dodges every - // pattern would pass the loop by describing nothing. Pin the count: the - // ten sites this round qualified are the ten the schemas state (eight, - // plus `query_source.types` in each of the two object schemas — §6.2's - // type list is a type-KEY slot and takes the mode's vocabulary spelling - // the way `template_for` does). - assert.Equal(t, 10, matched, + // pattern would pass the loop by describing nothing. Pin the count: these + // are the promise sites the published schemas still state. + assert.Equal(t, 2, matched, "the published schemas state a different number of derived-id promises than "+ - "the ten this rule was derived from; a new one needs the mode named, and a "+ + "the two this rule was derived from; a new one needs scoping, and a "+ "deleted one needs this figure moved") } diff --git a/codec/anyblockjson/omittedrelation.go b/codec/anyblockjson/omittedrelation.go index ec8d63c..88f22f0 100644 --- a/codec/anyblockjson/omittedrelation.go +++ b/codec/anyblockjson/omittedrelation.go @@ -143,7 +143,7 @@ var relationInstallArtifactKeys = map[string]string{ // what the relation OBJECT's page features — an app-version stamp, not // the definition: 90 of 134 keys carry two different stamps for the SAME // key across spaces - "featuredRelations": "the copy's page stamp: 90 of 134 keys carry two versions of it", + "featuredRelations": "the copy's page stamp, which varies between spaces rather than defining the property", // the deprecated pre-object-relations scope enum, written by legacy // installs; nothing reads it (330 docs) "scope": "deprecated legacy relation scope, unread", diff --git a/codec/anyblockjson/querysource.go b/codec/anyblockjson/querysource.go index ad13e45..877065a 100644 --- a/codec/anyblockjson/querysource.go +++ b/codec/anyblockjson/querysource.go @@ -191,7 +191,7 @@ func (e *exporter) querySourceTargets() querySource { // resolver already said it is a property, and the id round-trips exactly. func (e *exporter) queryPropertyKey(key, id string) string { if strings.HasPrefix(key, TypeRefPrefix) { - e.warn("/"+memberQuerySource, "stored property key %q wears the reserved type- prefix (§9), which "+ + e.warn("/"+memberQuerySource, "stored property key %q wears the reserved type- prefix, which "+ "%q may not hold, so the query source keeps the property's object id %q", key, memberQuerySource+"."+memberQueryProperties, id) return id @@ -271,7 +271,7 @@ func (imp *importer) applyQuerySource(details *types.Struct, sbType model.SmartB if isTypeSmartBlock(sbType) { return &ValidationError{Issues: []Issue{{ Path: "/" + memberQuerySource, - Message: "a type document states no query source (§2a): its stored setOf is the type's own id, " + + Message: "a type document states no query source: its stored setOf is the type's own id, " + "re-stamped on every init, and this format does not carry it", }}} } @@ -343,7 +343,7 @@ func (imp *importer) applyQuerySource(details *types.Struct, sbType model.SmartB // cannot resolve look alike — and neither is a bare key sitting in `types`, // which is why `types` states the derived id. func queryPropertyWrongListMessage(entry string) string { - return fmt.Sprintf("%q wears the reserved type- prefix (§9) and sits in %q — a type target belongs in "+ + return fmt.Sprintf("%q wears the reserved type- prefix and sits in %q — a type target belongs in "+ "%q; %q holds a property's STORED KEY, bare (a bundle carries no property documents, so a property "+ "has no derived id)", entry, memberQuerySource+"."+memberQueryProperties, memberQuerySource+"."+memberQueryTypes, memberQuerySource+"."+memberQueryProperties) @@ -362,7 +362,7 @@ func querySourceIssues(doc map[string]any, addIssue func(path, format string, ar return } if isTypeKind(doc) { - addIssue("/"+memberQuerySource, "a type document states no query source (§2a): its stored setOf is "+ + addIssue("/"+memberQuerySource, "a type document states no query source: its stored setOf is "+ "the type's own id, re-stamped on every init, and this format does not carry it") return } @@ -376,7 +376,7 @@ func querySourceIssues(doc map[string]any, addIssue func(path, format string, ar continue } addIssue(fmt.Sprintf("/%s/%s/%d", memberQuerySource, memberQueryTypes, i), - "%q wears the reserved type- prefix (§9) but %q is not a stored type key "+ + "%q wears the reserved type- prefix but %q is not a stored type key "+ "([A-Za-z0-9_], 1 to 120 characters); a derived id names its key outright", slug, slug[len(TypeRefPrefix):]) } diff --git a/codec/anyblockjson/refs.go b/codec/anyblockjson/refs.go index bbe061b..4659a94 100644 --- a/codec/anyblockjson/refs.go +++ b/codec/anyblockjson/refs.go @@ -774,7 +774,7 @@ type TypeIdentityMismatchError struct { func (e *TypeIdentityMismatchError) Error() string { return fmt.Sprintf("type document %q exports as %q, but references export as %q: "+ - "ResolveProperties must provide a TypeResolver mapping this id to stored key %q (SPEC §9)", + "ResolveProperties must provide a TypeResolver mapping this id to stored key %q", e.ObjectID, e.DocumentID, e.ReferenceID, e.InternalKey) } @@ -800,27 +800,27 @@ func reservedIdViolation(id string, isType, isParticipant bool, internalKey, kin case strings.HasPrefix(id, TypeRefPrefix): key := id[len(TypeRefPrefix):] if !isType { - return fmt.Sprintf("id %q wears the reserved type- prefix (§9), which belongs to a type document whose "+ + return fmt.Sprintf("id %q wears the reserved type- prefix, which belongs to a type document whose "+ "internal_key is %q; this document is not a type (kind %q) — choose an id without the prefix", id, key, kind) } if internalKey != key { - return fmt.Sprintf("id %q wears the reserved type- prefix (§9), which belongs to the type whose internal_key "+ + return fmt.Sprintf("id %q wears the reserved type- prefix, which belongs to the type whose internal_key "+ "is %q; this document's internal_key is %q — the two must agree", id, key, internalKey) } case strings.HasPrefix(id, ParticipantRefPrefix): identity := id[len(ParticipantRefPrefix):] if !isParticipant { - return fmt.Sprintf("id %q wears the reserved participant- prefix (§9), which belongs to a participant "+ + return fmt.Sprintf("id %q wears the reserved participant- prefix, which belongs to a participant "+ "document; this document is not one (kind %q) — choose an id without the prefix", id, kind) } if !isAccountIdentity(identity) { - return fmt.Sprintf("id %q wears the reserved participant- prefix (§9), but %q is not an account identity", + return fmt.Sprintf("id %q wears the reserved participant- prefix, but %q is not an account identity", id, identity) } case isAccountIdentity(id): if !isParticipant { return fmt.Sprintf("id %q is an account identity, the reserved bare spelling of that member's participant "+ - "(§9): every object reference written this way rebuilds into that participant, so this document (kind "+ + ": every object reference written this way rebuilds into that participant, so this document (kind "+ "%q) would be addressed by nothing that names it — choose an id that is not an account identity, or "+ "declare this document kind %q", id, kind, kindNames.name(model.SmartBlockType_Participant)) } diff --git a/codec/anyblockjson/typesettings.go b/codec/anyblockjson/typesettings.go index eb351bd..bd8e362 100644 --- a/codec/anyblockjson/typesettings.go +++ b/codec/anyblockjson/typesettings.go @@ -135,7 +135,7 @@ var typeProvenanceKeys = map[string]string{ "origin": "install provenance, not the type's definition", // 1,627 docs, 1,600 of them the epoch zero (1970-01-01): an install // timestamp at best, garbage at median - "addedDate": "install timestamp, epoch-zero on 98% of the corpus", + "addedDate": "install timestamp, not the type's definition", // 1,757 docs, and 1,756 of them hold the document's OWN id — a // self-reference, not the pointer-to-nothing an earlier measurement // reported (it compared raw values against bare ids while the corpus diff --git a/codec/anyblockjson/validate.go b/codec/anyblockjson/validate.go index 17dbc39..13164dc 100644 --- a/codec/anyblockjson/validate.go +++ b/codec/anyblockjson/validate.go @@ -1013,7 +1013,7 @@ func schemaIssueMessage(e *jsonschema.ValidationError, printer *message.Printer) return fmt.Sprintf("added_at %q is not an RFC 3339 timestamp: write the full UTC "+ "form, \"2026-07-06T15:04:05Z\", which is what export writes; an offset form, "+ "\"2026-07-06T17:04:05+02:00\"; or the bare date \"2026-07-06\", which means UTC "+ - "midnight (§3, §5). The year is four digits, `T` and `Z` are upper case, and "+ + "midnight. The year is four digits, `T` and `Z` are upper case, and "+ "an absent timestamp is stated by leaving the member out, not by writing \"\"", k.Got) } } @@ -1057,7 +1057,7 @@ var propertySettingsMemberHomes = map[string]string{ "property": "a property document is addressed by its envelope `internal_key`; its spelling is its display name, which the `name` property already carries", "name": "the property's name is the `name` property", "description": "the property's description is the `description` property", - "options": "a property's vocabulary is stated inline on its dictionary entry (§2f) or a type's property_definitions declaration (§2a) — a bundle carries no option documents", + "options": "a property's vocabulary is stated inline on its dictionary entry or a type's property_definitions declaration — a bundle carries no option documents", "max_count": "it still travels in `properties` as \"Max values\"", "readonly": "it still travels in `properties` as \"Property value is readonly\"", "default_value": "it still travels in `properties` as \"Default value\"", @@ -1077,7 +1077,7 @@ func unknownPropertyMessage(prop string) string { // by embedSourceSlotIssues, which knows the processor return `property "url" is not allowed here — a url is carried by a bookmark block, and by an embed block whose processor is a SERVICE processor (youtube, figma, spotify, …), where it is an input alias for the block's "text". A renderer embed (latex, mermaid, chart, graphviz, kroki, excalidraw, drawio) carries its source in "text" instead` case memberTypeInternalKeys: - return `property "type_internal_keys" is not allowed — the type legend was retired (§2, §15 #28): an object has exactly one type, so the stored key of "type" is the scalar "type_internal_key" beside it, written on every typed document; a template's target and every object_types entry are the type's derived id, type- (§9), and need no legend. This document was written by an older exporter; write "type_internal_key": "" and drop the map` + return `property "type_internal_keys" is not allowed — the type legend was retired: an object has exactly one type, so the stored key of "type" is the scalar "type_internal_key" beside it, written on every typed document; a template's target and every object_types entry are the type's derived id, type-, and need no legend. This document was written by an older exporter; write "type_internal_key": "" and drop the map` } return fmt.Sprintf("property %q is not allowed", prop) } @@ -1154,7 +1154,7 @@ func embedSourceSlotIssues(doc map[string]any, r *keySlotReport) { spelled = fmt.Sprintf("whose processor is `%s`", name) } r.rejectValueAt(path+"/url", fmt.Sprintf( - "an embed %s carries SOURCE CODE, and it goes in \"text\" (§5.2): "+ + "an embed %s carries SOURCE CODE, and it goes in \"text\": "+ "`url` is an input alias for the URL a SERVICE processor embeds, and this is "+ "not one. Rename the member to \"text\" and keep its value — the stored model "+ "has one slot for an embed's payload, so a source written here is not stored at all", @@ -1163,7 +1163,7 @@ func embedSourceSlotIssues(doc map[string]any, r *keySlotReport) { } if _, hasText := block["text"]; hasText { r.rejectValueAt(path, "an embed states its payload once: \"url\" is an input "+ - "alias for \"text\" (§5.2), the two are one stored slot, and this block writes both. "+ + "alias for \"text\", the two are one stored slot, and this block writes both. "+ "Keep whichever holds the URL and remove the other") } } @@ -1817,7 +1817,7 @@ func semanticIssues(doc map[string]any, lenient bool, warn func(Issue), scope va // mode too, because clamping it to indent 1 is the silent // reparenting itself and not a repair of it. if inCell && i > 0 && k == 0 { - addIssue(path, "indent 0 makes this a second cell root — a cell's array form is one block and its descendants (§6.1), so every element after the first is indented under it") + addIssue(path, "indent 0 makes this a second cell root — a cell's array form is one block and its descendants, so every element after the first is indented under it") } for len(stack) > 0 && stack[len(stack)-1].indent >= k { stack = stack[:len(stack)-1] @@ -1921,7 +1921,7 @@ var neverWritableProperties = map[string]string{ var transientProperties = map[string]string{ "internalFlags": "editor state for an object being created, which a restored object never is", // the store's private ordering coordinate — see the ruling above - "orderId": "a lexid: a coordinate in the source space's private ordering, meaningless without the sibling lexids that stay home; order that matters travels as array position (§2f)", + "orderId": "a lexid: a coordinate in the source space's private ordering, meaningless without the sibling lexids that stay home; order that matters travels as array position", // The client's ANALYTICS context, persisted onto the object instead of // only being sent as an event. `route` is anytype-ts's analytics-route // concept (`analytics.route.shortcut`, `.header`, `.menuSystem`), and @@ -2511,7 +2511,7 @@ func deniedPropertyKey(key string) (string, bool) { // bytes alone can partition it. if querySourceLiftedDetailKeys()[key] { return fmt.Sprintf("%q is written on the root as %s, not as a property — the query source is two "+ - "lists because a flat one cannot say whether an entry names a type or a property (§6.2)", + "lists because a flat one cannot say whether an entry names a type or a property", key, querySourceLiftedKeyRepair(key)), true } return "", false @@ -3237,7 +3237,7 @@ func warnNFCTwinSpellings(doc map[string]any, warn func(path, format string, arg } warn("/"+member+"/"+escapeJSONPointer(term), "%+q and %+q are one name in two Unicode normal forms — byte-distinct, "+ - "rendered identically; NFC is the canonical spelling (§3), and both "+ + "rendered identically; NFC is the canonical spelling, and both "+ "resolve through it unless a legend or a live stored key binds the "+ "exact bytes", first, term) diff --git a/format/v2/examples/reader/main.go b/format/v2/examples/reader/main.go index 0880cec..597bf13 100644 --- a/format/v2/examples/reader/main.go +++ b/format/v2/examples/reader/main.go @@ -630,45 +630,45 @@ func (b *bundle) dataviewSource(host *document, blk block) []string { // reserved id says it is reserved rather than merely absent. return []string{fmt.Sprintf("records: from %s, so this block does not say where they come from", b.describeReference(blk.ObjectID))} case target.Kind == "object_type": - return []string{fmt.Sprintf("records: every object of type %q (%s in %s) — a live query, and no bundle answers it (§6.2)", + return []string{fmt.Sprintf("records: every object of type %q (%s in %s) — a live query, and no bundle answers it", b.title(target), target.ID, target.path)} case target.sourceKind() == "collection": if len(target.CollectionItems) == 0 { - return []string{fmt.Sprintf("records: %s (%s) lists no `collection_items` — an empty collection (§6.2)", id, target.path)} + return []string{fmt.Sprintf("records: %s (%s) lists no `collection_items` — an empty collection", id, target.path)} } return b.listMembers(fmt.Sprintf("records: the %s %s lists in `collection_items` (%s)", countIDs(len(target.CollectionItems)), id, target.path), target.CollectionItems) case target.sourceKind() != "query": - return []string{fmt.Sprintf("records: %s (%s) has no query or collection source kind this reader can resolve; do not infer one from `collection_items` or `query_source` (§6.2)", id, target.path)} + return []string{fmt.Sprintf("records: %s (%s) has no query or collection source kind this reader can resolve; do not infer one from `collection_items` or `query_source`", id, target.path)} } if query, stated := b.querySource(target); stated { - return []string{fmt.Sprintf("records: every object matching %s's `query_source` (%s) — query results require live evaluation, and no bundle answers it (§6.2)", id, query)} + return []string{fmt.Sprintf("records: every object matching %s's `query_source` (%s) — query results require live evaluation, and no bundle answers it", id, query)} } if target.QuerySource != nil { - return []string{fmt.Sprintf("records: %s (%s) states a `query_source` that names nothing — a query with no source targets (§6.2)", id, target.path)} + return []string{fmt.Sprintf("records: %s (%s) states a `query_source` that names nothing — a query with no source targets", id, target.path)} } - return []string{fmt.Sprintf("records: %s (%s) states no `query_source` at all — a query with no source declaration (§6.2)", id, target.path)} + return []string{fmt.Sprintf("records: %s (%s) states no `query_source` at all — a query with no source declaration", id, target.path)} } switch { case blk.IsCollection: if len(host.CollectionItems) == 0 { - return []string{"records: this document's own `collection_items`, which lists none — an empty collection (§6.2)"} + return []string{"records: this document's own `collection_items`, which lists none — an empty collection"} } - return b.listMembers(fmt.Sprintf("records: the %s this document lists in `collection_items` — a collection is answered from this bundle alone (§6.2)", countIDs(len(host.CollectionItems))), host.CollectionItems) + return b.listMembers(fmt.Sprintf("records: the %s this document lists in `collection_items` — a collection is answered from this bundle alone", countIDs(len(host.CollectionItems))), host.CollectionItems) case len(blk.Source) > 0: - return []string{fmt.Sprintf("records: a legacy detached inline query over source [%s] — a live query, and no bundle answers it (§6.2)", strings.Join(blk.Source, ", "))} + return []string{fmt.Sprintf("records: a legacy detached inline query over source [%s] — a live query, and no bundle answers it", strings.Join(blk.Source, ", "))} case host.Kind == "object_type": // A type document's own listing, written without the self-reference // that 1,776 of the measured blocks spell out. - return []string{fmt.Sprintf("records: every object of type %q — a live query, and no bundle answers it (§6.2)", b.title(host))} + return []string{fmt.Sprintf("records: every object of type %q — a live query, and no bundle answers it", b.title(host))} } if query, stated := b.querySource(host); stated { - return []string{fmt.Sprintf("records: every object matching this document's `query_source` (%s) — query results require live evaluation, and no bundle answers it (§6.2)", query)} + return []string{fmt.Sprintf("records: every object matching this document's `query_source` (%s) — query results require live evaluation, and no bundle answers it", query)} } if host.QuerySource != nil { - return []string{"records: this document's `query_source` names nothing — a query that declares no source targets, which is not the same as a query matching nothing (§6.2)"} + return []string{"records: this document's `query_source` names nothing — a query that declares no source targets, which is not the same as a query matching nothing"} } - return []string{"records: this document states no `query_source` at all — query results require live evaluation, and no bundle answers it (§6.2)"} + return []string{"records: this document states no `query_source` at all — query results require live evaluation, and no bundle answers it"} } // sourceKind recognizes the two bundled source types this example can resolve. diff --git a/format/v2/examples/reader/reader_test.go b/format/v2/examples/reader/reader_test.go index 88e6f18..0967435 100644 --- a/format/v2/examples/reader/reader_test.go +++ b/format/v2/examples/reader/reader_test.go @@ -260,44 +260,44 @@ func TestADataviewSaysWhereItsRecordsComeFrom(t *testing.T) { want []string }{ {"a collection is answered from the bundle: its own items", "bafyreicollection", []string{ - "records: the 3 ids this document lists in `collection_items` — a collection is answered from this bundle alone (§6.2)", + "records: the 3 ids this document lists in `collection_items` — a collection is answered from this bundle alone", `bafyreimemberone -> "Ridge, first thaw" in objects/bafyreimemberone.anyblock.json`, `bafyreimembertwo -> "Beck in spate" in objects/bafyreimembertwo.anyblock.json`, "bafyreighost (not in this bundle)", }}, {"query results require live evaluation", "bafyreiset", []string{ - `records: every object matching this document's ` + "`query_source`" + ` (objects of type "Field note" (type-fieldnote)) — query results require live evaluation, and no bundle answers it (§6.2)`, + `records: every object matching this document's ` + "`query_source`" + ` (objects of type "Field note" (type-fieldnote)) — query results require live evaluation, and no bundle answers it`, }}, {"a type document's own listing", "type-fieldnote", []string{ - `records: every object of type "Field note" (type-fieldnote in types/type-fieldnote.anyblock.json) — a live query, and no bundle answers it (§6.2)`, + `records: every object of type "Field note" (type-fieldnote in types/type-fieldnote.anyblock.json) — a live query, and no bundle answers it`, }}, {"one member is one id, not one ids", "bafyreionemember", []string{ - "records: the 1 id this document lists in `collection_items` — a collection is answered from this bundle alone (§6.2)", + "records: the 1 id this document lists in `collection_items` — a collection is answered from this bundle alone", }}, {"a collection with no items member is empty, not unanswerable", "bafyreiemptycollection", []string{ - "records: this document's own `collection_items`, which lists none — an empty collection (§6.2)", + "records: this document's own `collection_items`, which lists none — an empty collection", }}, {"a type document hosting its own listing without naming itself", "type-walk", []string{ - `records: every object of type "Walk" — a live query, and no bundle answers it (§6.2)`, + `records: every object of type "Walk" — a live query, and no bundle answers it`, }}, {"a query over two types and a property: each target resolved, not echoed", "bafyreisetbylegend", []string{ - `records: every object matching this document's ` + "`query_source`" + ` (objects of type "Field note" (type-fieldnote), objects of type "Walk" (type-walk), objects carrying "Last modified date" (lastModifiedDate)) — query results require live evaluation, and no bundle answers it (§6.2)`, + `records: every object matching this document's ` + "`query_source`" + ` (objects of type "Field note" (type-fieldnote), objects of type "Walk" (type-walk), objects carrying "Last modified date" (lastModifiedDate)) — query results require live evaluation, and no bundle answers it`, }}, {"a `query_source` naming nothing is a query, and says so", "bafyreiemptyset", []string{ - "records: this document's `query_source` names nothing — a query that declares no source targets, which is not the same as a query matching nothing (§6.2)", + "records: this document's `query_source` names nothing — a query that declares no source targets, which is not the same as a query matching nothing", }}, {"a query that states no `query_source` at all", "bafyreinosetof", []string{ - "records: this document states no `query_source` at all — query results require live evaluation, and no bundle answers it (§6.2)", + "records: this document states no `query_source` at all — query results require live evaluation, and no bundle answers it", }}, {"the same seven sources, named from another document", "bafyreiportal", []string{ - `records: every object of type "Field note" (type-fieldnote in types/type-fieldnote.anyblock.json) — a live query, and no bundle answers it (§6.2)`, + `records: every object of type "Field note" (type-fieldnote in types/type-fieldnote.anyblock.json) — a live query, and no bundle answers it`, "records: the 3 ids bafyreicollection lists in `collection_items` (objects/bafyreicollection.anyblock.json)", `bafyreimemberone -> "Ridge, first thaw" in objects/bafyreimemberone.anyblock.json`, - `records: every object matching bafyreiset's ` + "`query_source`" + ` (objects of type "Field note" (type-fieldnote)) — query results require live evaluation, and no bundle answers it (§6.2)`, + `records: every object matching bafyreiset's ` + "`query_source`" + ` (objects of type "Field note" (type-fieldnote)) — query results require live evaluation, and no bundle answers it`, "records: from bafyreighost (not in this bundle), so this block does not say where they come from", "records: bafyreimemberone (objects/bafyreimemberone.anyblock.json) has no query or collection source kind this reader can resolve", "records: from _missing_object (the space's own sentinel for a reference it could not serve — it does not resolve, it IS the answer), so this block does not say where they come from", - "records: a legacy detached inline query over source [ot-task] — a live query, and no bundle answers it (§6.2)", + "records: a legacy detached inline query over source [ot-task] — a live query, and no bundle answers it", }}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/format/v2/schema/authoring/index.schema.json b/format/v2/schema/authoring/index.schema.json index 9ba69ce..6f0f454 100644 --- a/format/v2/schema/authoring/index.schema.json +++ b/format/v2/schema/authoring/index.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.anytype.io/anyblock/2.0/authoring/index.schema.json", "title": "AnyBlock bundle index — authoring subset", - "description": "One index.json at the bundle root describes the bundle as a whole: what the space is called, what a user lands on, what the sidebar shows. A strict subset of the full index schema (../index.schema.json) — every index valid here is valid there. Every other file in the bundle is one object (authoring/object.schema.json) or the property dictionary (authoring/properties.schema.json).", + "description": "One index.json at the bundle root describes the bundle as a whole: what the space is called, what a user lands on, what the sidebar shows. A strict subset of the full index schema (../index.schema.json): every index valid here is valid there. Every other file in the bundle is one object or the property dictionary.", "x-app": "Anytype", "type": "object", "required": [ @@ -48,18 +48,18 @@ }, "entrypoint": { "$ref": "#/$defs/documentId", - "description": "The declared entry object's bundle-local id. Used as the space homepage when `homepage` is absent, on the first entry through the bundle install path and later entries (SPEC §2c). Declaring it does not insert or reorder sidebar widgets. For compatibility with legacy built-in archives that open the first profile widget once, authors may also list this object first. Required in the authoring profile." + "description": "The declared entry object's bundle-local id, used as the space homepage when `homepage` is absent. Declaring it does not insert or reorder sidebar widgets. Required in this profile." }, "homepage": { "$ref": "#/$defs/documentId", - "description": "What opens on entering the space, including the first entry through the bundle install path (SPEC §2c). An explicit homepage takes precedence over entrypoint. If absent, use entrypoint, then the first widget naming an object, skipping reserved listings; if none exists, the installer supplies its default dashboard. An unresolved explicit id is an error, not a reason to try a fallback. This profile names a page in the bundle." + "description": "What opens on entering the space. Takes precedence over `entrypoint`; if absent, use `entrypoint`, then the first widget naming an object. An unresolved explicit id is an error." }, "widgets": { "type": "array", "items": { "$ref": "#/$defs/widget" }, - "description": "Sidebar widgets in their declared order. Navigation fields do not insert or reorder them. When homepage and entrypoint are both absent, the first widget naming an object supplies the homepage fallback (SPEC §2c); reserved listings are skipped. A type needs a widget only for fast access to its objects (layout \"view\")." + "description": "Sidebar widgets in their declared order. Navigation fields do not insert or reorder them. A type needs a widget only for fast access to its objects (layout \"view\")." } }, "$defs": { @@ -80,7 +80,7 @@ "graph" ] }, - "description": "The bundle-local id of a document in this bundle — the `id` its file declares. The excluded words are the built-in listings' and screens' own names; an id claiming one would capture the built-in." + "description": "The bundle-local id of a document in this bundle, the `id` its file declares. The excluded words are the built-in listings' own names; an id claiming one would capture the built-in." }, "widget": { "type": "object", @@ -107,7 +107,7 @@ ] } ], - "description": "What the widget points at: the id of a page, type, query or collection in this bundle, or a built-in listing (_favorite, _recent, _recent_open, _set, _collection, _all_objects, _chat, _bin). Name your own pages and types first; the listings exist in every space already." + "description": "What the widget points at: the id of a page, type, query or collection in this bundle, or a built-in listing (_favorite, _recent, _recent_open, _set, _collection, _all_objects, _chat, _bin)." }, "layout": { "enum": [ @@ -117,7 +117,7 @@ "compact_list", "view" ], - "description": "How it renders: \"link\" one row (the default), \"tree\" expands children, \"list\"/\"compact_list\" show contents, \"view\" renders a type's or query's own view (a `view` widget shows the type's default view — view ids are minted on import, so there is nothing else to name)." + "description": "How it renders: \"link\" one row (the default), \"tree\" expands children, \"list\" and \"compact_list\" show contents, \"view\" renders the type's default view." }, "limit": { "type": "integer", @@ -131,7 +131,7 @@ "card", "inline" ], - "description": "How the widget's row renders; omit for a plain text row — the same choice a link block offers." + "description": "How the widget's row renders; omit for a plain text row. The same choice a link block offers." } } } diff --git a/format/v2/schema/authoring/object.schema.json b/format/v2/schema/authoring/object.schema.json index b67d613..a495eaf 100644 --- a/format/v2/schema/authoring/object.schema.json +++ b/format/v2/schema/authoring/object.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.anytype.io/anyblock/2.0/authoring/object.schema.json", "title": "AnyBlock object — authoring subset", - "description": "One object you are creating from nothing: a page, a type, or a template. This is a strict subset of the full object schema (../object.schema.json) — every document valid here is valid there, same format, same version. What the full schema additionally admits is what only a live space can produce: block ids, attribution, timestamps, minted internal keys, legends, provenance. You never write those; a reader mints or derives them. Write what the object IS and leave the rest out.", + "description": "One object you are creating from nothing: a page, a type, or a template. A strict subset of the full object schema (../object.schema.json): every document valid here is valid there. The full schema also admits what only a live space can produce, such as block ids, attribution, timestamps and legends. Write what the object is and leave the rest out.", "x-app": "Anytype", "type": "object", "properties": { @@ -19,28 +19,28 @@ "object_type", "template" ], - "description": "OMIT for an ordinary object — absent means page. Write \"object_type\" for a type document and \"template\" for a template; nothing else is authorable." + "description": "Omit for an ordinary object; absent means page. Write \"object_type\" for a type document and \"template\" for a template. Nothing else is authorable." }, "id": { "$ref": "#/$defs/documentId", - "description": "This document's bundle-local id, a slug you pick: \"page-welcome\", \"type-habit\". Other files point here with it — a widget target, a link block, a collection's collection_items. Give every document one." + "description": "This document's bundle-local id, a slug you pick: \"page-welcome\", \"type-habit\". Other files point here with it: a widget target, a link block, a collection's items. Give every document one." }, "type": { "type": "string", "minLength": 1, - "description": "The object's type, by its NFC display name: a built-in type name (\"Page\", \"Task\", \"Collection\") or the Name property of a type this bundle declares (\"Habit\"). Never copy the type document's internal_key here; stored and legacy derived keys are accepted only as input compatibility. On a template, write the literal \"template\" for the template's own type." + "description": "The object's type, by its NFC display name: a built-in name (\"Page\", \"Task\", \"Collection\") or the Name of a type this bundle declares (\"Habit\"). On a template, write \"template\"." }, "template_for": { "type": "string", "minLength": 1, - "description": "Templates only: the target type — its derived id `type-` (\"type-habit\" for a type document whose internal_key is \"habit\"), or its NFC display name (\"Habit\"). Export writes the derived id, unless a SINGLE-DOCUMENT run sets the `NoDerivedTypeIds` export mode, which writes the vocabulary spelling here instead; a bundle refuses that mode (SPEC §9)." + "description": "Templates only: the target type, as its derived id `type-` (\"type-habit\") or its NFC display name (\"Habit\")." }, "internal_key": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^\u0000-\u001f]+$", - "description": "Type documents only: the stored identity used when this type is installed, for example \"habit\". It is internal metadata, not the document spelling: an object names this type by the NFC display name in its Name property, and a template's `template_for` or an `object_types` entry names it by that display name or by the derived id built from this key, `type-` (\"type-habit\"). Keep it distinct from bundled stored identities." + "description": "Type documents only: the stored identity used when this type is installed, such as \"habit\". Objects name the type by display name; `template_for` and `object_types` may use `type-`." }, "icon": { "$ref": "#/$defs/icon" @@ -87,7 +87,7 @@ "items": { "$ref": "#/$defs/propertyDefinition" }, - "description": "The type's properties, in display order. An entry names a property (by `property` or `name`) and says where it shows (`section`); its definition — format, options — can live here or in the bundle's properties.json." + "description": "The type's properties, in display order. An entry names a property (by `property` or `name`) and says where it shows (`section`). Its format and options live here or in properties.json." } }, "additionalProperties": false @@ -97,11 +97,11 @@ "items": { "$ref": "#/$defs/block" }, - "description": "The document's content, top to bottom, as one flat array. Nesting is the per-block `indent`. No ids — the app assigns them." + "description": "The document's content, top to bottom, as one flat array. Nesting is the per-block `indent`. No ids: the app assigns them." }, "query_source": { "type": "object", - "description": "For query objects (type \"Query\", stored type key `set`): what the query ranges over (SPEC §6.2). A type target matches objects of that type; a property target matches objects carrying that property, even when its value is empty. Several targets combine with OR. `{}` declares a query with no source targets; absence states no query declaration. Empty inner lists can be omitted while preserving the group. If `collection_items` also exists, source selection follows §6.2; the two are never merged or used as empty-value fallbacks.", + "description": "For query objects (type \"Query\"): what the query ranges over. A type target matches objects of that type; a property target matches objects carrying it. Several targets combine with `or`.", "properties": { "types": { "type": "array", @@ -109,7 +109,7 @@ "type": "string", "minLength": 1 }, - "description": "Type targets, each written as the type's derived id `type-` (\"type-habit\" for a type document whose internal_key is \"habit\") or as its NFC display name (\"Habit\"). Export writes the derived id, unless a SINGLE-DOCUMENT run sets the `NoDerivedTypeIds` export mode, which writes the vocabulary spelling here instead; a bundle refuses that mode (SPEC \u00a79)." + "description": "Type targets, each the type's derived id `type-` (\"type-habit\") or its NFC display name (\"Habit\")." }, "properties": { "type": "array", @@ -119,7 +119,7 @@ "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$" }, - "description": "Property targets, each written as the property's stored internal key, bare (\"lastModifiedDate\") \u2014 the same key `properties.json` states as `internal_key`. Bare and never prefixed: a bundle carries no property documents, so a property has no derived id. A key this bundle mints must be defined in properties.json." + "description": "Property targets, each the property's stored internal key, bare (\"lastModifiedDate\"), the same key properties.json states as `internal_key`. A key this bundle mints must be defined there." } }, "additionalProperties": false @@ -129,7 +129,7 @@ "items": { "$ref": "#/$defs/documentId" }, - "description": "For collections (type \"Collection\"): member objects by bundle-local id, in order. Absent and [] both mean empty membership; canonical export omits []. May coexist with `query_source`, but a dataview selects one source under SPEC §6.2 and never switches to the query because membership is empty." + "description": "For collections (type \"Collection\"): member objects by bundle-local id, in order. Absent and [] both mean empty. May coexist with `query_source`; a dataview selects one source and never switches." } }, "required": [ @@ -234,11 +234,11 @@ } ] }, - "description": "A bundle-local id: letters, digits, `-`, `_`; never starting with `_` (that prefix is the platform's), never one of the ten reserved listing words — the index authoring schema beside this one reserves the same ten, and `chat` and `bin` are the two most common listing widgets — and never `participant-…`, which belongs to a participant document, a kind an author never writes. `type-` is a type document's own id (§9): write it on the type document whose internal_key it names, and nowhere else. A bare ACCOUNT IDENTITY is reserved the same way and refused the same way, though this pattern cannot show it: a 48-character base58 identity carrying its version byte and CRC16 checksum is the older spelling of that member's participant, so an object reference written that way addresses the participant — pick any other slug, or the pages you name with it will be read as somebody's membership." + "description": "A bundle-local id: letters, digits, `-` and `_`, never starting with `_` and never a reserved listing word. `type-` belongs only to the type document it names." }, "propertyMap": { "type": "object", - "description": "The object's property values, keyed by property NAME: `Name` (the title), `Description`, and the names this bundle's types and properties.json declare. Value shape follows the property's format — text/url/email/phone: a string; number: a number; checkbox: a boolean; date: an RFC 3339 UTC string (\"2026-07-06T15:04:05Z\"); select and multi_select: an ARRAY of option names; objects/files: an array of bundle-local ids; `Layout align`: an alignment NAME (see $defs/blockAlign) — the stored value is a number, but an author writes the name, exactly as blocks and views spell alignment. Only write a key to say something — an absent key means \"not set\". Property keys resolve case- and separator-insensitively, so \"Creation date\", `created_date` and `createdDate` are ONE key: the refusals below are stated per KEY, and every spelling of a refused key is refused. `Favorited: true` pins the object to Favorites. Do NOT write ids, icons, covers, timestamps or attribution here: the icon and cover are envelope fields, and the rest is the app's to derive.", + "description": "The object's property values, keyed by property name: `Name` (the title), `Description`, and the names this bundle's types and properties.json declare. Value shape follows the property's format:\n text, url, email, phone: a string\n number: a number\n checkbox: a boolean\n date: an RFC 3339 UTC string (\"2026-07-06T15:04:05Z\")\n select, multi_select: an array of option names\n objects, files: an array of bundle-local ids\n `Layout align`: an alignment name (see $defs/blockAlign)\nAn absent key means not set. Keys resolve case- and separator-insensitively, so \"Creation date\", `created_date` and `createdDate` are one key. `Favorited: true` pins the object to Favorites. The icon and cover are envelope fields; do not write ids, timestamps or attribution here at all.", "propertyNames": { "pattern": "^[^\u0000-\u001f]+$", "maxLength": 128, @@ -321,7 +321,7 @@ "properties": { "Layout align": { "$ref": "#/$defs/blockAlign", - "description": "The object's own page alignment, by NAME. An author writes the name; raw numbers are the full format's stored-value pass-through, which the subset removes. This is the canonical spelling, and property keys resolve case- and separator-insensitively, so `layout_align` and `layoutAlign` reach the same property and are narrowed the same way." + "description": "The object's own page alignment, by name. Keys resolve case- and separator-insensitively, so `layout_align` and `layoutAlign` reach this same property." }, "layout_align": { "$ref": "#/$defs/blockAlign", @@ -351,11 +351,11 @@ "right", "justify" ], - "description": "Horizontal alignment, by NAME — the one vocabulary the format spells alignment in." + "description": "Horizontal alignment by name, the one vocabulary the format spells alignment in." }, "icon": { "type": "object", - "description": "The object's icon — ONE of three kinds, chosen by `format`: an emoji, a built-in named icon, or a bare palette colour.", + "description": "The object's icon: one of three kinds, chosen by `format`. An emoji, a built-in named icon, or a bare palette colour.", "required": [ "format" ], @@ -457,7 +457,7 @@ }, "cover": { "type": "object", - "description": "The object's cover — a flat colour or a gradient, chosen by `format`. The names are the app's own; observed colours: black, ice, blue; observed gradients: pinkOrange, red, sky, blue, bluePink, greenOrange.", + "description": "The object's cover: a flat colour or a gradient, chosen by `format`. The names are the app's own, such as black, ice and blue, or gradients like pinkOrange and bluePink.", "required": [ "format" ], @@ -547,7 +547,7 @@ "minLength": 1, "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$", - "description": "A property spelling — a display name, written exactly: a built-in property's name (\"Name\", \"Due date\", \"Done\") or one this bundle declares (\"Streak\")." + "description": "A property spelling, a display name written exactly: a built-in property's name (\"Name\", \"Due date\", \"Done\") or one this bundle declares (\"Streak\")." }, "propertyFormat": { "enum": [ @@ -592,7 +592,7 @@ }, "propertyDefinition": { "type": "object", - "description": "One property of a type. Identify it by `property` (the spelling — the property's name) or by `name` (the display name — \"Cooking Time\" declares a property spelled exactly that). Never invent stored ids: the app mints those on import.", + "description": "One property of a type. Identify it by `property`, the spelling, or by `name`, the display name: \"Cooking Time\" declares a property spelled exactly that. Never invent stored ids.", "properties": { "property": { "$ref": "#/$defs/propertyKey" @@ -617,7 +617,7 @@ "type": "string", "minLength": 1 }, - "description": "objects/files only: the types the property may point at, each as its derived id `type-` (\"type-habit\", \"type-task\") or its NFC display name (\"Habit\", \"Task\"). Export writes the derived id, unless a SINGLE-DOCUMENT run sets the `NoDerivedTypeIds` export mode, which writes the vocabulary spelling here instead; a bundle refuses that mode (SPEC §9). Empty or absent = any object." + "description": "objects and files only: the types the property may point at, each as its derived id `type-` (\"type-habit\") or its NFC display name (\"Habit\"). Empty or absent means any object." }, "include_time": { "type": "boolean", @@ -658,7 +658,7 @@ "name": { "minLength": 1, "pattern": "^[^\u0000-\u001f]+$", - "description": "When name supplies the property identity, readers NFC-normalize it before applying the 128-character writable-key bound. Standard JSON Schema cannot count characters after normalization, so this schema enforces the expressible non-empty/control-free part and semantic readers enforce the normalized maximum." + "description": "When `name` supplies the property identity, it is NFC-normalized before the 128-character key bound applies. This schema checks the non-empty, control-free part; readers enforce the bound." } } } @@ -728,7 +728,7 @@ }, "literalText": { "type": "string", - "description": "Raw content, verbatim — no inline markup is parsed here." + "description": "Raw content, verbatim: no inline markup is parsed here." }, "block": { "type": "object", @@ -740,7 +740,7 @@ "type": "integer", "minimum": 0, "maximum": 32, - "description": "Nesting depth; omit for top level. A block may be at most ONE level deeper than the block before it, and the first block is at 0." + "description": "Nesting depth; omit for top level. A block may be at most one level deeper than the block before it, and the first block is at 0." }, "type": { "enum": [ @@ -768,7 +768,7 @@ "table_of_contents", "dataview" ], - "description": "There is no title block — the title is the `name` property — and no image block: this format cannot fetch. A `row` contains only `column` blocks (indented under it); content nests under a column." + "description": "There is no title block; the title is the `name` property. There is no image block, since this format cannot fetch. A `row` contains only `column` blocks, and content nests under a column." } }, "allOf": [ @@ -1035,7 +1035,7 @@ "properties": {}, "additionalProperties": false }, - "description": "One `{}` per column — the entry only counts; the app assigns ids and widths. A row may not have more cells than there are columns. The implicit grid is rows × columns and must not exceed 100,000 pairs, including empty cells." + "description": "One `{}` per column; the entry only counts, and the app assigns ids and widths. A row may not have more cells than columns. The implicit grid is rows × columns, at most 100,000 pairs." }, "rows": { "type": "array", @@ -1062,18 +1062,18 @@ "properties": { "object_id": { "$ref": "#/$defs/documentId", - "description": "Set only for an inline dataview hosted by an object whose resolved type is neither Query nor Collection (§6.2). It must point to a Query or Collection object in the bundle. Omit on a Query or Collection's own dataview, and on a type document's own listing. The target's resolved type chooses the source and takes precedence over `is_collection` and the host's source fields. The authoring profile requires a non-empty id when present; omit the member instead of writing an empty string." + "description": "Set only for an inline dataview whose host is neither a Query nor a Collection, pointing at a Query or Collection in the bundle. Omit the member rather than writing an empty string." }, "is_collection": { "type": "boolean", - "description": "With `object_id` omitted, true selects THIS document's `collection_items`, a curated list (§6.2). Empty or absent membership means an empty collection. With `object_id` present, the target's resolved type chooses the source and this flag does not override it." + "description": "With `object_id` omitted, true selects this document's `collection_items`. With `object_id` present, the target's resolved type chooses the source and this flag does not override it." }, "properties": { "type": "array", "items": { "$ref": "#/$defs/dataviewProperty" }, - "description": "The properties available to the views — every property a view's columns, sorts, filters or group_by name, with its format." + "description": "The properties available to the views: every property a view's columns, sorts, filters or group_by name, with its format." }, "views": { "type": "array", @@ -1108,7 +1108,7 @@ }, "dataviewProperty": { "type": "object", - "description": "One property available to the views, with its format. `property` is the same member name — and the same spelling — the view's columns, sorts, filters and group_by use to refer to it.", + "description": "One property available to the views, with its format. `property` is the same member name, and the same spelling, the view's columns, sorts, filters and group_by use.", "required": [ "property", "format" @@ -1125,7 +1125,7 @@ }, "view": { "type": "object", - "description": "One way to look at the objects. No ids — the app assigns them.", + "description": "One way to look at the objects. No ids: the app assigns them.", "properties": { "type": { "enum": [ @@ -1164,7 +1164,7 @@ "items": { "$ref": "#/$defs/filterNode" }, - "description": "Top-level entries combine with AND; use one group node for OR." + "description": "Top-level entries combine with `and`; use one group node for `or`." }, "columns": { "type": "array", @@ -1271,7 +1271,7 @@ "current_year", "next_year" ], - "description": "Date properties, with equal/in/less/greater/less_or_equal/greater_or_equal: a rolling range instead of a fixed `value`. An \"overdue\" filter (`less` + a date) must sit in an `and` group with a `not_empty` on the same property, or undated objects match too." + "description": "Date properties, with equal/in/less/greater/less_or_equal/greater_or_equal: a rolling range instead of a fixed `value`. Pair an overdue filter with `not_empty` so undated objects do not match." } } } diff --git a/format/v2/schema/authoring/properties.schema.json b/format/v2/schema/authoring/properties.schema.json index a469b17..35d1fd5 100644 --- a/format/v2/schema/authoring/properties.schema.json +++ b/format/v2/schema/authoring/properties.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.anytype.io/anyblock/2.0/authoring/properties.schema.json", "title": "AnyBlock property dictionary — authoring subset", - "description": "One properties.json at the bundle root, beside index.json: the bundle's own properties, declared once, in one place. A strict subset of the full dictionary schema (../properties.schema.json) — every dictionary valid here is valid there. Declare a property here, reference it from a type's property_definitions by its key, and write its values in objects under the same key; the app mints the stored identity on import.", + "description": "One properties.json at the bundle root, beside index.json: the bundle's own properties, declared once, in one place. A strict subset of the full dictionary schema (../properties.schema.json): every dictionary valid here is valid there. Declare a property here, reference it from a type's property_definitions, and write its values in objects under the same key.", "x-app": "Anytype", "type": "object", "required": [ @@ -29,7 +29,7 @@ "$defs": { "property": { "type": "object", - "description": "One property. Identify it by `property` (the spelling your documents will write — the property's name: \"Streak\") or by `name` (\"Cooking Time\" declares a property spelled exactly that); `format` is required. Never invent stored ids — the app mints those on import.", + "description": "One property. Identify it by `property`, the spelling your documents will write (\"Streak\"), or by `name` (\"Cooking Time\"). `format` is required. Never invent stored ids.", "required": [ "format" ], @@ -59,7 +59,7 @@ "name": { "minLength": 1, "pattern": "^[^\u0000-\u001f]+$", - "description": "When name supplies the property identity, readers NFC-normalize it before applying the 128-character writable-key bound. Standard JSON Schema cannot count characters after normalization, so this schema enforces the expressible non-empty/control-free part and semantic readers enforce the normalized maximum." + "description": "When `name` supplies the property identity, it is NFC-normalized before the 128-character key bound applies. This schema checks the non-empty, control-free part; readers enforce the bound." } } } @@ -120,7 +120,7 @@ "minLength": 1, "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$", - "description": "The property's spelling — its display name, written exactly: \"Streak\", \"Last done\". A fresh name — a built-in property's name (\"Due date\") names the built-in property instead." + "description": "The property's spelling, its display name written exactly: \"Streak\", \"Last done\". Use a fresh name; a built-in property's name (\"Due date\") names the built-in instead." }, "name": { "type": "string", @@ -187,7 +187,7 @@ "type": "string", "minLength": 1 }, - "description": "objects/files only: the types the property may point at, each as its derived id `type-` (\"type-habit\", \"type-task\") or its NFC display name (\"Habit\", \"Task\"). Export writes the derived id, unless a SINGLE-DOCUMENT run sets the `NoDerivedTypeIds` export mode, which writes the vocabulary spelling here instead; a bundle refuses that mode (SPEC §9). Empty or absent = any object." + "description": "objects and files only: the types the property may point at, each as its derived id `type-` (\"type-habit\") or its NFC display name (\"Habit\"). Empty or absent means any object." }, "description": { "type": "string", diff --git a/format/v2/schema/index.schema.json b/format/v2/schema/index.schema.json index dfedef2..7bbda97 100644 --- a/format/v2/schema/index.schema.json +++ b/format/v2/schema/index.schema.json @@ -11,14 +11,14 @@ "properties": { "$schema": { "type": "string", - "description": "Which of the format's three grammars this document follows — an object document, a bundle index, or a property dictionary. OPTIONAL and DECORATIVE for validity: `formatVersion` alone gates the format, so a stale or invented URL here never makes a document invalid. What it does do is DISPATCH: a reader handed a file with no filename to go by uses this to pick the grammar, matching on the trailing `object|index|properties.schema.json` and ignoring the version segment. Without it a reader falls back to the document's shape, which can only tell the grammars apart when the document happens to carry a member unique to one. Every document this format writes carries it, and a hand-written one is easier for a reader to place if it does too." + "description": "Which of the format's three grammars this document follows. Optional, since `formatVersion` gates the version, but a reader dispatches on it and a URL naming another grammar is refused." }, "formatVersion": { "const": "2.0" }, "network_id": { "type": "string", - "description": "Optional opaque source network identifier from the exporting node's active configuration. Its value is not validated by export or bundle validation. Import uses it to determine whether remote files can be recovered. An absent, empty, or unrecognized identifier does not invalidate the bundle." + "description": "Optional opaque identifier of the exporting node's network, not validated. Import uses it to decide whether remote files can be recovered; an unrecognized value does not invalidate the bundle." }, "name": { "type": "string", @@ -31,13 +31,13 @@ }, "icon": { "$ref": "https://schemas.anytype.io/anyblock/2.0/object.schema.json#/$defs/icon", - "description": "The space's icon — exactly the shape `icon` has on any object (§2b), so an author never has to remember a second convention: an emoji, the **object id** of an image in the bundle, the raw content cid of an image no file object holds (a 1-to-1 space's icon, output-only), a built-in icon name, or a bare colour. Two flat keys used to stand here with no rule for which one wins, and they disagreed with the object surface about whether an image is a scalar or a list. This field was then narrowed to emoji-or-image, which silently dropped the icon of a space whose icon is a bare colour — 20 of 77 real spaces, the letter avatars the client draws when no image was ever set. An image needs the image object AND its file in the archive, which is why a generated bundle uses an emoji. (The installer resolves the space icon by image *name*, not id — the wiring does that lookup, so this field stays consistent with every other id in the format; an emoji or colour icon reaches no installer field at all, and is carried so a round trip does not lose it.)" + "description": "The space's icon, exactly the shape `icon` has on any object: an emoji, the object id of an image in the bundle, a raw content cid, a built-in icon name, or a bare colour." }, "entrypoint": { "type": "string", "minLength": 1, "pattern": "^[^_]", - "description": "The declared entry object's bundle-local id. Used as the space homepage when `homepage` is absent, on the first entry through the bundle install path and later entries (SPEC §2c). Declaring it does not insert or reorder sidebar widgets. For compatibility with legacy built-in archives that open the first profile widget once, authors may also list this object first. If absent, the first widget naming an object supplies the compatibility fallback; reserved listings are skipped." + "description": "The declared entry object's bundle-local id, used as the space homepage when `homepage` is absent. Declaring it does not insert or reorder sidebar widgets." }, "homepage": { "type": "string", @@ -53,11 +53,11 @@ "pattern": "^[^_]" } ], - "description": "What opens on entering the space, including the first entry through the bundle install path (SPEC §2c). An explicit homepage takes precedence over entrypoint. If absent, use entrypoint, then the first widget naming an object, skipping reserved listings; if none exists, the installer supplies its default dashboard. An unresolved explicit id is an error, not a reason to try a fallback. May name a bundle object or the reserved _widgets or _graph screens; explicit reserved values also take precedence." + "description": "What opens on entering the space. Wins over `entrypoint`; if absent use `entrypoint`, then the first widget naming an object, then the installer's default. An explicit id is never replaced." }, "widgets": { "type": "array", - "description": "Sidebar widgets in their declared order. Navigation fields do not insert or reorder them. When homepage and entrypoint are both absent, the first widget naming an object supplies the homepage fallback (SPEC §2c); reserved listings are skipped. A type needs a widget only for fast access to its objects (layout \"view\").", + "description": "Sidebar widgets in their declared order. Navigation fields do not insert or reorder them. When `homepage` and `entrypoint` are both absent, the first widget naming an object is the fallback.", "items": { "$ref": "#/$defs/widget" } @@ -67,19 +67,19 @@ "items": { "$ref": "#/$defs/widgetTarget" }, - "description": "The targets the client has already auto-added a widget for — its ledger for not re-adding one the user then deleted. Space state, not sidebar content: an entry usually names a widget that is NOT in `widgets` any more, which is the point. Entries are spelled like widget targets (an object id, or a reserved listing). Machine-written on export; an authored bundle has no reason to carry one." + "description": "The targets the client has already auto-added a widget for, so it does not re-add one the user deleted. Space state, not sidebar content. Written on export." }, "auto_widget_disabled": { "type": "boolean", - "description": "The user turned automatic widget creation off for this space entirely. Machine-written on export, like the ledger above." + "description": "The user turned automatic widget creation off for this space. Written on export." }, "manifest": { "$ref": "#/$defs/manifest", - "description": "Where to find what a reader cannot reach by walking the documents (§2c): the property dictionary and the bytes behind each file document. The format defines NO folder layout — objects/, types/, files/ are one exporter's convention — and a document is found by its id; a type document's id is its stored key spelled type- (§9), so no type table is needed or admitted here (§15 #26). An AUTHORED bundle may file a type document under some other id, and the object → type-document join then costs a scan; the member is still not admitted. The `NoDerivedTypeIds` export mode cannot produce such a bundle — it is scoped to a single document and the bundle composer refuses it, precisely because this join is the only one a bundle has (§9)." + "description": "Where to find what a reader cannot reach by walking the documents: the property dictionary and the bytes behind each file document. The format defines no folder layout." }, "unresolved": { "$ref": "#/$defs/unresolved", - "description": "What this bundle NAMES and cannot answer for (§2c). Optional, and present only when there is something to report — an index with no \"unresolved\" member says that it reports nothing, NEVER that every reference in every document resolves: what a writer checks here is bounded, so absence is not a completeness claim. Both losses reach a reader as silence — a property key that resolves to no definition, a reference that leads nowhere — and in both cases the reader's next question is whether the export is incomplete or the read was wrong. Only the writer can answer that, and this is where it does." + "description": "What this bundle names and cannot answer for. Present only when there is something to report; absence says it reports nothing, never that every reference resolves." } }, "$defs": { @@ -90,7 +90,7 @@ "properties": { "type": "string", "minLength": 1, - "description": "The property dictionary's path, relative to this file — properties.json at the bundle root (§2f). A pointer rather than an inline map because properties resolve by stored KEY through each document's own legend, and the dictionary is the file that answers for stored keys." + "description": "The property dictionary's path, relative to this file. It is properties.json at the bundle root unless the manifest names another path." }, "files": { "type": "object", @@ -98,16 +98,16 @@ "type": "string", "minLength": 1 }, - "description": "File object id → the blob's path, relative to this file (§2c, v0.47). The authoritative binding between a `kind: \"file_object\"` document and its bytes: the document itself carries no path — a document member is not a slot for archive bookkeeping — and every importer holding a file document must find its bytes, so the lookup lives where every other by-id lookup lives. Keys are object ids verbatim, an authored bundle writes them against its own minted ids and any layout it likes; adjacency of blob and document in files/ is one exporter's convention, never load-bearing. An entry that cannot be honoured — a key naming no document, a path escaping the bundle, a blob missing at the path — is a cross-document refusal for the tooling; a file document a present map does not bind is warned about (its bytes did not travel). The member has THREE states, and the third is the one a reader kept asking for. A populated map is the binding. An ABSENT member states nothing, which this format reads as a metadata-only export (§2c) — the mode inferred, and also what an authored bundle and every exporter written before the map existed produce. An EMPTY object, \"files\": {}, is the export SAYING it: this run enumerated its file documents and carried the bytes of none of them. Two bytes settle a real ambiguity: one audited space carries 666 file_object documents and not one blob, 68 of 79 measured bundles are in the same state, and an absent member alone cannot tell an export that chose metadata-only from one whose manifest never got written. Only the writer knows which, so only the writer can say it — a reader must not infer intent from an absence, and must not read an empty object as an error." + "description": "File object id to the blob's path, relative to this file. A populated map is the binding; `{}` is the export stating it carried no blobs; an absent member states nothing and reads the same way." } }, - "description": "Where to find what a reader cannot reach by walking the documents (§2c): the property dictionary and the bytes behind each file document. Types are NOT located here (§15 #26): a type document is found by its id, which is its stored key spelled type-, and every object states that key in type_internal_key. An AUTHORED bundle may file a type document under any id, and a reader that needs the join then indexes the type documents by their internal_key itself. The `NoDerivedTypeIds` export mode is not a source of such a bundle: it is scoped to a single document and the bundle composer refuses it, because this join is the only one a bundle has (§9)." + "description": "Where to find what a reader cannot reach by walking the documents: the property dictionary and the bytes behind each file document. Types are not located here; a type document is found by its id." }, "unresolved": { "type": "object", "additionalProperties": false, "minProperties": 1, - "description": "The bundle's account of the names it uses and cannot explain. Two lists, and each earns its place differently.\n\n`properties` RESTATES what properties.json already says per key (an entry whose format is the `unknown` sentinel, §2f). The restatement is the point: the per-key entry answers 'what is this key', one key at a time, in another file; this answers 'did this export lose definitions, and which' — a set, and a property of the export rather than of any key. index.json is where a bundle is described as a whole, so the set belongs here, and answering the question must not require opening a dictionary of hundreds of entries and filtering it.\n\n`targets` has no other home at all. Whether an id resolves is a cross-document fact no single document holds, and this file is the only one that names these ids. Stating them does NOT make them legal: a bundle whose index points at a document it does not carry is still refused by cross-document validation (§2c). What the statement buys is the distinction the refusal cannot make — an export that knew what it could not carry, against one that shipped a dangling reference without noticing.\n\nMeasured, so the shape is not theoretical: one audited 3,286-document space names 238 property keys nothing defines, and its homepage and 16 of its 23 widget targets name documents the bundle does not carry; 61 of 79 measured bundles carry at least one unresolved index reference.", + "description": "The bundle's account of the names it uses and cannot explain: property keys nothing defines, and ids no document in the bundle carries.", "properties": { "properties": { "type": "array", @@ -118,7 +118,7 @@ "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$" }, - "description": "The stored property keys the bundle's documents reference and nothing could define — mostly relations the user deleted, whose definition went with them. Written VERBATIM: a key nothing defines has no spelling but itself, and nothing may ever be derived from a stored id (§2f). Each also has a dictionary entry carrying `format: \"unknown\"`, so the key resolves to something; this list is the same fact stated as a set." + "description": "The stored property keys the bundle's documents reference and nothing could define, written verbatim. An export also gives each one a dictionary entry whose format is `unknown`." }, "targets": { "type": "array", @@ -128,7 +128,7 @@ "minLength": 1, "pattern": "^[^_]" }, - "description": "The ids this index names that no document in the bundle carries — an `entrypoint`, a `homepage`, a widget `target`, an image icon. Spelled like every other reference in this file, the derived-id fold included (§9), so the report and the slot it reports on name the same thing. A reserved listing (`_set`, `_widgets`) never appears: those name built-in screens, resolve everywhere, and are not the bundle's to carry. Neither does the auto-widget ledger's content — an entry there usually names a widget the user deleted, which is what the ledger is FOR, so a missing document is its normal state rather than a loss." + "description": "The ids this index names that no document in the bundle carries: an `entrypoint`, a `homepage`, a widget `target`, an image icon. A reserved listing never appears here." }, "deleted": { "type": "array", @@ -138,7 +138,7 @@ "minLength": 1, "pattern": "^[^_]" }, - "description": "The subset of `targets` the space DELETED: each names a tombstone in the source store. A reference to a deleted object is ordinary Anytype state — the app does not rewrite every object that mentions a deleted one — so a reader treats these as known, by design, and a restore keeps the id and writes a tombstone rather than a dangling reference. Every entry must also appear in `targets`, and none may appear in `omitted`." + "description": "The subset of `targets` the space deleted, each naming a tombstone in the source store. Every entry must also appear in `targets`, and none in `omitted`." }, "omitted": { "type": "array", @@ -148,7 +148,7 @@ "minLength": 1, "pattern": "^[^_]" }, - "description": "The subset of `targets` the space still HOLDS and this export did not write: an archived object under an export without archived objects, or an object outside a partial export's scope. Not a loss in the space, only in this bundle. Every entry must also appear in `targets`, and none may appear in `deleted`. A target in neither subset is ABSENT — the space had no row for it, typically because it had not synced — and that is the one class a reader should warn about." + "description": "The subset of `targets` the space still holds that this export did not write, such as an archived or out-of-scope object. Every entry must also appear in `targets`, and none in `deleted`." }, "types": { "type": "array", @@ -158,7 +158,7 @@ "minLength": 6, "pattern": "^type-" }, - "description": "The types this bundle's documents name by derived id — `type-` in a `type_internal_key`, a `template_for`, an `object_types` entry — that no type document carries and the source space has no row for: an object or template an old import created with a type it never made. Spelled as the derived id, the address a document uses and the validator checks; never folded. A reader imports such an object as a Page, and a template as one for Page, and warns; a slot that merely REFERENCES the type (a `query_source.types` entry, a property's `object_types`) drops the entry instead of retyping anything. Declaring the loss is what makes the bundle admissible on the full surface; an undeclared one is still refused as an exporter bug, and so is a declaration naming a type the bundle actually carries." + "description": "Types named by derived id that no type document carries and the source space never held. An object of one imports as a Page; a slot that only references it drops the entry instead." } } }, @@ -182,7 +182,7 @@ "pattern": "^[^_]" } ], - "description": "An object id from this bundle (a page, a type, a set, a collection), or one of the eight reserved listings — the built-in screens a live sidebar can show. The leading `_` is the platform's address space and is what keeps the two kinds of target apart: an object id from the bundle may never begin with one, so a bundle cannot shadow a listing with an object of its own. All eight survive import (widget.IsPredefinedWidgetTargetId knows their wire spellings); a `_`-prefixed name outside the inventory is refused by name, with the inventory in the message, because it can only be a typo." + "description": "An object id from this bundle, or a reserved listing: `_favorite`, `_set`, `_collection`, `_all_objects`, `_recent` (edited), `_recent_open` (opened), `_chat`, `_bin`. Object ids never start with `_`." }, "widget": { "type": "object", @@ -198,11 +198,11 @@ "layout": { "$ref": "https://schemas.anytype.io/anyblock/2.0/object.schema.json#/$defs/widgetLayout", "default": "link", - "description": "How the widget renders — the widget BLOCK's own §5 vocabulary, one $ref rather than a copy. `link` is a single row; `tree` expands children; `list` and `compact_list` show contents; `view` renders a type's or query's own view." + "description": "How the widget renders: `link` is a single row, `tree` expands children, `list` and `compact_list` show contents, `view` renders a type's or query's own view." }, "limit": { "$ref": "https://schemas.anytype.io/anyblock/2.0/object.schema.json#/$defs/widgetListingLimit", - "description": "How many entries a listing widget shows. Ignored by `link`. The cap is the object schema's $defs/widgetListingLimit — one $ref rather than a copy, like the display members beside it." + "description": "How many entries a listing widget shows, ignored by `link`. The cap is the object schema's $defs/widgetListingLimit." }, "view_id": { "type": "string", @@ -211,19 +211,19 @@ }, "auto_added": { "type": "boolean", - "description": "The client added this widget automatically (a new type, the bin) rather than the user placing it. Machine-written on export; carried so a restored sidebar keeps behaving like the original — the client treats auto-added widgets as its own to manage." + "description": "The client added this widget automatically rather than the user placing it. Written on export, so a restored sidebar keeps behaving like the original." }, "card_style": { "$ref": "https://schemas.anytype.io/anyblock/2.0/object.schema.json#/$defs/cardStyle", - "description": "How the widget's row renders — the link block's own §5 member, one $ref rather than a copy. Defaults to `text`." + "description": "How the widget's row renders, the link block's own member. Defaults to `text`." }, "icon_size": { "$ref": "https://schemas.anytype.io/anyblock/2.0/object.schema.json#/$defs/iconSize", - "description": "The icon size on the widget's card — the link block's own §5 member. Defaults to `none`." + "description": "The icon size on the widget's card, the link block's own member. Defaults to `none`." }, "description": { "$ref": "https://schemas.anytype.io/anyblock/2.0/object.schema.json#/$defs/linkDescription", - "description": "What the widget's card shows under the title — the link block's own §5 member. Defaults to `none`." + "description": "What the widget's card shows under the title, the link block's own member. Defaults to `none`." }, "properties": { "type": "array", @@ -233,7 +233,7 @@ "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$" }, - "description": "The property SPELLINGS shown on the widget's card — the link block's own §5 member. They resolve through the bundle's property dictionary (properties.json), the file that answers for stored keys; there is no per-document legend here." + "description": "The property spellings shown on the widget's card. They resolve through the bundle's property dictionary, properties.json; there is no per-document legend here." } } } diff --git a/format/v2/schema/object.schema.json b/format/v2/schema/object.schema.json index 9792976..69fbf30 100644 --- a/format/v2/schema/object.schema.json +++ b/format/v2/schema/object.schema.json @@ -8,7 +8,7 @@ "properties": { "$schema": { "type": "string", - "description": "Which of the format's three grammars this document follows — an object document, a bundle index, or a property dictionary. OPTIONAL and DECORATIVE for validity: `formatVersion` alone gates the format, so a stale or invented URL here never makes a document invalid. What it does do is DISPATCH: a reader handed a file with no filename to go by uses this to pick the grammar, matching on the trailing `object|index|properties.schema.json` and ignoring the version segment. Without it a reader falls back to the document's shape, which can only tell the grammars apart when the document happens to carry a member unique to one. Every document this format writes carries it, and a hand-written one is easier for a reader to place if it does too." + "description": "Which of the format's three grammars this document follows. Optional, since `formatVersion` gates the version, but a reader dispatches on it and a URL naming another grammar is refused." }, "formatVersion": { "const": "2.0" @@ -50,7 +50,7 @@ }, "id": { "type": "string", - "description": "This document's own address: the string every reference to it spells (§9). Three spellings are RESERVED and say what the document IS. `type-` is a type document's id, and belongs only to the type whose `internal_key` is that remainder. `participant-` is a participant document's id — and so is the BARE `` on its own: a base58 account identity, 48 characters carrying a version byte and a CRC16 checksum, the spelling documents written before the prefix used and the one an object-reference slot still reads as that member. No other kind may carry a bare account identity here, for that reason: a reference written that way rebuilds into `_participant__`, so the document would be addressed by nothing that names it. The checksum makes that half semantic — no schema can verify a CRC16 — so it is stated here and enforced by the reader, while the two prefixes are refused by the conditions below. Every other id is an ordinary address, and a `-` elsewhere (`page-welcome`) is an ordinary slug." + "description": "This document's own address, the string every reference spells. Three spellings are reserved: `type-` for a type, `participant-` and a bare `` for a participant." }, "type": { "type": "string", @@ -59,15 +59,15 @@ "template_for": { "type": "string", "minLength": 1, - "description": "Templates only: the target type, written as its derived id `type-` (§9) — a reference by key, the same spelling every id-valued slot folds a type to. A display name or the legacy `ot-` is accepted on input; canonical export writes the derived id — except in a single document exported under the `NoDerivedTypeIds` mode (§9, a mode a bundle refuses), where this slot carries the VOCABULARY spelling instead, the same word the envelope `type` writes for that type." + "description": "Templates only: the target type. In a bundle it is the derived id `type-`; a display name or the legacy `ot-` is accepted on input." }, "internal_key": { - "description": "The STORED identity key of a definition document (a type, a property, a property option) — the uniqueKey's internal part, written verbatim. This is an id the app MINTS, not something an author writes: for a custom property or type it is a bson id like 6a83296f61fab2265263ae34, which no author can produce, and for a bundled one it is the bundled camelCase key (dueDate). Export writes it for fidelity; an authored definition document may omit it, and the import wiring then mints a fresh internal key exactly as the app does when a user creates one. It is deliberately NOT a spelling: unlike every key slot in §3 it is never translated through a legend or the bundled table, so it does not match the display-name spelling documents use (that spelling is the `property` member of a property definition). Charset is a deny rule, not an allowlist — a property option's stored key is built from its NAME, so keys like completion_status_Not Started are real stored keys.", + "description": "The stored identity key of a type, property or property option, written verbatim. The app mints it: a bson id for a custom one, the camelCase key for a bundled one. An author may omit it.", "type": "string", "minLength": 1 }, "property_settings": { - "description": "Only on property documents (kind \"property\" or \"bundled_property\"), where it is REQUIRED: the definition of the property this document IS — one propertyDefinition (§2d, §2e). Three members travel today, each mirroring stored presence EXACTLY, value included: `format` (required — a §3 format NAME, never a raw enum number: the vocabulary `type_settings.property_definitions[].format` speaks, plus `map`, which only a property document may state ($defs/propertyFormat); stands for the stored relationFormat key, which `properties` refuses), `include_time` (present exactly when the stored relationFormatIncludeTime key is; meaningful on format \"date\" only, and a stored null travels as null), and `object_types` (present exactly when the stored relationFormatObjectTypes key is; the target types in priority order — a type-key slot exactly like `type_settings.property_definitions[].object_types`, each written as the type's derived id `type-` (§9), with a display name or the legacy `ot-` accepted on input; meaningful on \"objects\"/\"files\" only). The members refused with `false` have a home already: `internal_key` is the envelope's (and `property`, the spelling, is derived from it — a property document never states its own spelling), `name` and `description` are properties', `options` are a property-definition entry's — the dictionary's (§2f) or a type's property_definitions declaration (§2a); a bundle carries no option documents (§15 #21), so no third surface states a select vocabulary — and max_count/readonly/default_value still travel in `properties` under their stored keys until the dictionary lifts them — admitting a second spelling of any of them here would be the §15 #14 disease this group exists to end.", + "description": "Only on property documents, where it is required: the definition of the property this document is. `format` is required; `property`, `name`, `options` and the other stored members are refused here.", "type": "object", "allOf": [ { @@ -101,10 +101,10 @@ "uninstalled": { "type": "boolean", "const": true, - "description": "Only on type documents (kind \"object_type\"): the user REMOVED this type from the space (stored `isUninstalled`), and the bundle carries it for backup fidelity — its objects still carry the key, and the definition is still the space's. The mirror of the member a removed property's declaration and dictionary entry carry (§2f). Written `true` only; absent is the same statement as `false`. A reader restores it hidden, as the user left it, never as an installed type; a type that owns the same stored key and is live in the destination stays live. Output-only: the authoring subset refuses it, because an author declares a type to use it." + "description": "Type documents only: the user removed this type, and the bundle carries it for backup fidelity. Written `true` only; absent means `false`. An author never writes it; a reader restores it hidden." }, "type_settings": { - "description": "Only on type documents (kind \"object_type\" or \"bundled_object_type\"): everything that defines the TYPE, in one gated subtree (§2a). `layout` is the recommended layout of objects of this type (a layout name; a stored number outside the vocabulary passes through raw). `api_key` is the type's public API key — NOT a slug: of 1,326 corpus type documents with one, it differs from the document's own spelling in 247 (the `property` type's api key is `relation`, the word the public API kept when the format renamed the concept) — calling it a slug would imply it is the term used elsewhere in the document, which for those 247 it is not. `plural_name` is the plural display name. `default_template` is the object id of the template new objects of this type start from. `default_view` is the default view type (a §6.2 view-type name; a stored number outside the vocabulary passes through raw). `property_definitions` is the type's property list — one propertyDefinition + `section` per entry (§2e); it lived at the document root as `type_properties` until v0.32, and the word is `property_definitions` rather than `properties` because the document already uses that word for property VALUES at the root — one word per concept.", + "description": "Type documents only: everything that defines the type. Carries `layout`, `api_key`, `plural_name`, `default_template`, `default_view` and `property_definitions`, the type's property list.", "type": "object", "properties": { "layout": { @@ -149,11 +149,11 @@ }, "file_remote": { "type": "string", - "description": "File and file_object documents only: standard padded base64 of UTF-8 JSON describing remote file access. The decoded payload has its own integer version, independent of formatVersion (version 1 schema: https://schemas.anytype.io/anyblock/file-remote/1.schema.json). Invalid or unsupported payloads are ignored with a warning; they do not invalidate the object. Bundles must report an unresolved file when no usable remote metadata or embedded bytes remain." + "description": "File and file_object documents only: padded base64 of UTF-8 JSON describing remote file access. The decoded payload carries its own version. An unusable payload is ignored with a warning." }, "property_internal_keys": { "type": "object", - "description": "Legend: the stored property key each SPELLING in this document names (§3) — a spelling is a display name, so this is the map from \"Cooking time\" to the key the space stores it under. Present only for keys the bundled table cannot invert; a reader consults it before its own vocabulary. A value is a stored key and obeys the same writable-key rule as a property name (§3).", + "description": "Legend: the stored property key each spelling in this document names, mapping a display name like \"Cooking time\" to the key the space stores it under. A reader consults it first.", "propertyNames": { "pattern": "^[^\u0000-\u001f]+$", "maxLength": 128 @@ -170,11 +170,11 @@ "minLength": 1, "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$", - "description": "The STORED type key the envelope `type` spelling names (§2, §3): the bundled key (`page`, `task`) or the minted key of a space's own type, written on EVERY typed document — bundled or not — so a reader resolves the key and treats the spelling as a caption. The type document is `type-` (§9). In a SINGLE DOCUMENT exported under the `NoDerivedTypeIds` mode the type document keeps its STORE id and nothing in this document names it — which is why that mode stops at one document and a bundle refuses it, this join being the only one a bundle has (§9). A scalar, because an object has exactly one type; the former `type_internal_keys` map is retired (§15 #28)." + "description": "The stored type key the document's `type` spelling names: a bundled key (`page`, `task`) or a space's own minted key, on every typed document. In a bundle the type document is `type-`." }, "option_ids": { "type": "object", - "description": "Legend: the id of the option each select/multi_select NAME in this document stands for (§3, §9a). Nested: an outer key is a property spelling as this document writes it, an inner key is an option name exactly as the value slot spells it, and the value is the full option id. Written unconditionally wherever export spells an option by name; absent from an id-less export. Read as a HINT, not an address: an id is honoured only where the target space still serves it as a live option of that property, and otherwise the name resolves as it would without the legend. An outer key naming a property the document never spells is reported as a warning — the entry can never be consulted.", + "description": "Legend: the id of the option each select or multi_select name in this document stands for. An outer key is a property spelling, an inner key an option name. Read as a hint, not an address.", "propertyNames": { "pattern": "^[^\u0000-\u001f]+$", "maxLength": 128 @@ -204,7 +204,7 @@ "items": { "type": "string" }, - "description": "For a COLLECTION object: the ids of its members, in order, from the internal store key `objects` (§2). This is the only place its membership is written. A dataview whose `object_id` names this collection reads the target's list; without an object_id, `is_collection: true` selects the host's list (§6.2). Absent and [] both mean empty membership; canonical export omits the empty array. The import wiring resolves the object's type; payload presence alone never determines it. May coexist with `query_source`: preserve both, but each dataview uses only its selected source, with no union, intersection, or fallback when empty. Entries are object references (§9). The old object-root `items` spelling is rejected; rename it to `collection_items` and keep its ordered ids." + "description": "For a collection object: the ids of its members, in order. This is the only place its membership is written. Absent and [] both mean empty; canonical export omits the empty array." }, "store": { "type": "object", @@ -213,7 +213,7 @@ "root": { "type": "object", "x-output-only": true, - "description": "Escape hatch for non-default root-block attributes. The two members the reader understands are typed here so an external validator running this schema alone reaches the same verdict the reader does (§12); any other member is carried and ignored.", + "description": "Escape hatch for non-default root-block attributes. The two members a reader understands are typed here; any other member is carried and ignored.", "properties": { "fields": { "type": "object" @@ -284,7 +284,7 @@ } }, { - "$comment": "The derived-id reservation, kind half (SPEC §9): `type-` is a TYPE document's id, so no other kind may wear the prefix. Stated as a constraint on `id` under a non-type `kind` so the verdict lands on the member at fault. The other half — that the remainder is this document's own internal_key — is semantic: JSON Schema cannot compare one member against a substring of another.", + "$comment": "Only a type document's id may start `type-`. The rest of the rule, that what follows is that document's own internal_key, is checked by the reader: JSON Schema cannot compare one member against part of another.", "if": { "properties": { "kind": { @@ -310,7 +310,7 @@ } }, { - "$comment": "The same reservation for participants: `participant-` is a participant document's id. That the remainder is a real account identity — a base58 strkey with its version byte and CRC16 checksum — is semantic, as no schema can express a checksum. The BARE identity, the spelling written before the prefix and still read as one, is reserved for participant documents by the same rule and for the same reason; it is semantic in whole, so it appears in the description of `id` and nowhere in this grammar.", + "$comment": "Only a participant document's id may start `participant-`, or be a bare account identity. Both remainders are checked by the reader: no schema can verify a checksum. The bare form is stated on `id` alone.", "if": { "properties": { "kind": { @@ -336,7 +336,7 @@ "$defs": { "querySource": { "type": "object", - "description": "For a query object (stored type key `set`), what the query ranges over (§6.2), promoted out of the stored `setOf` detail. Two lists distinguish targets: a TYPE target matches every object OF that type; a PROPERTY target matches every object that CARRIES that property, even with an empty value. Several targets combine with OR. Order across lists has no meaning; within each list the stored order is preserved. THREE STATES: absent means no query declaration; present and empty (`{}`) declares a query with no source targets; populated declares the source to evaluate. Empty inner lists may be omitted, but the enclosing group remains. May coexist with `collection_items`; a dataview uses the source selected by its target's resolved type or, without an object_id, by its own source fields. It never merges the two sources or switches because one is empty. Query results require evaluation against the space; the bundle carries the definition.", + "description": "For a query object, what the query ranges over. A type target matches every object of that type, a property target every object carrying it; targets combine with `or`. `{}` is a query with no target.", "properties": { "types": { "type": "array", @@ -344,7 +344,7 @@ "type": "string", "minLength": 1 }, - "description": "Type targets: every object of the type matches the query. Each is written as the type's derived id `type-` (\u00a79) \u2014 the same spelling `template_for` and every `object_types` write, and the id the type's own document carries, so a reader joins an entry to that document by string equality with no rule to apply (\u00a72c). A display name or the legacy `ot-` is accepted on input; canonical export writes the derived id \u2014 except in a single document exported under the `NoDerivedTypeIds` mode (\u00a79, a mode a bundle refuses), where this slot carries the VOCABULARY spelling instead, the same word the envelope `type` writes. One further spelling reaches this list and only by degradation: an entry no resolver could classify keeps the store id it was written with, and export warns \u2014 types is where such an entry goes because the stored slot's declared targets are types and every path that builds a view from a source reads the first entry as one." + "description": "Type targets: every object of the type matches. Each is the type's derived id `type-`, the id that type's own document carries. A display name or legacy `ot-` is read on input." }, "properties": { "type": "array", @@ -354,7 +354,7 @@ "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$" }, - "description": "Property targets: every object that CARRIES the property matches the query, whatever its value \u2014 an object holding it empty belongs. Each is written as the property's STORED internal key, bare (`lastModifiedDate`, `6a83296f61fab2265263ae34`) \u2014 the same spelling `internal_key` carries everywhere else. Bare, and never a derived id, because a property has no address to derive one from: a bundle carries no property documents (\u00a715 #23), so a property travels as a dictionary entry and every slot in this format that identifies one holds either a display-name spelling or this key. A reader resolves an entry in two steps: the shipped bundled property table, which every reader carries; then this bundle's `properties.json`, by `internal_key` \u2014 the dictionary's used-key census counts an entry here, so a key the bundle mints is defined there. `NoDerivedTypeIds` does not touch this list: a stored key is not a derived id." + "description": "Property targets: every object carrying the property matches, whatever its value. Each is the property's stored internal key, bare (`lastModifiedDate`, `6a83296f61fab2265263ae34`)." } }, "additionalProperties": false @@ -365,7 +365,7 @@ "card", "inline" ], - "description": "How a link renders: a plain text row (the default), a card, or inline. One definition for every surface that shows a link — the link block and a bundle index's widgets both $ref it, so they cannot disagree." + "description": "How a link renders: a plain text row (the default), a card, or inline. Shared by the link block and a bundle index's widgets." }, "iconSize": { "enum": [ @@ -391,15 +391,16 @@ "compact_list", "view" ], - "description": "How a sidebar widget renders: `link` is a single row (the default), `tree` expands children, `list` and `compact_list` show contents, `view` renders a type's or query's own view. One definition for the widget block and a bundle index's widgets." + "description": "How a sidebar widget renders: `link` is a single row (the default), `tree` expands children, `list` and `compact_list` show contents, `view` renders a type's or query's own view." }, "widgetListingLimit": { "type": "integer", "minimum": 0, "maximum": 100, - "description": "How many entries a sidebar listing widget shows — the product cap for a bundle index's flat widget (the live corpus maximum is 50). One definition, referenced by the index schema, so the cap cannot drift into a second spelling. The widget BLOCK's own `limit` member deliberately takes the whole int32 range instead: the block is the fidelity fallback for exactly the widget the index refuses over this cap (the widget-object lift keeps the whole document), so the cap must not bind there." + "description": "How many entries a sidebar listing widget shows: the product cap for a bundle index's flat widget. The widget block's own `limit` takes the whole int32 range instead." }, "propertyMap": { + "description": "The object's property values, keyed by spelling. On a list format a bare value and a one-element array are the same value; on a single-valued format an array is not unwrapped.", "type": "object", "propertyNames": { "pattern": "^[^\u0000-\u001f]+$", @@ -429,10 +430,10 @@ "minLength": 1, "maxLength": 255, "pattern": "^[^/]+$", - "description": "The id of an object in this space, a bundle-local slug, or a raw content CID — the WHOLE string, and nothing else. A reference carries no caption, display hint or name: there is no separator inside it and no half of it to discard, so `bafyrei\u2026#alice` is one id, one that no space mints, and it names nothing. (Earlier drafts of this format spelled a reference `#` and asked readers to split at the first `#`. That form is gone; `#` is now an ordinary character, kept legal here only because a reference is written through verbatim from whatever a space stores.) To show a human-readable name for a reference, look the id up: index the bundle's documents by their envelope `id` and read the target's `Name`. Never a URL and never a filesystem path — this format does no I/O, so it can only name an object or content the reader can resolve. To supply an image by URL, use a layer that can fetch it: it uploads the image, mints the file object and writes that object's id here. An object id uses the dag-cbor CID codec (normally beginning `bafyrei`). A raw or dag-pb content CID (normally beginning `bafybei`) is content, not an object: it resolves against content storage, an icon carries one in its `cid` variant (§2b), and no reference slot should hold one." + "description": "The id of an object in this space or a bundle-local slug, whole: nothing appended, nothing split. An object id is dag-cbor (`bafyrei`); a content cid (`bafybei`) is not one. Never a URL or file path." }, "iconColor": { - "description": "One of the ten palette colours — the same palette select options use (§2a) — or, for a stored value the palette has no name for, its raw number. That escape is bounded at 9007199254740991 (2^53-1), the same bound `size` carries: it is the largest integer this format moves through a v1 float value and writes back out denoting the same number, and the reference exporter drops a stored value above it with a warning rather than emitting one this schema refuses.", + "description": "One of the ten palette colours, the same palette select options use, or a raw number, 1 to 9007199254740991, for a stored value the palette has no name for.", "type": [ "string", "integer" @@ -453,11 +454,11 @@ "minLength": 1, "maxLength": 64, "pattern": "^[^/]+$", - "description": "A name from a vocabulary this format does not enumerate (the app's icon, cover-colour and gradient sets). Validation checks the shape only — a name outside the app's set is accepted here and shows as no icon." + "description": "A name from a vocabulary this format does not enumerate: the app's icon, cover-colour and gradient sets. Only the shape is checked; an unknown name shows as no icon." }, "iconVariants": { "type": "object", - "description": "The icon variant machinery, shared by every icon slot. It carries the per-variant if/then rules but NOT the set of variant names: each slot states its own, so a value outside a slot's set fails exactly one enum and the reader is told one thing.", + "description": "The icon variant rules shared by every icon slot. It carries the per-variant conditions but not the set of variant names: each slot states its own.", "required": [ "format" ], @@ -517,7 +518,7 @@ "format": {}, "file": { "$ref": "#/$defs/objectRef", - "description": "The image: the id of a file object in this bundle. A raw content cid — a participant avatar, an invite or 1-to-1 space icon — is not a file object and travels in the `cid` variant instead; a `file` holding one is read as that variant for compatibility with documents written before it existed, and is never written." + "description": "The image, as the id of a file object in this bundle. A raw content cid travels in the `cid` variant instead." }, "color": { "$ref": "#/$defs/iconColor" @@ -549,7 +550,7 @@ "minLength": 46, "maxLength": 255, "pattern": "^[^/]+$", - "description": "The image's raw content cid (dag-pb or raw codec, normally beginning `bafybei`): an image no file object in this bundle holds — a participant's avatar, a 1-to-1 space's icon — which a client fetches by content id through the gateway or the identity repo. Never an object reference: nothing in the bundle resolves it, nothing declares it unresolved, and a reader that cannot reach content storage shows no icon. Output-only: an author cannot mint a content address, and supplies an image as a file object instead." + "description": "The image's raw content cid (dag-pb or raw codec, normally starting `bafybei`), fetched from content storage. Never an object reference. Output only: an author supplies an image as a file object." }, "color": { "$ref": "#/$defs/iconColor" @@ -586,7 +587,7 @@ "type": "string", "minLength": 1, "x-output-only": true, - "description": "A legacy emoji superseded by this named icon, carried so export loses nothing (§2b). Export writes it; a document that supplies it is not choosing an icon — `format` already did." + "description": "A legacy emoji superseded by this named icon, carried so export loses nothing. Export writes it; supplying it does not choose the icon, since `format` already did." } } } @@ -619,7 +620,7 @@ ] }, "icon": { - "description": "The object's icon: ONE of four kinds, chosen by `format` (§2b). Replaces the flat iconEmoji/iconImage/iconName/iconOption keys, which are refused in `properties`.", + "description": "The object's icon: one of five kinds, chosen by `format`. The flat iconEmoji, iconImage, iconName and iconOption keys are refused in `properties`.", "allOf": [ { "$ref": "#/$defs/iconVariants" @@ -639,7 +640,7 @@ } }, "plainIcon": { - "description": "An icon restricted to the two kinds a callout block (§5.2) and a bundle index (§2c) can hold: an emoji, or an image object in this space. The same shape as the object icon, minus the two variants only an object has.", + "description": "An icon restricted to what a callout block and a bundle index can hold: an emoji, or an image as a file object in this space or the raw `cid` variant export writes.", "allOf": [ { "$ref": "#/$defs/iconVariants" @@ -657,7 +658,7 @@ }, "cover": { "type": "object", - "description": "The object's cover: ONE of three kinds, chosen by `format` (§2b). Replaces the flat coverId/coverType/coverScale/coverX/coverY keys, which are refused in `properties`. `cover_id` alone cannot be read: the same string names a colour under one type and a gradient under another.", + "description": "The object's cover: one of three kinds, chosen by `format`. The flat coverId, coverType, coverScale, coverX and coverY keys are refused in `properties`.", "required": [ "format" ], @@ -700,7 +701,7 @@ "prebuilt" ], "x-output-only": true, - "description": "Where this image came from (§2b). Export writes it; a generator omits it — an uploaded image has no provenance to claim." + "description": "Where this image came from. Export writes it; an author omits it, since an uploaded image has no provenance to claim." }, "scale": { "type": "number" @@ -907,7 +908,7 @@ "fields": { "type": "object", "x-output-only": true, - "description": "Verbatim internal per-block data, minus the keys lifted into first-class props: a `code` block's `lang` becomes `language` (§5.1), and a TABLE column's `width` becomes that column entry's `width` in pixels (§6.1). Output-only (§4a) — export writes what was stored, import stores it back, and no member here is interpreted, defaulted or refused.\nIt is a known inventory rather than an open world. A sweep of 24,889 exported documents found these keys and no others: `width` — on a `column` the layout column's share of its row as a fraction, and on an `image`, `video` or `embed` the same fraction of the text column (597 occurrences, and the only place a layout column's width is written at all); `isUnwrapped` on a `code` block and `isRtlDetected` on a paragraph — editor display flags; `type` on an `embed` — the diagram language a `kroki` processor renders, e.g. `blockdiag`; `lang` on a block that is not `code`, where nothing lifted it; `_link_migrated` — a migration marker the app stamped; and a `link` block's `cardStyle`, `iconSize`, `description` and `relations`, the pre-2.0 numeric spelling of the `card_style`, `icon_size`, `description` and `properties` beside them. Those four are stale — real exports carry `cardStyle: 0` next to `card_style: \"card\"`, which disagree. The first-class prop is the value; the bag keeps the old numbers only so the round trip loses nothing." + "description": "Verbatim internal per-block data, minus keys lifted into first-class properties (`lang` to `language`). Output only. A link's `cardStyle`, `iconSize`, `description` and `relations` are stale copies." } }, "allOf": [ @@ -940,7 +941,7 @@ }, "text": { "type": "string", - "description": "The block's text. Formatting is expressed INLINE, inside this string — this format has no `marks` array and no offsets (§8). A CommonMark-inline subset plus a small tag whitelist, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` — an external link\n `[text](anytype://object?objectId=)` — a link to an object in this space\n `\">text` — a decorated object reference (icon + name)\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no HTML beyond those tags. `\\n` is a soft line break within the block. A literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` in prose is backslash-escaped." + "description": "The block's text, with formatting written inline. There is no marks array and no offsets. The accepted syntax, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` links out\n `[text](anytype://object?objectId=)` links to an object in this space\n `\">text` is a decorated object reference\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no other HTML. `\\n` is a soft line break. Escape a literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` with a backslash." } } } @@ -969,7 +970,7 @@ }, "text": { "type": "string", - "description": "The block's text. Formatting is expressed INLINE, inside this string — this format has no `marks` array and no offsets (§8). A CommonMark-inline subset plus a small tag whitelist, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` — an external link\n `[text](anytype://object?objectId=)` — a link to an object in this space\n `\">text` — a decorated object reference (icon + name)\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no HTML beyond those tags. `\\n` is a soft line break within the block. A literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` in prose is backslash-escaped." + "description": "The block's text, with formatting written inline. There is no marks array and no offsets. The accepted syntax, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` links out\n `[text](anytype://object?objectId=)` links to an object in this space\n `\">text` is a decorated object reference\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no other HTML. `\\n` is a soft line break. Escape a literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` with a backslash." } } } @@ -995,7 +996,7 @@ }, "text": { "type": "string", - "description": "The block's text. Formatting is expressed INLINE, inside this string — this format has no `marks` array and no offsets (§8). A CommonMark-inline subset plus a small tag whitelist, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` — an external link\n `[text](anytype://object?objectId=)` — a link to an object in this space\n `\">text` — a decorated object reference (icon + name)\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no HTML beyond those tags. `\\n` is a soft line break within the block. A literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` in prose is backslash-escaped." + "description": "The block's text, with formatting written inline. There is no marks array and no offsets. The accepted syntax, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` links out\n `[text](anytype://object?objectId=)` links to an object in this space\n `\">text` is a decorated object reference\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no other HTML. `\\n` is a soft line break. Escape a literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` with a backslash." } } } @@ -1018,7 +1019,7 @@ }, "text": { "type": "string", - "description": "The block's text. Formatting is expressed INLINE, inside this string — this format has no `marks` array and no offsets (§8). A CommonMark-inline subset plus a small tag whitelist, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` — an external link\n `[text](anytype://object?objectId=)` — a link to an object in this space\n `\">text` — a decorated object reference (icon + name)\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no HTML beyond those tags. `\\n` is a soft line break within the block. A literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` in prose is backslash-escaped." + "description": "The block's text, with formatting written inline. There is no marks array and no offsets. The accepted syntax, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` links out\n `[text](anytype://object?objectId=)` links to an object in this space\n `\">text` is a decorated object reference\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no other HTML. `\\n` is a soft line break. Escape a literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` with a backslash." } } } @@ -1044,7 +1045,7 @@ }, "text": { "type": "string", - "description": "The block's text. Formatting is expressed INLINE, inside this string — this format has no `marks` array and no offsets (§8). A CommonMark-inline subset plus a small tag whitelist, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` — an external link\n `[text](anytype://object?objectId=)` — a link to an object in this space\n `\">text` — a decorated object reference (icon + name)\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no HTML beyond those tags. `\\n` is a soft line break within the block. A literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` in prose is backslash-escaped." + "description": "The block's text, with formatting written inline. There is no marks array and no offsets. The accepted syntax, and nothing else:\n `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``\n `[text](https://example.com)` links out\n `[text](anytype://object?objectId=)` links to an object in this space\n `\">text` is a decorated object reference\n `underline`, `text`, `text`\nEverything else is literal text: no block syntax, no images, no autolinks, no other HTML. `\\n` is a soft line break. Escape a literal `*`, `` ` ``, `[`, `]`, `~`, `<` or `\\` with a backslash." } } } @@ -1096,7 +1097,7 @@ }, "text": { "type": "string", - "description": "The embed's payload, and the ONLY slot the stored model has for one. LITERAL: no inline markup is read here and none is written (\u00a78.4). What it holds is decided by `processor` \u2014 SOURCE CODE for a renderer (`latex`, the default, `mermaid`, `chart`, `graphviz`, `kroki`, `excalidraw`, `drawio`), the URL to embed for a service processor (every other value), where `url` is accepted as an input alias for this member and nowhere else." + "description": "The embed's payload, read literally with no inline markup. `latex` (the default), `mermaid`, `chart`, `graphviz`, `kroki`, `excalidraw` and `drawio` hold source code; any other holds a URL." } } } @@ -1133,7 +1134,7 @@ "properties": { "url": { "type": "string", - "description": "INPUT ALIAS for `text`, and only on a SERVICE processor (\u00a75.2). Export never writes it \u2014 it writes `text` \u2014 and the two are ONE stored slot, so a block stating both is refused rather than having one of them silently dropped. A RENDERER processor (`latex`, `mermaid`, `chart`, `graphviz`, `kroki`, `excalidraw`, `drawio`) refuses this member outright, and so does an absent `processor`, which means `latex`: their payload is source code, the model has nowhere to put a second string, and a document written that way lost its diagram on import." + "description": "Input alias for `text`, accepted only on a service `processor`. Stating both is refused. A renderer processor, or an absent `processor`, refuses this member. Export always writes `text`." } }, "not": { @@ -1165,7 +1166,7 @@ "properties": { "object_id": { "type": "string", - "description": "The block's target object, as a reference: the whole string is the id. See $defs/objectRef \u2014 a reference carries no caption or name after it, so nothing here is ever split." + "description": "The block's target object as a reference: the whole string is the id. See $defs/objectRef. A reference carries no caption or name after it, so nothing here is split." }, "name": { "type": "string" @@ -1188,7 +1189,7 @@ "added_at": { "type": "string", "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9]))?$", - "description": "When the file was added, an RFC 3339 timestamp \u2014 the same grammar a `date` property value carries (\u00a73). Export always writes the full UTC form, `2026-07-06T15:04:05Z`; input also takes a non-UTC offset (converted to UTC), fractional seconds (truncated to whole seconds) and a bare date, `2026-07-06`, which means UTC midnight. The stored field is a UNIX SECOND, not a string, so a value this pattern admits but the CALENDAR does not \u2014 `2026-02-30T12:00:00Z` \u2014 is refused by the reader's semantic pass rather than accepted: it has no number to become, and it used to import as zero and be gone from the next export with nothing said. The year is four digits because that is the whole range a unix second can be written back out in (\u00a73)." + "description": "When the file was added, as an RFC 3339 timestamp. Export writes full UTC (`2026-07-06T15:04:05Z`); input also takes an offset, fractional seconds or a bare date. A date no calendar has is refused." }, "hash": { "type": "string" @@ -1214,7 +1215,7 @@ }, "object_id": { "type": "string", - "description": "The block's target object, as a reference: the whole string is the id. See $defs/objectRef \u2014 a reference carries no caption or name after it, so nothing here is ever split." + "description": "The block's target object as a reference: the whole string is the id. See $defs/objectRef. A reference carries no caption or name after it, so nothing here is split." } } } @@ -1234,7 +1235,7 @@ "properties": { "object_id": { "type": "string", - "description": "The block's target object, as a reference: the whole string is the id. See $defs/objectRef \u2014 a reference carries no caption or name after it, so nothing here is ever split." + "description": "The block's target object as a reference: the whole string is the id. See $defs/objectRef. A reference carries no caption or name after it, so nothing here is split." }, "card_style": { "$ref": "#/$defs/cardStyle" @@ -1296,10 +1297,10 @@ "then": { "properties": { "fields": { - "description": "A `row` and a `column` carry no attributes of their own — their children are the content — with one exception, and it lives in the output-only bag: a layout column's width. That one fact is why these two types have a branch at all.", + "description": "A `row` and a `column` carry no attributes of their own; their children are the content. The one exception lives in the output-only bag: a layout column's width.", "properties": { "width": { - "description": "A layout column's share of its row, as a FRACTION — measured from 0 to just over 1 across 405 corpus columns, where 0 means unset. This is the only place it is written: a TABLE column's `width` is lifted to a first-class prop and is in pixels (§6.1), while a layout column's stays in the verbatim bag. Deliberately left untyped — the bag is written back exactly as stored, so a schema demanding a number here would refuse a document export itself produced (§11)." + "description": "A layout column's share of its row, as a fraction from 0 to just over 1, where 0 means unset. A table column's `width` is a first-class property in pixels instead." } } } @@ -1324,7 +1325,7 @@ "minLength": 1, "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$", - "description": "The property this block renders inline, by its document-facing spelling (`Due date`) — the same member name, and the same spelling, every other slot that names one property uses (a dataview's `properties[]` entry, a view's `columns[]`/`sorts[]`/`filters[]`). Spelled `key` before v0.41." + "description": "The property this block renders inline, by its document-facing spelling (`Due date`). Every slot spelled `property` names one that way." } }, "required": [ @@ -1347,11 +1348,11 @@ "properties": { "object_id": { "type": "string", - "description": "The object this view takes its records FROM (§6.2). When authoring, set only for an inline dataview on a non-Query, non-Collection host, pointing to a Query or Collection object; omit on their own dataviews and on a type document's own listing. The full reader also accepts stored type-document targets and self-references. Canonical export omits redundant self-targets on Query, Collection and type documents, preserving the selected source through is_collection and removing overridden legacy source (§6.2); import recognizes explicit self-targets when restoring the primary dataview block (§7). A TYPE document supplies the objects of that type. A query object supplies records matching its own `query_source`. A COLLECTION supplies the ids the target lists in its own `collection_items`, in that order. A non-empty target's resolved type decides; `is_collection`, the host's source fields and legacy block `source` do not override it. An unresolved target or source kind is reported, never guessed from populated payloads. An empty string is equivalent to absence and export omits it. With object_id absent or empty, THIS document is the source and the block's remaining source fields select how to read it. A target deleted from the exporting space is written as `_missing_object`; an object merely absent from the bundle retains its id (§9)." + "description": "The object this view takes its records from: a query, collection or type document, or this one. When authoring, set it only on a non-query, non-collection host. Empty means absent." }, "is_collection": { "type": "boolean", - "description": "Selects a curated LIST when this block's `object_id` is absent or empty (§6.2): if true, read THIS document's `collection_items`. Empty or absent membership means an empty collection, even beside a populated `query_source`. If false or absent, use a non-empty legacy block `source` first; otherwise a type document lists its own type's objects, and other hosts use `query_source`. Beside a non-empty object_id this flag decides nothing: the target's resolved type chooses the source. When export omits a redundant self-target, this flag is true for a Collection and omitted for a Query or type listing. Coexisting root source fields are preserved but never merged or used as empty-value fallbacks." + "description": "Selects a curated list when `object_id` is absent or empty. If true, read this document's `collection_items`. Else a non-empty legacy `source`, then a type document's own objects, else `query_source`." }, "source": { "type": "array", @@ -1359,7 +1360,7 @@ "type": "string" }, "x-output-only": true, - "description": "Legacy source of a DETACHED inline query (§6.2). Used only with an absent or empty `object_id` and without `is_collection: true`; otherwise preserve it as inactive stored state, except when canonical self-target omission clears it to keep the selected source unchanged. Entries travel VERBATIM, not as §9 references: an older client's type id `ot-` or property key is neither folded nor expected to name a document in this bundle. Output-only (§4a): generators describe a source through the target document's `query_source` or `collection_items` instead." + "description": "Legacy source of a detached inline query, read only when `object_id` is absent or empty, `is_collection` is not true and it is non-empty. Entries travel verbatim. Output only." }, "properties": { "type": "array", @@ -1396,7 +1397,7 @@ "type": "integer", "minimum": 0, "maximum": 2147483647, - "description": "How many entries the widget shows. The whole int32 range ON PURPOSE, where the bundle index's flat widget caps the same fact at $defs/widgetListingLimit: a widget above that cap is not lifted into the index and travels as a full document, and this block is that fallback — bounding it here would invalidate the very document the refusal produces." + "description": "How many entries the widget shows, 0 to int32 max. The whole range is allowed here, because this block is the fallback for a widget above the flat index cap at $defs/widgetListingLimit." }, "view_id": { "type": "string" @@ -1437,7 +1438,7 @@ "header": { "type": "string", "x-output-only": true, - "description": "The header row's cell text for this column — an API-read annotation linking the header word to the column id a table edit addresses. Derived from the header row on every read; ignored on input." + "description": "The header row's cell text for this column, linking the header word to the column id a table edit addresses. Written only by an API read that asks for it; ignored on input." }, "width": { "type": "number" @@ -1512,7 +1513,7 @@ "properties", "map" ], - "description": "A property format, by NAME (§3): the public REST API vocabulary, plus names for the internal formats a store really carries — emoji, objects, properties, and map, which 72 production property documents hold (the bundled templatePlaceholders property). One list, referenced from every slot that states a format, so the three slots cannot drift apart. Includes `map`, which only a property document's own property_settings may state: it is the format of hidden system properties (templatePlaceholders is the only carrier — 72 production documents) and no authored property may declare it, so `type_settings.property_definitions[]` and a dataview's `properties[]` reference authorableFormat instead." + "description": "A property format by name: the public API vocabulary plus the internal formats a store carries (emoji, objects, properties, map). An authorable slot may not state `map`." }, "authorableFormat": { "enum": [ @@ -1530,21 +1531,21 @@ "objects", "properties" ], - "description": "A property format an author may DECLARE (§3) — propertyFormat without `map`. `map` names the shape of a hidden system property's value, never something a type or a view declares: it occurs on 0 of 19,862 `type_settings.property_definitions` entries and 0 of 28,034 dataview property entries in a 38,061-document corpus, and its only carrier is the bundled, hidden templatePlaceholders. Splitting the vocabulary keeps a property document able to state what it is (§2d) while keeping the authorable slots — the ones a model writes, and the ones a generated per-type schema is built from — unable to invent one." + "description": "A property format you can declare on a type or a view. The full vocabulary adds `map`, which only a property document's own settings may state." }, "propertyDefinition": { "type": "object", - "description": "ONE property, described in one shape, wherever this format describes a property (§2e). Exactly three homes reference it — a dictionary entry, a type document's property-definition list, and a property document's property_settings — and no fourth spelling of the concept may exist: a property document and a type's property list once described the same property in two vocabularies, and the raw one was a measured live trap (§15 #14). The shape states no `required` of its own and stays open: each home adds its own requirements, narrows what it must (an authored home may not declare `map`), refuses the members another surface already owns, and closes itself with unevaluatedProperties. The property's identity splits into two members on purpose: `property` is the document-facing spelling (a label, translated like every key slot), `internal_key` the stored id the app mints (export fidelity, never required from an author) — the word `key` used to carry both meanings and no longer exists in this shape.", + "description": "One property, in the one shape this format uses wherever it describes a property. `property` is the document-facing spelling, `internal_key` the stored id; each home adds its own requirements.", "properties": { "property": { - "description": "The property's document-facing SPELLING (§3) — `Due date`, the same display-name spelling every other key slot writes, inverted through the document's own legend and the vocabulary in force. A spelling, never a stored id: the stored id is `internal_key`.", + "description": "The property's document-facing spelling, such as `Due date`. A spelling, never a stored id; the stored id is `internal_key`.", "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$" }, "internal_key": { - "description": "The property's STORED internal key, written by export for fidelity: the bson id the app minted for a custom property (`6a83296f61fab2265263ae34`), the camelCase key of a bundled one (`dueDate`). An author does not write this — an author cannot produce a correct one, and does not need to: identity is `property`, or `internal_key`, or a `name` the spelling derives from, and when a custom property states no internal key the import wiring MINTS a fresh one, exactly as the app does when a user creates a property. Taken verbatim on the way in — a stored key is always its own address (§3).", + "description": "The property's stored internal key: a bson id for a custom property (`6a83296f61fab2265263ae34`) or the camelCase key of a bundled one (`dueDate`). An author omits it; a spelling resolves or mints it.", "type": "string", "minLength": 1, "maxLength": 128, @@ -1571,7 +1572,7 @@ "type": "string", "minLength": 1 }, - "description": "objects/files only: the types the property may point at, in priority order, each written as the type's derived id `type-` (§9). A display name (a bundled one, or the Name of a type this bundle declares) or the legacy `ot-` is accepted on input; canonical export writes the derived id — except in a single document exported under the `NoDerivedTypeIds` mode (§9, a mode a bundle refuses), where this slot carries the VOCABULARY spelling instead. Empty means any object." + "description": "objects and files only: the types the property may point at, in priority order, each as the derived id `type-`. A display name or the legacy `ot-` is accepted on input." }, "description": { "type": "string" @@ -1595,7 +1596,7 @@ }, "typeProperty": { "type": "object", - "description": "One entry of a type document's property-definition list (§2a): a propertyDefinition plus `uninstalled` and `section`. `section` is the ONE field that belongs to the type rather than the property — of 1,651 (bundle, key) pairs declared by 2+ types within one space (at most 33 in any one space), exactly one differs in anything else — and that one differs only in its `property` spelling, never in what the property IS. The shared members are referenced, never restated; this layer only narrows: `format` to the authorable vocabulary, `object_types` to a real list (a type declares targets or omits the member — only a property document's stored value can hold a null). An entry must identify the property it declares: state a `property` (the document-facing spelling), or an `internal_key` (the stored id), or a `name` the spelling is derived from. A NAME ALONE IS ENOUGH — `{\"name\": \"Cooking Time\", \"format\": \"number\"}` declares a property spelled `Cooking Time`: the name IS the spelling. Prefer `property` or `name` when authoring: `internal_key` is an id the app mints (every exported example carries one like `6a83296f61fab2265263ae34`, because export writes the keys a real space holds), an author has no space to draw one from, and inventing one is never right — when a custom property states none, the import wiring mints a fresh internal key exactly as the app does when a user creates one. A spelling runs through the same resolution however it is stated, so a property named \"Due Date\" folds onto the bundled `dueDate` instead of minting a lookalike beside it.", + "description": "One entry of a type document's property list: a property definition plus `uninstalled` and `section`. Identify the property with `property`, `internal_key`, or a `name` to derive the spelling from.", "allOf": [ { "$ref": "#/$defs/propertyDefinition" @@ -1622,7 +1623,7 @@ "name": { "minLength": 1, "pattern": "^[^\u0000-\u001f]+$", - "description": "When name supplies the property identity, readers NFC-normalize it before applying the 128-character writable-key bound. Standard JSON Schema cannot count characters after normalization, so this schema enforces the expressible non-empty/control-free part and semantic readers enforce the normalized maximum." + "description": "When `name` supplies the property identity, it is NFC-normalized before the 128-character key bound applies. This schema checks the non-empty, control-free part; readers enforce the bound." } } } @@ -1637,7 +1638,7 @@ }, "uninstalled": { "type": "boolean", - "description": "The user REMOVED this property from the space (\u00a72f, \u00a715 #22). The object stays and is hidden \u2014 uninstalling a custom property is the same act as uninstalling a bundled one \u2014 so there is no \"deleted\" member beside this one. Export writes `true` only; absent is the same statement as `false`, and an author never writes it: the authoring subset refuses it, because an author declaring a property has nothing to uninstall. It is here as well as on a dictionary entry because THIS entry is a complete standalone definition (\u00a72e): a reader that opens one type document and builds its property list from it would otherwise build a removed property as a live one. What a reader MUST NOT do, in either home, is install it as a live property. The dictionary entry's other three owned members do not travel here \u2014 `hidden` is the store's own listing bit, `api_key` the property's public address, `bundled_diverged` a verdict about a space a reader never saw \u2014 because a type's declaration says how THAT type uses a property and none of those is a thing it says. The shape's third home refuses this member as it refuses those: a property document's `property_settings` mirrors stored presence exactly, and the removal is not one of the members that travel there." + "description": "The user removed this property from the space. The object stays and is hidden. Export writes `true` only; absent means the same as `false`. An author never writes it, and a reader never installs it." }, "section": { "enum": [ @@ -1685,12 +1686,12 @@ "internal_key": { "type": "string", "minLength": 1, - "description": "The option's stored key — a minted id, so an AUTHOR never writes one and export writes it only where it exists. It is minted rather than derived: the name and colour say what the option MEANS and the array position says where it sits, but nothing about an option implies its stored key. Carrying it is what lets a bundle state a vocabulary completely rather than only describe it." + "description": "The option's stored key, a minted id. An author never writes one, and export writes it only where it exists." }, "api_key": { "type": "string", "minLength": 1, - "description": "The option's public API key (stored `apiObjectKey`) — the spelling callers address the option by, and NOT a slug of the name: an api key does not follow a rename and nothing rewrites it, so `Canceled` keeps the api key `cancelled` it was minted with and `Product` keeps `produc`. Export writes it where the store holds one; an author never writes one. It travels because no restore mints it — the app's rule that derives an api key from a name lives on the create path, and import does not take that path (relation and relation-option snapshots are written straight into their trees), so an option restored from a bundle that states none gets none at all and the API addresses it by a hash-derived local key instead. The same reasoning gives a TYPE its `type_settings.api_key`." + "description": "The option's public API key, the spelling callers address it by. Not a slug of the name: it does not follow a rename. Export writes it where the store holds one; an author never writes one." } }, "required": [ @@ -1716,14 +1717,14 @@ }, "dataviewProperty": { "type": "object", - "description": "One entry of a dataview's `properties[]`: a property available to the block's views, with its format (§6.2). Declares the property; the views then refer to it — a column, sort or filter naming a property this list does not carry is still legal (the §3 chain answers its format).", + "description": "One entry of a dataview's `properties[]`: a property available to the block's views, with its format. A column, sort or filter may name a property this list does not carry.", "properties": { "property": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$", - "description": "The property this entry declares, by its document-facing spelling (`Due date`) — the same member name, and the same spelling, the view's `columns[]`, `sorts[]` and `filters[]` beside it use to refer to it. Spelled `key` before v0.41, twelve lines from siblings that spelled `property`, and each was a schema error in the other's position." + "description": "The property this entry declares, by its document-facing spelling (`Due date`). The view's `columns[]`, `sorts[]` and `filters[]` refer to it by that same spelling." }, "format": { "$ref": "#/$defs/authorableFormat" @@ -1980,7 +1981,7 @@ "minLength": 1, "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$", - "description": "The property this column shows, by its document-facing spelling — the same member name, and the same spelling, the sorts, filters and the dataview's `properties[]` entries use." + "description": "The property this column shows, by its document-facing spelling. The sorts, filters and the dataview's `properties[]` entries use that same spelling." }, "hidden": { "type": "boolean" @@ -2077,7 +2078,7 @@ "todo", "video" ], - "description": "The closed set of object layouts, by NAME (§3). Validate refuses a name outside this set: a typo would import as a raw string onto a number-format detail, where every consumer reads it with an integer getter and silently sees `basic`. A raw NUMBER is accepted beside a name, because a stored value outside the vocabulary round-trips as its number rather than being lost." + "description": "The closed set of object layouts, by name. A name outside this set is refused; a raw number is accepted beside a name, so a stored value outside the vocabulary round-trips as its number." }, "viewType": { "enum": [ @@ -2088,7 +2089,7 @@ "calendar", "graph" ], - "description": "How a dataview view renders. One definition, shared by a view's own `type` and a type's `default_view`, so the two cannot drift into different vocabularies for one concept." + "description": "How a dataview view renders. Shared by a view's own `type` and a type's `default_view`, so the two cannot drift apart." }, "blockAlign": { "enum": [ @@ -2097,7 +2098,7 @@ "right", "justify" ], - "description": "Horizontal alignment, by NAME. One definition for one concept: a block's `align`, a view column's `align`, and the `layout_align` property value (§3) all draw from it. The property slot cannot $ref this definition — a property SPELLING is not fixed to its stored key, so the vocabulary there is enforced by the semantic pass on the RESOLVED key — but the vocabulary is this one: an unknown name on `layout_align` is a validation error, and a raw NUMBER is accepted beside a name, because a stored value outside the vocabulary round-trips as its number rather than being lost." + "description": "Horizontal alignment by name, shared by a block's `align`, a view column's `align` and the `layout_align` property value. An unknown name is rejected; only the property value also takes a raw number." }, "objectOrigin": { "enum": [ @@ -2112,7 +2113,7 @@ "usecase", "webclipper" ], - "description": "How an object entered its space, by NAME — the vocabulary of the `origin` property value (§3). No slot $refs this definition, because a property SPELLING is not fixed to its stored key (a legend may rebind it): the vocabulary is enforced by the semantic pass on the RESOLVED key, and stated here so a reader of the schema can learn it. An unknown name is a validation error; a raw NUMBER is accepted beside a name, because a stored value outside the vocabulary round-trips as its number rather than being lost." + "description": "How an object entered its space, by name: the vocabulary of the `origin` property value. An unknown name is refused, and so is a number this set can name; only an unnamed number passes." }, "importType": { "enum": [ @@ -2125,7 +2126,7 @@ "pb", "txt" ], - "description": "Which importer created an import- or usecase-originated object, by NAME — the vocabulary of the `import_type` property value (§3), enforced like $defs/objectOrigin by the semantic pass on the resolved key. Named or refused, never stored as a stray string: the underlying enum's ZERO is notion, so an unchecked string here used to read back as a false claim that the object came from Notion." + "description": "Which importer created an import- or usecase-originated object, by name: the vocabulary of the `import_type` property value. A name outside this set is refused." }, "imageKind": { "enum": [ @@ -2134,7 +2135,7 @@ "cover", "icon" ], - "description": "What an image was uploaded FOR, by NAME — the vocabulary of the `image_kind` property value (§3), enforced like $defs/objectOrigin by the semantic pass on the resolved key. Set on file objects. `automatically_added` marks an image a pipeline brought in rather than a person, and travels beside `is_hidden_discovery`, which is what a client actually filters on." + "description": "What an image was uploaded for, by name: the vocabulary of the `image_kind` property value, set on file objects." }, "participantPermissions": { "enum": [ @@ -2144,7 +2145,7 @@ "reader", "writer" ], - "description": "What a space member may DO, by NAME — the vocabulary of the `participant_permissions` property value (§3), enforced like $defs/objectOrigin by the semantic pass on the resolved key. The names are the proto's own identifiers snake_cased, NOT the public REST API's `role` (viewer/editor/admin): that mapping is not injective over the stored enum — its inverse sends every name outside three back to Reader, `owner` included — and a name that does not round-trip to the number it came from is not a name this format can write. The enum's ZERO is `reader`, so a string this list does not name is refused rather than stored: it used to land verbatim on a number detail, where every int getter answered 0 and a mistyped owner read as a viewer." + "description": "What a space member may do, by name: the vocabulary of the `participant_permissions` property value. These are the stored identifiers, not the API's viewer, editor and admin roles." }, "participantStatus": { "enum": [ @@ -2155,7 +2156,7 @@ "removed", "removing" ], - "description": "Where a space member is in joining or leaving the space, by NAME — the vocabulary of the `participant_status` property value (§3), enforced like $defs/objectOrigin by the semantic pass on the resolved key. `canceled` is the proto's spelling and stays one word: one concept, one spelling (§15 #14). `joining` occurs in no bundle of the 79-bundle corpus and is published anyway — a vocabulary with a hole in it exports a bare integer the day something writes into the hole." + "description": "Where a space member is in joining or leaving the space, by name: the vocabulary of the `participant_status` property value." } } } diff --git a/format/v2/schema/properties.schema.json b/format/v2/schema/properties.schema.json index b0e371f..95227ae 100644 --- a/format/v2/schema/properties.schema.json +++ b/format/v2/schema/properties.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.anytype.io/anyblock/2.0/properties.schema.json", "title": "AnyBlock JSON property dictionary", - "description": "One properties.json at the root of a bundle names every property the bundle's objects use — the dictionary (§2f). It is a sibling of index.json, not a section inside it: an index says where things are, a dictionary says what they mean. A bundle carries no property document at all (§15 #23): this file is where every property something references is stated — replacing the ~9,500 documents per account (kind \"property\") that restated the bundled table field for field (98% of installed copies are identical to it), and the rest besides — and it is what lets a third-party reader interpret a backup WITHOUT shipping the bundled table: every entry carries its format, so a date is a date and an option name is an option name even to a reader that has never seen Anytype.", + "description": "One properties.json at the root of a bundle names every property the bundle's objects use. A sibling of index.json: an index says where things are, a dictionary says what they mean. Every entry carries its own format and states the complete definition, bundled key or not, so a reader can interpret a backup without shipping the bundled table.", "type": "object", "required": [ "formatVersion" @@ -11,7 +11,7 @@ "properties": { "$schema": { "type": "string", - "description": "Which of the format's three grammars this document follows — an object document, a bundle index, or a property dictionary. OPTIONAL and DECORATIVE for validity: `formatVersion` alone gates the format, so a stale or invented URL here never makes a document invalid. What it does do is DISPATCH: a reader handed a file with no filename to go by uses this to pick the grammar, matching on the trailing `object|index|properties.schema.json` and ignoring the version segment. Without it a reader falls back to the document's shape, which can only tell the grammars apart when the document happens to carry a member unique to one. Every document this format writes carries it, and a hand-written one is easier for a reader to place if it does too." + "description": "Which of the format's three grammars this document follows. Optional, since `formatVersion` gates the version, but a reader dispatches on it and a URL naming another grammar is refused." }, "formatVersion": { "const": "2.0" @@ -21,15 +21,15 @@ "items": { "$ref": "#/$defs/dictionaryEntry" }, - "description": "One propertyDefinition per property the bundle's objects actually REFERENCE — used-only, not everything the space holds: a space installs a median 125 bundled properties and uses 57, and the 68 nothing touches buy a reader nothing (the restore reinstalls the bundled table anyway). A property nothing references is not exported at all — no document (a bundle carries none, §15 #23), no entry: nothing names the key, so there is no value to explain and no format to look up. This is the dictionary's ONE list (§15 #24): a bundled property the bundle carries is an entry here like any other, and every entry states the COMPLETE definition — name, format, description, object types, include-time on a date, max count on a format that can hold more than one value, readonly, default value, options, the entry's own flags — whatever the property has and its format leaves room for, whether the key is bundled or space-minted, whether the space's copy matched the shipped table or not (§15 #25). There is no reduced entry that leaves a reader to fill the rest from its own table, and no reader needs the table to interpret the file. A reader tells a bundled key from a space-minted one by looking the stored key up in its own shipped table, the lookup every §3 slot runs, never by a flag on the entry; `bundled_diverged` is not that flag — it says a bundled key's copy had DIVERGED from the table at export time, and its absence says nothing about bundled-ness. Keys are CANONICAL SPELLINGS: the display name from the shipped table for a bundled key, the stored key verbatim for a space-minted one — a pure function of the key, because this file carries no legend. The reader flow is object → spelling → (1) the document's property_internal_keys legend, (2) a verbatim match against a dictionary key, (3) the shipped name table over the dictionary's own keys, with the forgiving fold behind it for near-misses and pre-v0.48 derived-slug spellings. Step 3 carries most value slots, because §3 writes a legend line only for a spelling the bundled table does not bind. Look up, never transform: the name and the key say different words (`Creation date` / `createdDate`). No folder convention, no scanning (§2f)." + "description": "One property definition per property the bundle's objects reference. Each entry's `property` holds the canonical spelling: the bundled table's display name for a bundled key, the stored key otherwise." } }, "$defs": { "dictionaryEntry": { - "description": "One dictionary entry: a propertyDefinition (§2e), referenced rather than restated. Its `property` is the entry's spelling: the display name for a bundled property (`Due date`), the stored key verbatim for a space-minted one — a bson id like `6a32d4856761631534b22f85`, from which nothing must ever be derived. Its `internal_key` is the stored key itself, written by export for fidelity and never required from an author. This is the third of the shape's three homes, beside a type document's property-definition entry and a property document's property_settings — and the only one a bundle this format's composer writes (§15 #23), which is why its owned members — `hidden`, `bundled_diverged`, `api_key` and, shared with a type's declaration, `uninstalled` — carry what a property document used to: the three flags only the export could know, and the stored api key no restore re-derives. Three of the four are the entry's alone; `uninstalled` is stated by a type's `property_definitions` entry as well, because that entry is a complete standalone definition too and a definition that presents a removed property as live is not complete. The layer narrows `object_types` to a real array (only a property document's STORED value can hold a null; a dictionary describes, it does not mirror a store slot) and requires `format`, because self-sufficiency is the constraint that shapes the dictionary: an entry without a format is readable only by a reader shipping the same bundled vocabulary, which is exactly the dependence the dictionary exists to end. An entry must identify the property it declares: state a `property` (the spelling), or an `internal_key` (the stored id), or a `name` the spelling is derived from. A NAME ALONE IS ENOUGH — `{\"name\": \"Cooking Time\", \"format\": \"number\"}` declares a property spelled `Cooking Time`. Prefer `property` or `name` when authoring: `internal_key` is an id the app mints, an author has no space to draw one from, and inventing one is never right — when a custom property states none, the import wiring mints a fresh internal key exactly as the app does when a user creates one. A derived spelling runs through the same resolution as a written one, so a property named \"Due Date\" folds onto the bundled `dueDate` instead of minting a lookalike beside it.\n\nCARDINALITY IS NOT A MEMBER of this entry, and does not need to be. On a format that holds a LIST — objects, files, select, multi_select, properties — a value may be written as the bare value or as a one-element array holding it, and the two are THE SAME VALUE: both store one list and both re-export as the array, so the shape a document happens to use carries nothing a reader can get wrong. The equivalence runs in that direction only: on a single-valued format an array is not unwrapped, so `[\"hi\"]` on a text property is a list of strings and stays one. `max_count` is the only thing the entry says about how many values fit, and it is written only where the format leaves room for more than one.", + "description": "A property definition plus `hidden`, `api_key`, `bundled_diverged`, `value_names` and `uninstalled`; `format` required. Identify with `property`, `internal_key` or, off an `unknown` entry, `name`.", "allOf": [ { - "description": "Which shape this entry has. An entry is a propertyDefinition (§2e) — the shared shape, referenced across files by its published URL — UNLESS its format is the sentinel `unknown`, which says no definition could be found for the key at all. The two are not narrowings of one another: a definition states what a property holds, and `unknown` states that nothing states it, so the `unknown` branch REPLACES the shared shape instead of layering over it.", + "description": "Which shape this entry has: a property definition, unless `format` is the sentinel `unknown`, in which case the entry states identity only and nothing else.", "if": { "properties": { "format": { @@ -69,7 +69,7 @@ "name": { "minLength": 1, "pattern": "^[^\u0000-\u001f]+$", - "description": "When name supplies the property identity, readers NFC-normalize it before applying the 128-character writable-key bound. Standard JSON Schema cannot count characters after normalization, so this schema enforces the expressible non-empty/control-free part and semantic readers enforce the normalized maximum." + "description": "When `name` supplies the property identity, it is NFC-normalized before the 128-character key bound applies. This schema checks the non-empty, control-free part; readers enforce the bound." } } } @@ -78,7 +78,7 @@ "properties": { "object_types": { "type": "array", - "description": "objects/files only: the types the property may point at, in priority order, each written as the type's derived id `type-` (§9); a display name or the legacy `ot-` is accepted on input." + "description": "objects and files only: the types the property may point at, in priority order, each as the derived id `type-`. A display name or the legacy `ot-` is accepted on input." }, "value_names": { "type": "array", @@ -88,23 +88,23 @@ "type": "string", "minLength": 1 }, - "description": "The complete set of names a value of this property can be — the entry's answer to the one question a bundle could not otherwise answer about its own bytes. Written for the properties whose stored value is a NUMBER whose meaning is an app enum and whose exported value is that enum's NAME (§3): `layout`, `resolvedLayout`, `layoutAlign`, `origin`, `importType`, `imageKind`, `participantPermissions`, `participantStatus`, and `recommendedLayout`, which a type document carries as `type_settings.layout`. Written only where the entry itself states `\"format\": \"number\"` — the encoder's table is keyed on the stored key, while an entry states the format the SPACE holds, and a copy that has diverged from the bundled table could otherwise publish a number's names beside a format that holds no numbers. Sorted, complete, and DERIVED from the encoder's own table, so the published list cannot say something export has stopped writing.\n\nIt exists because `format` alone is a lie of omission on exactly these keys. The entry says `\"format\": \"number\"` — the stored format, correctly — while every value in the bundle is a string: across a 79-bundle corpus all 62,325 values in the six object-carried slots of the seven keys named first are strings and not one is a number. (The participant pair was named after that corpus was taken, and its 5,038 slots are bare integers in every bundle of it — the last of these gaps to close, and the largest.) A reader holding only the bundle could not learn the members from anywhere: object.schema.json publishes enum vocabularies for the slots that constrain them, not for a property value, and `$defs/propertyMap` accepts any value at all. So the reader guesses, and the entry's own `description` tells it to guess WRONG: `layout`'s reads \"Anytype layout ID(from pb enum)\", which is the store's own text describing the STORED number, and a reader that believes it writes `\"Layout\": 1`; `participantPermissions`' reads \"Possible values: models.ParticipantPermissions\", a pointer to a Go symbol in a repository the bundle does not ship. That write is refused now, naming the value 1 stands for — but the refusal is the second line of defence and this member is the first. Read `format` with `value_names`, never `description`: a description is free text a user may have edited, while this list is the encoder's.\n\nREAD-facing, and not an authoring surface. Export writes it; an author never does, and one written by hand is answered with a warning rather than obeyed — the format enforces no vocabulary an entry invents, and it does not take the entry's word for one it owns. All nine of these keys are hidden, readonly or both in the shipped table, so the member states what a value MEANS, never what a caller may choose. These keys are single-valued, so a value is one of these names written bare: an array is not unwrapped on a single-valued format, and `[\"profile\"]` is a different stored value from `\"profile\"` — a list of strings sitting on a number-format key, which nothing reads as a layout.\n\nAbsence says only that THIS entry publishes no vocabulary. Usually that is because the property has none — most properties do not — and on an entry whose `format` is not `number` it is because the names of a number are not that entry's to publish, which a diverged copy of a named key can reach. It never means the writer omitted a list it had." + "description": "The complete, sorted set of names a value of this property can take. Written where `format` is `number` and the number means an app enum: a value is the name, never the number. Export writes it." }, "uninstalled": { "type": "boolean", - "description": "The user REMOVED this property from the space, and the bundle carries it for backup fidelity rather than as a document (§2f, §15 #22). Export writes `true` only; absent is the same statement as `false`, and an author never writes it — there is nothing to uninstall in a bundle that has not been installed. A reader MAY recreate the property with its removal mark set, or skip it: a removed property appears in no listing either way, so both restores look the same, and the entry still says what a value stored under the key means. What a reader MUST NOT do is install it as a live property. The flag is the whole statement of the removal (§15 #24): the dictionary keeps no list of installed keys for it to contradict. Stated by TWO of the shape's three homes, and the only member that is: a type's `property_definitions` entry (object.schema.json) states it as well, because that entry is a COMPLETE standalone definition too (§2e) and a definition presenting a removed property as live is not complete. The THIRD home refuses it — a property document's `property_settings` mirrors stored presence exactly, and a removal is not one of the members that travel there — while this entry refuses the type declaration's own `section` in turn. The entry's four other owned members do not travel to a declaration: `hidden`, `api_key` and `bundled_diverged` state something about the property in the SPACE, which a declaration does not speak for — it says only how ONE type uses the property — and `value_names` is derived from the encoder's own table, so a declaration carrying it would publish a second copy of a vocabulary it does not own." + "description": "The user removed this property from the space; a reader must not present it as in use. Export writes `true` only, never an author. A type's `property_definitions` entry states it too." }, "api_key": { "type": "string", - "description": "The property's public API key (stored `apiObjectKey`) — the spelling callers address it by on the API surface. NOT a slug of the name: an api key does not follow a rename and nothing rewrites it, so `Location` keeps `restaurant_location`. It travels because no restore mints one — the rule that derives an api key lives on the app's create path and an import does not take it — and since §15 #23 a bundle writes no property document, so this entry is the only place the stored value can travel. Export writes it where the store holds one; an author never writes one. A member of the dictionary entry only: a type's declaration says how THAT type uses a property, and the property's public address is not one of the things it says." + "description": "The property's public API key, the spelling callers address it by. Not a slug of the name: it does not follow a rename. Export writes it where the store holds one; an author never writes one." }, "hidden": { "type": "boolean", - "description": "The store HIDES this property from every listing (its stored `isHidden`), and a bundle carries no property document to say so (§2f, §15 #23), so the entry does. Export writes `true` only; absent is the same statement as `false`, and an author never writes it — a property is declared to be used, and hiding one is a store fact the export records, like `internal_key`. A reader that recreates the property sets the mark; a reader that only interprets values ignores it. Distinct from a type declaration's `section`, which says where a property sits on ONE type: `hidden` says whether the property is shown at all. A member of the dictionary entry only, on the same footing as `api_key`, `bundled_diverged` and `value_names`: the shape's other two homes refuse it. NOT on `uninstalled`'s footing — a type's `property_definitions` entry states that one, because a removal is what makes a definition incomplete, while a store-wide listing bit is not something a type's declaration says at all." + "description": "The store hides this property from every listing. Export writes `true` only; absent means the same as `false`. An author never writes it." }, "bundled_diverged": { "type": "boolean", - "description": "The space's copy of a BUNDLED property — a key the shipped table names — had DIVERGED from that table when the bundle was written (§2f, §15 #25): a rename, a changed description or hidden bit, any definition member. The entry states the user's version, complete, like every entry; this member says that version is the user's and not the table's, so a reader restoring the key takes the entry over its own table. It is written because it is NOT derivable: a reader could diff the entry against its table today, but the shipped table moves between app versions — once Anytype renames a bundled property, a later reader cannot tell whether the user renamed it or the table did. The verdict is knowable only at export time, and this records it. Export writes `true` only, and only for a key in the writer's shipped table whose copy the identity check refused; absent means EITHER \"not a bundled property\" OR \"bundled and not diverged\", and the reader separates those with the table lookup it already runs for every key (§15 #24) — this is not a `bundled` flag. Reader behaviour: key in the reader's table and no flag → install the fresh bundled property, accepting any newer name the table has; key in the table with `bundled_diverged: true` → the user's version wins, create or update from the entry; key not in the reader's table → create from the entry, flag or no flag. An author never writes it — there is no space whose copy could have diverged. A member of the dictionary entry only, on the same footing as `hidden`, `api_key` and `value_names`: the shape's other two homes refuse it. `uninstalled` is no longer on that footing — a type's `property_definitions` entry states it, a definition that presents a removed property as live being incomplete — but a verdict about a space's history is not something a type's declaration says." + "description": "The space's copy of a bundled property failed the identity check against the bundled table, so a reader takes this entry over its own. Export writes `true` only, never an author." } }, "required": [ @@ -130,7 +130,7 @@ ] }, "undefinedPropertyEntry": { - "description": "An entry for a property key the bundle's documents REFERENCE and nothing could define — almost always a relation the user deleted, whose definition went with it. In one audited 3,286-document space that is 628 values across 313 documents naming 153 such keys, and their values are uninterpretable on sight: `\"66602dc5e5672d06c0e19245\": 1717538400` could be a date, a count or an id, and nothing in that bundle can tell — it has no entry, no type declaration, no format cached on a dataview column, and one legend line spelling the key as itself. A key a type document DECLARES is not one of these: a declaration states a name and a format, so the writer takes them and this entry is not what it writes (that is 4 keys over a 79-bundle corpus, 2 of them in this space, and it is why the counts here are lower than an earlier measurement of the same export). Without an entry the key resolves to NOTHING in this file, and a reader cannot tell \"the writer had nothing to say\" from \"I failed to look\". This is that distinction, made once: every referenced key resolves to something, and for some of them the something is that there is no answer. How much was lost, and to how many keys, is stated in index.json rather than counted here.\n\n`unknown` is NOT a property format. It is absent from `$defs/propertyFormat`, the one list every format slot in the format references, so a type's `property_definitions` entry, a dataview property and a property document's own `property_settings` all refuse it — a declaration says how a property is USED, and an absent definition is not a use. It is absent from the authoring subset for the same reason from the other side: an author declaring a property always knows what it holds, and this member exists for what an EXPORT found it could not describe.\n\nAn entry that states it states NOTHING ELSE — no name, no description, no options, no target types, no flags, no `value_names`. There is nothing else to state, and that is the whole content of the claim: an entry carrying a format's worth of detail beside `unknown` would be describing a definition it has just said it does not have. Identity is all that remains, and identity is the point — the key, so the reference resolves. A `name` was admitted here once, for \"a legend line, a type's declaration\", and no writer ever wrote one: a legend line binds a spelling to a stored key and carries no name, and a type's declaration states a format beside its name, which makes it a definition and this shape not what the writer reaches for.\n\nA reader MUST NOT create a property from one of these. There is nothing to create: no format, no name the store ever held. What it may do is read the values under the key as the raw JSON they are, and say so — which is exactly what the writer knew.", + "description": "An entry for a property key the bundle's documents reference and nothing could define. Export writes it, never an author. It states identity only, and a reader must not create a property from it.", "type": "object", "required": [ "format" @@ -141,18 +141,18 @@ "minLength": 1, "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$", - "description": "The entry's spelling, as on any entry (§2f): for a key nothing defines this is the stored key verbatim, because a key the bundled table does not name has no other pure spelling and nothing may ever be derived from a bson id." + "description": "The entry's spelling. For a key nothing defines this is the stored key verbatim, since a key the bundled table does not name has no other spelling." }, "internal_key": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^\u0000-\u001f]+$", - "description": "The stored key itself — the thing the documents' values are keyed by, and the reason the entry exists." + "description": "The stored key itself, the key the documents' values are written under." }, "format": { "const": "unknown", - "description": "The sentinel: no definition of this property could be found. Not a format — see this shape's description." + "description": "The sentinel: no definition of this property could be found. Not a format; see this shape's description." } }, "additionalProperties": false diff --git a/format/v2/schema/prose_test.go b/format/v2/schema/prose_test.go new file mode 100644 index 0000000..1fed95f --- /dev/null +++ b/format/v2/schema/prose_test.go @@ -0,0 +1,250 @@ +package schema + +// prose_test.go guards the prose of the published schemas: the descriptions a +// caller outside this repository actually reads, and that a model generating a +// document is steered by. +// +// A description says what a slot is and what is legal in it. It does not say +// why the rule exists, what the format used to do, or what evidence the rule +// was chosen from. That reasoning belongs in a Go comment beside the rule or in +// the format documents, neither of which is shipped to a caller. + +import ( + "encoding/json" + "regexp" + "sort" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// maxDescription bounds every description except a schema's own root one. One +// sentence naming the slot and stating what is legal fits well inside it. +const maxDescription = 200 + +// maxRootDescription bounds the root description, the one slot that states what +// a whole document is for. It is longer by design, not unbounded. +const maxRootDescription = 600 + +// maxComment bounds a $comment: a note on a construction the keywords cannot +// state on their own. It is served like everything else, so it is bounded like +// everything else. +const maxComment = 300 + +// rootPointer is the root description, exempt from maxDescription. +const rootPointer = "/description" + +// maxGrammarDescription bounds a slot that has to teach a syntax a caller +// writes by hand. A vocabulary can be listed in an enum; a grammar cannot, so +// these slots carry the one worked statement of it. +const maxGrammarDescription = 800 + +// grammarSlots are the descriptions exempt from maxDescription because the +// caller cannot produce a valid value without the syntax spelled out. Every +// entry is a grammar, not an explanation: adding one means a caller would +// otherwise have to guess at syntax, never that the prose would not fit. +// Keys are "::", so a pointer that exists in two schemas +// is exempt only in the one that needs it. +var grammarSlots = map[string]bool{ + // The inline markup a text block's `text` carries. The same grammar on + // every text branch, because a caller reads the branch they are writing. + "object.schema.json::/$defs/blockCore/allOf/0/then/properties/text/description": true, + "object.schema.json::/$defs/blockCore/allOf/1/then/properties/text/description": true, + "object.schema.json::/$defs/blockCore/allOf/2/then/properties/text/description": true, + "object.schema.json::/$defs/blockCore/allOf/3/then/properties/text/description": true, + "object.schema.json::/$defs/blockCore/allOf/4/then/properties/text/description": true, + // The same markup grammar, restated for the authoring subset. + "authoring/object.schema.json::/$defs/richText/description": true, + // One value shape per property format. A caller writing a property value + // has to be told the shape; an enum cannot carry a mapping. + "authoring/object.schema.json::/$defs/propertyMap/description": true, +} + +// proseRules reject what means nothing to a caller who cannot read this +// repository, and the emphasis the schemas do not use. +var proseRules = []struct { + name string + pattern *regexp.Regexp + fix string +}{ + { + name: "section mark", + pattern: regexp.MustCompile(`§`), + fix: "state the rule in plain words; the sections are internal documents", + }, + { + name: "spec reference", + pattern: regexp.MustCompile(`\bSPEC\b`), + fix: "state the rule in plain words; a caller cannot open the spec", + }, + { + name: "numbered constraint", + pattern: regexp.MustCompile(`\b[CDI]\d+\b`), + fix: "state the rule in plain words; a constraint number names nothing a caller can look up", + }, + { + name: "internal filename", + pattern: regexp.MustCompile(`[\w/]+\.md\b`), + fix: "a caller cannot open a file in this repository; say what the file says", + }, + { + name: "corpus evidence", + pattern: regexp.MustCompile(`(?i)\bcorpus\b`), + fix: "the evidence behind a rule is not the rule; keep it in a Go comment", + }, + { + // A real limit ("100,000 pairs") is a rule a caller acts on and stays. + // What goes is the framing that turns a measurement into evidence. + name: "sample evidence", + pattern: regexp.MustCompile(`(?i)(\bsweep of\b|\b\d+ occurrences?\b|\b\d+ of \d|\bacross \d|\bpopulated cases\b)`), + fix: "how often something was observed is not a rule; keep it in a Go comment", + }, + { + name: "changelog", + pattern: regexp.MustCompile(`(?i)\b(was removed|was retired|used to|no longer|older exporter|legacy readers?)\b`), + fix: "a description says what is legal now, not what changed", + }, + { + name: "em dash", + pattern: regexp.MustCompile("—"), + fix: "use a full stop; two short sentences beat one qualified sentence", + }, +} + +// allCapsRun finds runs of three or more capitals: emphasis by shouting. +var allCapsRun = regexp.MustCompile(`\b[A-Z]{3,}\b`) + +// allowedAllCaps are the all-caps tokens that are spellings rather than +// emphasis. Keeping the set this small is the point: it leaves no room for a +// NOT or a SINGLE to come back in. +var allowedAllCaps = map[string]bool{ + "JSON": true, "URL": true, "URI": true, "UTF": true, "API": true, + "NFC": true, "CID": true, "DAG": true, "HTML": true, "UTC": true, "RFC": true, + "ISO": true, "SVG": true, "PNG": true, "GIF": true, "JPEG": true, + "EBNF": true, "GET": true, "POST": true, "PATCH": true, "DELETE": true, +} + +// proseEntry is one description, addressed by JSON pointer so a failure names +// the slot to edit. +type proseEntry struct { + pointer string + text string +} + +// collectProse walks a schema and returns every description and $comment. +// $comment is served in the same bytes as the rest of the schema, so it reaches +// a caller and costs them the same tokens, whatever its name suggests. Values +// under example, examples, default, const and enum are skipped: those are data +// a caller sends, not prose a caller reads. +func collectProse(t *testing.T, doc []byte) []proseEntry { + t.Helper() + + var root any + require.NoError(t, json.Unmarshal(doc, &root)) + + var entries []proseEntry + var walk func(node any, pointer string) + walk = func(node any, pointer string) { + switch n := node.(type) { + case map[string]any: + keys := make([]string, 0, len(n)) + for k := range n { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + if k == "example" || k == "examples" || k == "default" || k == "const" || k == "enum" { + continue + } + child := pointer + "/" + k + if text, ok := n[k].(string); ok && (k == "description" || k == "$comment") { + entries = append(entries, proseEntry{pointer: child, text: text}) + continue + } + walk(n[k], child) + } + case []any: + for i, v := range n { + walk(v, pointer+"/"+strconv.Itoa(i)) + } + } + } + walk(root, "") + + require.NotEmpty(t, entries, "this schema carries no prose at all") + return entries +} + +// publishedSchemas is every schema this package hands out. +func publishedSchemas() map[string][]byte { + return map[string][]byte{ + "object.schema.json": Object(), + "index.schema.json": Index(), + "properties.schema.json": Properties(), + "file-remote.v1.schema.json": FileRemoteV1(), + "authoring/object.schema.json": AuthoringObject(), + "authoring/index.schema.json": AuthoringIndex(), + "authoring/properties.schema.json": AuthoringProperties(), + } +} + +func TestPublishedProse(t *testing.T) { + names := make([]string, 0, len(publishedSchemas())) + for name := range publishedSchemas() { + names = append(names, name) + } + sort.Strings(names) + + for _, name := range names { + doc := publishedSchemas()[name] + + t.Run(name, func(t *testing.T) { + entries := collectProse(t, doc) + + t.Run("a description is one sentence long, not an essay", func(t *testing.T) { + for _, e := range entries { + limit := maxDescription + switch { + case e.pointer == rootPointer: + limit = maxRootDescription + case grammarSlots[name+"::"+e.pointer]: + limit = maxGrammarDescription + case strings.HasSuffix(e.pointer, "/$comment"): + limit = maxComment + } + assert.LessOrEqualf(t, len(e.text), limit, + "%s is %d characters, over the %d the slot allows\n %s\n a description names the slot and states what is legal; the reasoning belongs in a Go comment", + e.pointer, len(e.text), limit, e.text) + } + }) + + t.Run("a description carries nothing only this repository can resolve", func(t *testing.T) { + for _, e := range entries { + for _, rule := range proseRules { + if found := rule.pattern.FindString(e.text); found != "" { + assert.Failf(t, "prose rule violated", + "%s carries %s %q\n %s\n %s", + e.pointer, rule.name, found, e.text, rule.fix) + } + } + } + }) + + t.Run("a description does not shout", func(t *testing.T) { + for _, e := range entries { + for _, run := range allCapsRun.FindAllString(e.text, -1) { + if allowedAllCaps[run] { + continue + } + assert.Failf(t, "emphasis by shouting", + "%s shouts %q\n %s\n put the important word first in the sentence instead", + e.pointer, run, e.text) + } + } + }) + }) + } +} diff --git a/internal/prose/userfacing_test.go b/internal/prose/userfacing_test.go new file mode 100644 index 0000000..ecd32ed --- /dev/null +++ b/internal/prose/userfacing_test.go @@ -0,0 +1,103 @@ +package prose + +// userfacing_test.go guards the strings this repository hands a caller: the +// validation errors, warnings and hints a document's author reads when their +// document is refused. +// +// The published schemas are held to the same rule by +// format/v2/schema/prose_test.go. This is the other half of the same surface. +// A message names what is wrong and what to write instead. It does not cite +// the section that states the rule, the sweep that found it, or the mode that +// produced it — a reader outside this repository has none of those. +// +// Comments are exempt and deliberately so: the reasoning behind a rule is +// worth keeping, it just belongs next to the code rather than on the wire. +// This test reads string literals only, so moving a citation out of a message +// and into the comment above it is the fix. + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// sourceRoots are the packages whose strings can reach a caller. +var sourceRoots = []string{"codec", "bundle", "cmd", "format"} + +var messageRules = []struct { + name string + pattern *regexp.Regexp + fix string +}{ + { + name: "section mark", + pattern: regexp.MustCompile(`§`), + fix: "state the rule in plain words; the sections are internal documents", + }, + { + name: "spec reference", + pattern: regexp.MustCompile(`\bSPEC\b`), + fix: "state the rule in plain words; a caller cannot open the spec", + }, + { + name: "internal filename", + pattern: regexp.MustCompile(`\b[\w/]+\.md\b`), + fix: "a caller cannot open a file in this repository; say what the file says", + }, + { + name: "corpus evidence", + pattern: regexp.MustCompile(`(?i)\bcorpus\b`), + fix: "the evidence behind a rule is not the rule; keep it in a comment", + }, + { + name: "sample evidence", + pattern: regexp.MustCompile(`(?i)(\bsweep of\b|\b\d+ occurrences?\b|\b\d+ of \d{2,}|\baudited \d|\bmeasured \d)`), + fix: "how often something was observed is not a rule; keep it in a comment", + }, +} + +func TestMessagesCarryNothingOnlyThisRepositoryCanResolve(t *testing.T) { + root, err := filepath.Abs("../..") + require.NoError(t, err) + + var checked int + for _, dir := range sourceRoots { + err := filepath.Walk(filepath.Join(root, dir), func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") || + strings.HasSuffix(path, "_test.go") { + return err + } + fset := token.NewFileSet() + file, perr := parser.ParseFile(fset, path, nil, 0) + if perr != nil { + return perr + } + rel, _ := filepath.Rel(root, path) + ast.Inspect(file, func(n ast.Node) bool { + lit, ok := n.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return true + } + checked++ + for _, rule := range messageRules { + if found := rule.pattern.FindString(lit.Value); found != "" { + t.Errorf("%s:%d carries %s %q in a string a caller can read\n %s\n %s", + rel, fset.Position(lit.Pos()).Line, rule.name, found, + strings.TrimSpace(lit.Value), rule.fix) + } + } + return true + }) + return nil + }) + require.NoError(t, err) + } + require.NotZero(t, checked, "no string literals were read at all") +}