Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bundle/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-<internal_key>, 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-<key> 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 "+
Expand Down
2 changes: 1 addition & 1 deletion bundle/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions bundle/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion codec/anyblockjson/authoring.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
58 changes: 29 additions & 29 deletions codec/anyblockjson/dataviewsource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
})
Expand Down
14 changes: 1 addition & 13 deletions codec/anyblockjson/documentkind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion codec/anyblockjson/documentkind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
Expand Down
49 changes: 31 additions & 18 deletions codec/anyblockjson/fieldsbag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions codec/anyblockjson/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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-<internal_key>, §9)",
"derived id instead, type-<internal_key>",
slug, len(cands)))
return slug
}
Expand Down Expand Up @@ -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{{
Expand Down
2 changes: 1 addition & 1 deletion codec/anyblockjson/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-<internal_key>, 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`,
Expand Down
68 changes: 38 additions & 30 deletions codec/anyblockjson/noderivedtypeidsprose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-<internal_key>`,
// 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-<internal_key>`, 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"
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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")
}

Expand Down
Loading
Loading