GO-7383 API v2: a JSON HTTP API for agents over AnyBlock JSON - #3242
Closed
requilence wants to merge 262 commits into
Closed
GO-7383 API v2: a JSON HTTP API for agents over AnyBlock JSON#3242requilence wants to merge 262 commits into
requilence wants to merge 262 commits into
Conversation
…2 notes A cached ?dry_run=true result must never replay as its later real twin — the (space, key) hash now covers body + raw query, so same key with a different query is a 409 idempotency_conflict (C8 read as different request). APIV2.md gains §8.1 recording the Phase-2 implementation decisions as built: the snapshot create path and its rejected alternatives, the explicit create-vs-reject policy, the type/set/shortcut specifics, and the SPEC §2a recommendedLayout example-vs-export discrepancy flagged as a spec bug.
…documents FillRecommendedRelations detects already-filled lists by the FIRST entry of recommendedRelations; a type document whose typeProperties are all featured/hidden/file left the regular section empty and sent the RPC down the layout-defaults path, silently clobbering the document's featured list. CreateType now seeds an empty regular section with the system default sidebar properties (createdDate, creator, links) resolved through the create-missing resolver, keeping the RPC on its verbatim path. Plus adapter helper tests (snapshotRootId, bundledIdsToInstall).
Brings 26 base commits (format evolution) under the v2 API work. Conflict resolutions, both in pkg/lib/anyblockjson: - json.go: kept our exported FormatName/FormatByName alongside base's collapsed text-format vocabulary. FormatName now delegates to base's formatName helper so it applies the same shorttext->"text" fold; calling formatNames.name directly would have returned "" for shorttext. - typeproperties.go: our exported TypeProperty (+ the jsonTypeProperty alias, RecommendedList, BuildRecommendedLists) now carries base's new Options and ObjectTypes fields, and BuildRecommendedLists threads them into PropertyDefinition so a property minted through the PATCH-type surface gets the same shape import gives it. Adaptation to the collapsed text formats (base dropped "shortText" from the format vocabulary and the schema enum): the synthesized set document no longer declares name as shortText — every dataview property now resolves through the one text name — and the two agent-facing format lists (the POST /properties served schema and its error hint) stop advertising a format the schema rejects. Verified in the merged tree: our dataview block id matches template.DataviewBlockId, and the create surface already accepted recommendedLayout as a name, which is what base standardized on. go build ./... clean; core/api and anyblockjson suites green; gofmt clean.
API v2 Phase 3 pre-checks edit ops (leaf containment, replaceText targets) before the full document validation; export the package's leaf and text-bearing type sets so the wiring shares one source of truth.
MutateObject(ctx, space, object, build) locks the object, hands build the same consistent read the Phase-1 reader produces, diff-applies the snapshot build returns via state.NewDocFromSnapshot + history.ResetToVersion (the proven import updateExistingObject shape, incl. the bundled-revision guard), and reports the post-apply heads for the new etag. The shared locked-read helper is factored out of the read adapter.
PATCH /v2/spaces/{id}/objects/{id} applies the closed, id-addressed op set
(setProperties, updateBlock, replaceBlock, replaceSubtree, insertBlocks,
moveBlock, deleteBlock, replaceText, setCell, addItems, removeItems)
atomically: marshal the live state to its flat document, mutate the
document, validate it wholesale (R5 = SPEC §12 V1-V5), Unmarshal with the
Phase-2 create-missing resolvers, one diff-apply through ObjectMutator.
Block refs resolve by full id or unique suffix; payload indents are
relative (R3); every payload block id rides createdBlocks keyed by payload
position; If-Match is advisory (C7); dry_run computes without committing.
PATCH refuses objects whose marshal reports loss warnings (C11).
PUT replaces the whole document through the same pipeline (etag/warnings
stripped so a GET body round-trips; envelope id pinned; absent type keeps
the live one; canUpdateObject exclusions mirrored). diffStats diff the
canonical before/after documents - the accidental-full-rewrite signal.
PATCH/PUT object routes (write-rate-limited; concurrency safety is the
If-Match header - the idempotency middleware stays POST-only) and
GET /v2/schemas/ops/{op}: one tiny C13-strict schema plus a single-op
minimal example per op, wired into the §5 index as an ops list.
Record the decisions as built: document-level op pipeline over the reset-to-version apply, the ObjectMutator port shape, R5 via the format's own Validate, the C11 PATCH guard, diffStats semantics, R3 indent rules, per-op decisions, and PUT details.
The 4-lens review (core/api/APIV2_PHASE3_CODEREVIEW.md) found that resetting the live object to an AnyBlock snapshot turns everything the format deliberately drops into a destructive CRDT change: A1: the snapshot carries no RelationLinks, so the diff emitted RelationRemove for every CUSTOM relation key — which deletes the detail value on replay (the GO-7217 class; ResetToVersion repairs bundled keys only). Custom property values survived locally but vanished on other devices. A2: resolvedLayout is a stripped local detail, so resolveLayout saw unset->recommended as a change and ran the note conversion, which moved an unnamed page's first paragraph into the title and unlinked it. A3: structural blocks (header/title/description/featuredRelations) are dropped by the format (SPEC §7); the diff deleted them on every edit and featuredRelations only returns on a full source rebuild, so the featured row vanished for open clients. A4: the apply runs with NoRestrictions and only 4 sbTypes were excluded, so the API could rewrite objects the editor forbids (workspace, archive, widgets, a set's dataview) and reach type objects while bypassing the /v2/types guards. Fixes, all in the API adapter (core smartblock/apply semantics untouched): preserveEditorOwnedState carries live RelationLinks, resolvedLayout, the structural block subtree (restored leading the root) and extra object type keys into the reset state; checkObjectEditable enforces the object's own Blocks/Details restrictions before applying. Multi-type objects no longer lose ObjectTypes[1..]. Tests: the adapter had zero coverage — added unit tests for each finding (relation links, resolvedLayout, structural restore + ordering, extra types, edit preserved, no-structural-blocks case, restriction refusals, the structural-block predicate). Still open from the review: A5 (ResetToVersion forces DoSnapshot -> a full snapshot change per edit) needs a scoped change to the shared smartblock.ResetToVersion used by import/history/block-service, so it is filed rather than bundled here; A6 and the Tier-B seams follow.
Export the conversion machinery at fragment level for the API v2 edit path: UnmarshalBlocks (flat run -> subtree with topIds), UnmarshalBlock (single block with forced id, table internals included), UnmarshalPropertyValue (import twin of MarshalPropertyValue), MarshalBlockSubtree (subtree -> flat JSON run), and the inline codec (ParseInlineText/RenderInlineText). Fragment runs are validated by wrapping them in a minimal synthetic page document and running the existing document validation, so V1 monotonicity and the $5 per-type shape checks apply unchanged. Two fragment-specific guards: structural block types (title/description/featuredProperties) are rejected explicitly instead of silently absorbed, and no primary-dataview pinning happens on fragments.
…nary Apply Split apicore.ObjectMutator into MutateObject (PATCH: the adapter hands the callback an ObjectEdit carrying a child state of the live doc and commits it with ONE plain sb.Apply — per-block restriction checks, undo recording, hooks/events and the minimal id-matched change diff ride the normal editor path) and ResetObject (PUT: the previous reset-to-version machinery with preserveEditorOwnedState, kept until its own rework). The bundled-revision guard stays on the PATCH path (untouched revision/sourceObject are now simply inherited by the child state, so it only fires on an actual downgrade). Adapter tests cover the commit path, error rollback, restriction refusal and the revision guard.
Rewrite the Phase-3 PATCH pipeline: ops now mutate a child state of the live object (v2_stateops.go) instead of splicing the flat JSON document and resetting the object to the reimported snapshot. The flat document survives only as the read-only view the ops address blocks through (refs, unique suffixes, indent arithmetic, error texts — unchanged agent-facing contract) and as the diffStats input; payload blocks are interpreted through the anyblockjson fragment API with the same validation a whole document gets. Op -> state mapping: setProperties = SetDetail/RemoveDetail + AddRelationLinks (the A1 fix in miniature); updateBlock/replaceBlock = fragment re-import of the one block with its forced id, live children kept; replaceSubtree/insertBlocks = fragment run + InsertTo splice; moveBlock = Unlink + InsertTo; deleteBlock = Unlink; replaceText = find/replace on the document text + ParseInlineText back to marks; setCell = re-import of the one table block; add/removeItems = GetStoreSlice/UpdateStoreSlice. Validation without the whole-document pass: fragment runs validate in a synthetic document (V1 + the §5 shape checks), duplicate ids are checked explicitly against the state with op-shaped paths, the op-level pre-checks are unchanged, and a debug-flag-gated read-only marshal+Validate safety net (ANYTYPE_API_V2_VALIDATE_EDITS=1) logs any post-op issue. Create-missing option resolution moved BEFORE the object lock (review B6/A6) — no create-RPC ever runs while holding it. The edit tests port to the new pipeline (same ops, same error texts, same documents — asserted via the resulting state); PUT stays on the reset pipeline through ResetObject.
…uilt Record the redecision superseding v0.3.3's document-level apply: ops are operations on a child state, committed with one ordinary Apply; the mutation port split (MutateObject/ResetObject); the exact op->state mapping; how R5 validity is preserved without the whole-document Validate; create-missing before the lock; why diffStats stay the canonical document diff (dry-run parity); and what is now untouched by construction.
The 4-lens redesign review (core/api/APIV2_REDESIGN_CODEREVIEW.md) confirmed the state-ops thesis but found three things strictly worse than the pipeline it replaced. A1: create-missing prewarm ran before ANY precondition — before the object was known to exist, before If-Match, before restrictions. A PATCH to a nonexistent object returned 404 and still permanently created every option the batch named, and because prewarm resolves all ops up front, one rejected request could mint the whole batch's options. PatchObject now reads the object and checks preconditions FIRST, prewarms only once they pass, then takes the lock. (The dry-run branch reuses that read instead of re-reading.) A2: the per-op view rebuild was a DoS regression. Each op marshaled the whole document AND built a fresh storeresolver whose caches start empty, re-running ListAllRelations/ListRelationOptions per op, under the object lock, with no op cap; begin()'s document was discarded so even a 1-op PATCH marshaled three times. Now: one resolver reused across the whole PATCH, the view seeded from begin()'s document, the after-document reused from the view when still valid, a 512-op cap, and a ctx check per op so an abandoned request stops holding the lock. A3: every table op re-minted both layout wrappers (the format does not carry them, so the importer generated fresh ids), turning a cell edit into 'replace both wrappers, re-parent every row and column' — and two devices editing different cells merged into a table with duplicated rows/columns, while diffStats reported BlocksChanged:1. replaceLive now pins the live wrapper ids onto the re-imported table. Tests: wrapper ids survive a setCell; a stale If-Match is refused before the mutator with no option created; the op cap is enforced path-addressed. The stale-If-Match test no longer expects MutateObject — that it is never reached is the fix.
The state-ops redesign replaced the whole-document Validate with per-fragment validation, which cannot see invariants that span the spliced result. The 4-lens review found two of those silently unenforced. B3: the post-op document validation is ON by default again (it was an env-gated log). It costs nothing — the after-document is already marshaled for diffStats — and it is the cheap backstop for both findings below. The escape hatch inverts to ANYTYPE_API_V2_SKIP_EDIT_VALIDATE=1, for debugging a suspected false rejection. B1: V3 row->column containment was enforced by nobody. resolveTarget checks only leaf types (a row is correctly not a leaf), and checkFlatRun now sees an isolated fragment. Single ops (insertBlocks inside a row, moveBlock inside a row, replaceSubtree over a column) returned 200 and produced documents that fail anyblockjson.Validate — so the object's own GET body was no longer PUT-able, violating R5. The restored post-op validate rejects the whole PATCH. B2: the absolute depth bound. Fragment validation is run-RELATIVE, so a deep run passes on its own and can push the document past the bound when spliced. Worse, marshalDoc installed a no-op warning sink, which makes the exporter CLAMP instead of failing: the view then reported wrong depths to later ops in the same batch (deleteBlock saw descendants == 0, skipped the recursive guard and dropped a whole subtree), and the clamped after-document validated clean. marshalDoc now treats a degradation warning as an error on the internal paths, so the PATCH is rejected instead of committing a corrupted edit. Tests: the row->column violation is refused while a legal insert into a column still passes (no false rejection); an over-deep insert is refused rather than clamped.
C1: R5 payload-validation errors lost their ops[i] prefix. Fragment
validation produces run-relative paths, so in a multi-op batch every payload
problem surfaced as '/blocks/0/type' — the repair loop could not tell which
op failed. invalidFragmentError now rebases those paths onto the op that
carried the payload ('ops[2].blocks[3].text'), for insertBlocks,
replaceSubtree, updateBlock and replaceBlock.
C2: dry_run reported every would-be-created option twice, because the
pre-lock prewarm and the in-lock op both resolve the same name and the
dry-run branch recorded without memoizing. The real run reported one. Since
dry_run exists to preview exactly that, it now dedupes via a dryReported set
(a separate set, not the created-id map, so an empty id can never be mistaken
for a resolved one).
C3: the object-level restriction verdict now rides ObjectRead, captured under
the same locked read. dry_run previously skipped checkObjectEditable
entirely, so it returned 200 for an object the real PATCH refuses with a
restriction error; both paths now reach the same verdict, and the real path
refuses before prewarm rather than inside the lock.
Tests: a dry run previews one option, not two; a restricted object is refused
on the dry run as well.
Remaining from the review: C4 (updateBlock's merge round-trips the touched
block through its exported form, so non-format fields on that one block are
rebuilt) and the Tier-D/E items, incl. E'8's change-set assertion.
…veBlock Omitting all of after/before/inside now appends at the end of the document root — the ops-path into an empty object (SPEC §7 keeps title/description out of the document, so a fresh object had zero addressable anchors and PUT was the only way to give it content). Payload indents stay relative: at root, indent 0 = document top level. position still requires inside. The multi-target error is reworded to 'at most one of after, before, inside is allowed' since zero is legal now. §8.3 v0.3.5 notes added.
Agents auto-retry on timeout, and PATCH is where a blind retry does damage: a retried successful insertBlocks duplicates blocks, a retried deleteBlock 404s misleadingly. The C8 store, body+query hash, reservation and replay all existed — this wires the middleware onto the object PATCH/PUT routes and the types/properties PATCH routes, and lets it act on POST/PATCH/PUT. GETs with a key pass through untouched.
… in setProperties Appending one tag to a long multiSelect required read → whole-array rewrite → write. add appends without duplicating; remove deletes matching entries and is a no-op when absent. Only list-shaped formats (select, multiSelect, objects, files); scalar keys are rejected path-addressed naming the format. add shares set's create-missing option-name resolution, incl. the pre-lock prewarm; remove resolves read-only so it never mints the option it names. A key may appear in at most one of set/unset/add/remove per op.
…ing, pre-release)
Four routes to changing a block's text was the surface's largest
disambiguation load, and replaceBlock's silent text-wipe (a checkbox
toggle losing the text) was the documented small-model trap. updateBlock
{id, set} with merge-and-null-clears expresses everything replaceBlock
did except the wipe. The op set is 10 ops; an agent sending replaceBlock
gets the unknown-op error with a hint naming updateBlock's semantics.
replaceBlock tests migrated to updateBlock; the wipe-only assertions
retired.
A 3-lens review of the v0.3.5 modification-surface changes found one major and two contained defects; two lenses reached the major independently. Idempotency replay identity ignored method and path. The hash covered body + query and the store key was (space, key) — but PATCH/PUT carry the target object in the PATH, which POST never did. Two byte-identical edits to different objects under one reused key replayed the FIRST object's 2xx, with its etag, leaving the second object unedited and no error to repair from — the one failure class an agent cannot detect. Agents commonly derive keys from request content, and 'apply the same ops to N objects' produces identical bodies, so this was reachable in normal use. Method and path now join the hash, turning the collision into the existing 409 idempotency_conflict. Prewarm minted options for PATCHes that cannot succeed: it scanned every add value, including scalars (always rejected by the apply path) and keys also claimed by set (rejected as a cross-field conflict), leaving orphan options in the space behind a guaranteed 400. It now skips both. add on a single-select silently produced a two-valued select, since status is list-shaped in the value encoding but holds one value. It is refused with a steer to set; add on an EMPTY select stays legal, which the paired test pins. Tests: cross-object and cross-method replay refusal, PUT replay (the method switch covered PUT but nothing exercised it), and a router-level test asserting the middleware is actually registered on each edit route — the E'8 class, where reverting router.go alone left every other test green.
…0-3 surface Apply the consolidated findings of three read-only reviews (phase4, phase5, crosscut) so Phases 0-3 read as fact and Phases 4-5 are buildable against the current primitives. - Phase 4 replanned: collections read path, sorts/C10 pagination, single-form primary example, explicit validation/resolution rules (key scope, system-key allowlist, read-only option resolution, per-space global semantics, empty-date warning, type pseudo-key), placeholder substitution for stored views, search declared a read (C8/C9 exempt), build-vs-reuse inventory. - Phase 5 / §7 aligned with the shipped op set: R9 create-missing option names, add/remove on set_properties, check_item over updateBlock, markdown decided as an insertBlocks payload alternative, reference/editing channel caveats (full-read relabeling, D'1 markup source), hard dependency order, handle-state story, create-with-markdown caveats. - §3/§4 refreshed: built items moved out of the build list; B1/B2 reworded to steering decisions (replaceText/setCell and both filter forms ship). - §5/§6: nine shipped kinds + search kind + filter-grammar discovery slot; rollout records the ungated as-built posture; archive marked outstanding. - §8.x corrected to the shipped code: idempotency hash covers method/path/query/body, etag 8-char prefix accepted, R5 whole-document net ON by default (B'3), A'1/C'3 appended to §8.3, DELETE /objects re-marked [build]. - SPEC.md bumped to v0.7: §6.2.1 scope split - the filter grammar + parser ship now as pkg/lib/anyblockjson/filterstring for the API surface; only the document view field 'filter' stays reserved post-v1. §12/§13 updated.
…ed filter/sort fragment codec filterstring (SPEC §6.2.1, scope split v0.7): recursive-descent parser for the compact filter grammar — AND/OR precedence (AND binds tighter), parentheses, the full condition vocabulary incl. set literals (exactIn), HAS ALL, IS [NOT] EMPTY, EXISTS, date-preset functions with the counting pair carrying its operand, RFC 3339 → unix conversion on date-formatted properties. Emits the §6.2 structured filters array so both request forms land on one internal tree. Every error is offset-addressed (*Error: byte offset + offending token) with did-you-mean against wired-in property keys and option names (read-only — a query never creates options). The EBNF the parser pins ships as filterstring.EBNF (+ Examples) for the discovery surface and the Phase-5 GBNF conversion. UnmarshalFilters/UnmarshalSorts: the fragment-granularity §6.2 codec the two request forms converge on — enum vocabulary validation with /filters/i//sorts/i paths, the counting-preset operand and placeholder format rules via the document path's checkDateFilters, and the unguarded date-comparison warning riding Options.OnWarning (C11).
POST /v2/spaces/{spaceId}/search + POST /v2/search (global), and the
sets/collections read path (GET sets/{id}/objects|views,
collections/{id}/objects|views).
Search: both filter forms — the compact string (filterstring) and the
structured §6.2 array — land on one internal tree via
anyblockjson.UnmarshalFilters and one direct database.Query; both supplied
is a 400 ambiguous_input. Sorts take any property key (v1's 4-value enum
is gone); pagination is the C10 query params, a body limit is rejected by
the strict schema with steering. Search is a read: no idempotency
middleware on the routes (asserted by test), dry_run ignored. The Phase-4
rules as specced: key scope from the type's recommended set or the
space's keys, the system-key allowlist, READ-ONLY option resolution with
did-you-mean (a query never creates the option it names), per-space
global resolution with skip-warnings and honest totals (sum of per-space
store counts — never total=len(fetched)), the unguarded-date-comparison
warning riding the C6 channel, and type as a filter pseudo-key composing
with the scalar top-level type by AND. Global rows carry spaceId.
Sets/collections: one implementation branching on layout; a wrong-layout
target 400s naming the other route. Sets execute their setOf source
(types → type In, relations → NotEmpty, OR-combined) — the direct
store-query path, NOT v1's racy shared-subId subscribe hack. Collections
read the store slice in curated order (honest totals; dangling members
drop out). ?view= resolves by id or unique suffix and substitutes the
§6.2 dynamic placeholders server-side: _filter_template_2_ → the caller's
participant id (V2Deps.AccountId, wired from the account component),
_filter_template_1_ → the host object; anything else drops the leaf and
warns — never v1's silent empty result. Views read back as raw §6.2 view
objects with option names.
C5 row building is extracted into a shared objectRowBuilder;
V2ListResponse gains a warnings channel.
… with the system-key allowlist The compact filter string on POST /sets now parses through anyblockjson/filterstring against the queried type's reference set and lands as the structured array in the set's initial dataview (SPEC §6.2.1: the document field filter stays reserved — export keeps writing filters). Option names are deliberately NOT parse-validated on this path: a set create is a write, where select option names create-missing (R9/§8.1) — unlike the read-only query path. Parse errors surface offset-addressed at /filter with did-you-mean. validateViewKeys (the R9 sets rule) now admits the Phase-4 system-key allowlist — createdDate, lastModifiedDate, creator, lastOpenedDate — keys in no type's recommended lists that back bread-and-butter queries (rule 2).
…ammar on the filters kind GET /v2/schemas/search serves the strict (C13) search request schema with the spec's single-filter worked example (asserted parseable by test). The compact filter-string grammar gets its discovery slot ON the existing filters kind — one concept, one slot (C2): that kind's response now carries the structured-array schema AND the EBNF + examples pinned by the filterstring parser (every served example asserted parseable) — the same artifact the Phase-5 GBNF conversion consumes.
…ild items to the Built ledger Records the implementation decisions Phase 4 settled: the parser's case-insensitive keywords and format-resolver-driven RFC 3339→unix conversion, the one-tree convergence through UnmarshalFilters, the effective sort list (explicit sorts primary under full-text with a relevance tiebreak), fulltext totals from the materialized candidate-bounded set, the global merge comparator's collation approximation, spaceId on global rows, the read-only-vs-create-missing option split between the query and sets-create paths, empty-setOf as an explicit 400, collection store-slice ordering, placeholder degradation semantics, and the AccountId plumbing. §2/§3 markers updated so nothing built is re-budgeted; only the [B3] rows encoder stays gated.
…runes Phase-4 review fixes (parser lens): - input capped at 4096 bytes and group nesting at 32 — a paren-bomb body used to overflow the goroutine stack, a runtime FATAL that killed the whole process (gin.Recovery cannot catch it) - the lexer decodes full runes: non-ASCII property keys (café, дата) are identifiers now, and 'unexpected character' names the rune the caller wrote instead of a stray continuation byte - date presets are rejected on the conditions the engine would silently drop (notEqual and everything non-ordering), addressed at the preset name token instead of the closing paren - counting presets bound their day count to [0, 36500] - unterminated-string errors echo at most 32 runes, never the rest of the input - steering hints: single quotes -> double-quote example; reserved-word keys and known keys the syntax cannot spell -> the structured filters array - EBNF gains identifier/number productions and the case-insensitivity note; ParseDate is exported for the structured-form date check; SPEC §6.2.1 records the format-driven date mapping and the parser's interpretation calls
…-date warning The canonical filter example — done = false AND (dueDate < currentWeek() OR dueDate IS EMPTY) — warned on every execution that the comparison also matches objects with no dueDate, steering agents to add a notEmpty guard that contradicts the filter's own OR-empty branch. An empty leaf on the same property under the enclosing OR is intent to include the undated objects, so the warning is suppressed there; an empty leaf on a different property still warns.
- structured filters reject an RFC 3339 string on a date property with the unix conversion spelled out — before, the string survived to the store and silently matched nothing (the rule-3 hazard on the form the parser could not protect); convergence tests pin both request forms to the identical filter tree (dates, presets, set literals, booleans, the type pseudo-key) - full-text pushes Limit offset+limit+1 into the store so the candidate budget escalates past the 100-doc floor: total capped near 100 and deep pages came back empty with has_more false (verified failing on revert with 120 indexed matches) - global search: offset capped at 2000 with steering to the space search; an unknown fields entry warns instead of dropping the whole space from results and total; reference sets are computed lazily for the bare-query fan-out; spaceRefs filters space views by the v1 ListSpaces status predicate so removing/deleted spaces get no index minted as a side effect - date sorts left includeTime-less default to second granularity, so ordering no longer changes with the presence of the full-text tiebreak - option-name validation skips when the store cannot list options instead of asserting 'no such option' about unread data - collection membership reorder is O(n log n) with one details read per record (was an O(n^2) comparison-time-reading insertion sort over the whole membership) - sets/collections ?fields= is rule-1 validated with did-you-mean (a typoed key 400s instead of returning rows that silently carry no properties); POST /sets answers a type filter leaf with a targeted 'already scoped' message instead of unknown-property - ensureSpace guard regression tests for all five Phase-4 entry points; placeholder host/empty-account unit tests; pagination boundary tests; compile-time assertion that account.Service keeps AccountID()
The 45 operation descriptions had grown a shared preamble. Sixteen repeated
Idempotency-Key, seventeen repeated dry_run, twenty cited a C-number, twelve a
section mark, twenty-two shouted in capitals. Median 269 characters, longest
1750, and the fact that only applied to THIS endpoint was somewhere in the
middle of it.
The shared behaviour now lives once in the API description: auth, the
idempotency key, dry runs, If-Match and etag, pagination, the error shape,
warnings, strict binding, body caps, what deleting does, and how a space is
addressed. An operation carries what is left, which for thirteen of them is
nothing. An empty description is the correct outcome, not a gap.
Median is 105 characters now, longest 399, and the internal references are
gone: a C-number, a section mark or a phase name names nothing a reader
outside this repository can look up, so where the rule mattered it is written
out in words instead.
TestV2DocumentProse guards it. It runs over the generated document rather than
the annotations, because that is what reaches a reader, and it covers whatever
swag rewrites on the way plus the prose that arrives from a model comment. It
fails on a C-number, a section mark, a phase name, a .md filename, a run of
three capitals outside a tiny allowlist of spellings (GET, JSON, URL and
friends), an em dash, or any description over 400 characters. The API
description is the one length exemption: it is where the shared behaviour is
stated. Against the pre-rewrite document it reports 143 failures.
Facts that left an operation went to a parameter, a response or a field
comment rather than being dropped: the delete-route steer is on the 400, the
already-deleted 404 is on the 404, the op names are on the {op} parameter, and
has_more's bounds-scoping is on the field.
v1's documents are byte-identical.
…case The second merge from the format branch (the first was 260635c), bringing the pre-freeze review, the Tier 1 fixes and SPEC v0.8, whose headline is that the format's own vocabulary is snake_case: 100 identifiers, every block type, field name, enum value and inline tag attribute the format defines. Thirteen files conflicted, all in pkg/lib/anyblockjson. The rule that resolved almost all of them: a format identifier takes their snake_case spelling, a property key takes our slug vocabulary. Both sides were converging on snake_case from opposite ends, and the regenerated goldens show the result -- "empty_placement" beside "property": "due_date", and "customStatus" passing through unslugged because no vocabulary entry claims it. The interesting resolutions: - export.go: their new id domain (seedIdLabels/idLabel/blockLabel, one uniqueness domain across sanitizing, compacting and generating) meets our C4 split of CompactIds into CompactObjectRefs+CompactBlockLabels. Kept both; their locals[candidate] avoid-set is subsumed by our fullIds, which covers both id populations rather than one. - flat_invariants_test.go: both branches independently wrote a file of that name pinning the same two invariants. No symbol collides, so both survive: theirs keeps the name (the invariants themselves, hostile-input driven, plus the snake_case walker), ours becomes flat_rules_test.go (the specific rules those invariants rest on). - FLAT.md: they added a stale-vocabulary banner to a document a8ded19 retired. Kept the deletion. - import.go: their featured_properties case references a `structural` variable our rewrite replaced with the structuralBlockTypes map. Dropped the hunk, renamed the map entry instead. The rename then had to reach the format code this branch added after they forked, which their invariant test cannot see because it walks their tables: blockvocab.go and fragment.go (featured_properties), markdownblocks.go (bulleted_list_item, numbered_list_item, heading_N, is_header), viewvocab.go (the aggregation names) and filters.go (the condition and date-preset lists). Test fixtures followed, swept against the schema's own field set rather than a hand-written list, so property keys and the reserved widget targets (allObjects, recentOpen) were left alone by construction. pkg/lib/anyblockjson is green, as are the cmd tools, core/api/wrapper, snapshotdiff and storeresolver. core/api/v2 is NOT green: 41 tests across core/api/eval, core/api/v2/model and core/api/v2/service still speak the old format vocabulary in served schemas, examples and fixtures. That is the follow-up commit; it is a v2-only sweep and must stop at the v1 boundary, where camelCase is deliberate.
…abulary The kind:"objectType" dispatch mismatch (type creation was completely unreachable) turned out to be one instance of a much larger gap: v2's own request-parsing vocabulary was never updated when the shared AnyBlock library (pkg/lib/anyblockjson) moved to snake_case. Fixed: - kind dispatch (objectType -> object_type) for POST/PATCH types - envelope fields: template_for, type_properties - view-set vocabulary: group_by, cover_property, end_property, hide_icon, card_size, cover_fit, colored_groups, page_size, default_template_id, default_type_id, wrap_content, list_size, alternate_rows, custom_order, include_time, empty_placement, no_collate, and the column aggregation enum (count_value, count_distinct, count_empty, count_not_empty, percent_empty, percent_not_empty) - block schema fields: object_id, icon_emoji, icon_image, card_style, background_color - search/sort probes: date_preset - property format multi_select (was unreachable as multiSelect) - structured filters' condition enum in the served schema doc (the real validator already only accepted snake_case; the doc was stale) Two silent production bugs this uncovered along the way, not just schema/doc strings: keycanon.go's view-key canonicalizer and keys.go's envelope canonicalizer were both still keyed on the old camelCase field names, so canonicalization silently stopped the moment a caller sent the (now correct) wire spelling. Fixed both. Deliberately left alone: the compact filter string compiler in pkg/lib/anyblockjson/filterstring, which still emits camelCase condition tokens that its own downstream validator rejects (root cause of the documented "IS NOT EMPTY" bug). That's a shared-library fix with a wider blast radius than this v2-scoped pass -- needs separate sign-off. go test ./core/api/v2/...: 223 -> 172 failing subtests, verified via git stash A/B on each round (not a fluke of one run), zero regressions introduced. Full findings, root causes, and remaining scope are in core/api/v2/EVAL_FINDINGS.md.
Same gap as the previous commit, one level down: pkg/lib/anyblockjson's block type enum was already migrated to snake_case (heading_1, heading_2, bulleted_list_item, toggle_heading_1, ...), but v2's own code and test fixtures still spelled several of them the old way. One confirmed functional regression, not just doc/test staleness: object.go's outlineHeadingTypes map -- which decides whether a block's text is included in a `?outline=true` read -- was still keyed on "heading1"/"heading2"/"heading3"/"toggleHeading1-3". Since real blocks carry "heading_1" etc., the map matched nothing: outline reads silently stopped showing heading text for every object. Fixed. Also fixed two served-schema examples that were invalid against their own schema (schemas.go's "object"/"template" kind examples, and schemas_ops.go's replace_subtree op example, both using "heading2" / "bulletedListItem"), and the same stale spellings across 5 test files' fixtures (edit_test.go, create_test.go, payloadids_test.go, object_test.go, schemas_ops_test.go). go test ./core/api/v2/...: 170 -> 29 failing subtests (223 -> 29 across both commits, an 87% reduction). Verified clean build, vet, and gofmt. EVAL_FINDINGS.md not yet updated for this round -- the remaining 29 are a mixed tail (chat mention-tag casing, a few filterstring.go-adjacent view/search cases, some untraced) still to characterize.
…ctOrders, mention tags) Continuing the same migration: three more genuine production gaps found by tracing the remaining test failures instead of guessing. - schemas_ops.go: the table row schema (opTableProps) still advertised isHeader; the real document validator only accepts is_header, so a caller following v2's own served schema for set_cell/insert_blocks table rows got rejected downstream. Fixed, plus the 5 test fixtures using it. - viewops.go: the "groups"/"objectOrders" output-only-field guard (the one that gives a clear "this is output-only, don't write it" error) was still keyed on the old objectOrders spelling, so real object_orders input silently fell through to a generic "unknown field" refusal instead. Fixed, plus nested group_id/object_ids in test fixtures. - inline mention tags (<mention object_id="...">): pkg/lib/anyblockjson's codec already renders AND parses object_id in both directions -- nothing in production still says objectId. Fixed the one stale doc string (schemas.go's chat message description) and two stale test expectations (chat_test.go, object_test.go). Plus a leftover multiSelect fixture and a handful of test assertions still checking pre-fix error text (templateFor path, customOrder path, multiSelect in a hint) that only needed to catch up to the wording the earlier commits already corrected. go test ./core/api/v2/...: 29 -> 9 failing subtests. All 9 remaining now trace to one place: pkg/lib/anyblockjson/filterstring's compact filter compiler still emits camelCase condition/field tokens (notEqual, greaterOrEqual, allIn, notEmpty, datePreset, ...) that the downstream validator rejects. That's the shared-library fix flagged in the previous commits -- confirmed here to be the sole remaining cause, not fixed in this commit.
… the compact filter compiler The last remaining piece: pkg/lib/anyblockjson/filterstring compiles the compact filter STRING syntax (SPEC 6.2.1) into the structured filter tree, and every condition/field name it can produce was a hardcoded pre-migration literal -- ten call sites, plus one JSON struct tag, plus the date-preset value table. Fixed, all confirmed against anyblockjson's own canonical enums (json.go's conditionNames / datePresetNames): - condition tokens: notEqual/greaterOrEqual/lessOrEqual/notContains/notIn/ allIn/notAllIn/exactIn/notExactIn/notEmpty -> their snake_case forms - the emitted node's date_preset field tag (was datePreset) - the datePresets value table: daysAgo()/daysFromNow() and every multi-word preset (lastWeek, currentMonth, ...) now fold into the snake_case structured value (number_of_days_ago, last_week, current_month, ...) instead of the camelCase one. The MAP KEYS -- the compact syntax's own function-name vocabulary the user types, e.g. currentWeek() -- correctly stay camelCase; that's a different, intentional vocabulary confirmed against the served EBNF grammar, not part of this migration. - the multiSelect format check inside stringValue (ResolveFormat now returns "multi_select"), and doc comments to match Same fix applied to core/api/v2/service/schemas.go's structured "filters" kind: the date_preset field's documented enum and worked example were teaching the same stale camelCase values. Cleaned up every consumer's own stale expectations that assumed the old output: pkg/lib/anyblockjson/filterstring's own test suite, and core/api/v2/service's search_test.go / list_create_test.go / viewops_test.go -- all of which hardcode either the compiler's output or raw structured filter JSON using the old spelling directly. go test ./core/api/v2/... ./pkg/lib/anyblockjson/...: all green (0 failures). This closes the loop from EVAL_FINDINGS.md: 223 -> 0 failing subtests across five commits. go build ./..., go vet, and gofmt are all clean.
…-validation error text Cosmetic but genuinely wrong: the error Path and Message said emptyPlacement while the actual field (confirmed against dataview.go's json:"empty_placement" tag) is empty_placement. Found while auditing for other leftover camelCase after the filterstring fix.
Mechanical merge only: the tree builds, the API adaptation follows.
Resolution rule: the anyblock branch is authoritative for pkg/lib/** and
cmd/**, so all 34 conflicts there took theirs wholesale. Verified this loses
nothing -- the four fixes this branch contributed upstream (the filterstring
snake_case condition tokens, the grammar comment, and filters.go's
empty_placement) are all present and equivalent on their side.
core/api/APIV2.md was the only conflict needing judgement. Kept THIS branch's
C2/C3/C4 rows: their copy still describes the pre-migration camelCase
vocabulary, while ours carries the snake_case decision (§8.46) and already
had C4 right (object refs full on every shape, no refs legend). Adopted their
clarifying `?ids=` comment -- object ids are never compacted -- which is the
one thing their text said better.
Four signature changes fixed with intent, not silenced:
- snapshotdiff.Compare now takes the smartblock type; ScoreCorruptionJSON
threads it from the original document's own import rather than assuming
a page, since the comparator uses it to know which slots the format
legitimately omits.
- MarshalPropertyValue now also returns the key's option-id legend. Rows
drop it knowingly: a row has no envelope to hang a legend on and select
values have always been served there as bare names. Whether `fields=`
should gain an id-bearing shape is a surface decision, recorded in the
comment, not settled here.
- TypeProperty.Key split into Property (document-facing spelling) and
InternalKey (stored key) per §2e; validateTypePropertyFormats resolves on
the spelling and falls back to the internal key. Its issue path was also
still /typeProperties -- now /type_properties.
- BuildRecommendedLists now returns an error, wrapped rather than dropped.
go build ./core/api/... is clean. go test ./core/api/... has 133 failing
subtests, which cluster into ~6 root causes rather than 133 problems -- 83
of them are the single §2e rename of a dataview property entry from
{key, format} to {property, format}. Catalogued in the follow-up.
…properties
The format split the member `key`, which used to mean both a document-facing
spelling and a stored id, into `property` (the spelling) and `internal_key`
(the stored key). Two API-facing slots carried the old name.
- Dataview property entries. `dataviewProperties` built {key, format};
the schema is now {property, format} with additionalProperties:false and
`property` required — there is no stored-key member on this entry at all,
and the §3 chain resolves an exact stored key through `property` anyway,
so this is a pure rename rather than a choice between the two names.
- type_properties entries, in the served `type` example and across the
test fixtures.
This was 118 of the merge's 133 failing subtests — one rename, not a
hundred problems. 133 -> 15.
Test-side fallout of the same §2e rename: a fixture read p["key"].(string) on a dataview properties[] entry, which is now nil and panicked — aborting the whole service test binary and hiding the real failure count behind an early exit (15 reported, 44 actual).
A type document no longer has a top-level `key` or `type_properties`. Both moved into the gated `type_settings` subtree — `key` as `api_key` (it was always the apiObjectKey slug) and the array as `property_definitions` — alongside `layout`, `plural_name`, `default_template` and `default_view`, which left `properties` at the same time. POST /types takes an AnyBlock document, so it mirrors: docEnvelope gains a TypeSettings member and CreateType's identity layer reads the slug from type_settings.api_key. A stale top-level `key` or `type_properties` gets a named migration steer rather than the format's own message, which talks about property spelling and is unactionable for someone who wrote `"key": "task"` meaning the type's api key. PATCH /types takes v2's own partial body, so it could have kept the flat spellings. It mirrors too, deliberately: `properties` is now name and description only, the layout is patched as type_settings.layout, and api_key is absent from the patch surface entirely — the slug is identity, union-checked at create, and re-pointing it would silently break every URL that names the type. One vocabulary across POST and PATCH (C2) beats a flat alias this endpoint alone would keep alive. typeDetailValue now takes the request pointer instead of a bare wire name; it was hardcoding /properties/ and would have reported /properties/type_settings/layout. The served `type` example is rebuilt in the new shape and verified to pass anyblockjson.Validate rather than assumed to. 44 -> 26 failing subtests.
…grate Neither API v2 nor AnyBlock has been released, so a caller sending the pre-§2a `key` or `type_properties` does not exist. The steer I added for them was compatibility scaffolding for an empty set, and the guard against `type_settings` on an object document restated a rule the validator already owns. Verified rather than assumed: anyblockjson.Validate already refuses all three, path-addressed — /type_settings: only valid on type documents (kind "object_type", §2a) /type_properties: moved: ... states its definitions in "type_settings" ... /key: property "key" is not allowed ... and its type_properties message is better than the one I wrote. One statement of each rule, in the validator. No test movement (26 -> 26): the removed code was unreachable for any document a caller can now write.
…and kind:"template" Three more §2a/§2b/§2e consequences. - §2b collapsed the flat icon_emoji/icon_image pair into one typed `icon` whose `format` selects the variant. Both op payload defs are additionalProperties:false, so a variant the op schema does not publish is one a grammar-constrained decoder cannot author at all — the schema now carries the whole plainIcon shape (emoji/file/icon/color) plus icon_size, and TestSchemaOp's companion assertion was inverted to match: the flat pair must NOT resolve, or the guard would stop noticing. - §2e renamed the property block's `key` to `property`, in the op schema and in viewops' three dataview-property sites — two reads (membership, and the bare insert_view column builder, which was silently producing a one-column view) and one write. - POST /templates now injects kind:"template" the way POST /types injects kind:"object_type". The endpoint is the kind; making a caller restate what the URL already said is the trap C2 exists to avoid. 26 -> 19 failing subtests.
Stale spellings the earlier passes did not reach: the property block's `key` (fixture and its reader), two type documents still carrying an envelope `key`, a second test-side `["key"]` read that panicked and truncated the run again, the /typeProperties issue paths (one in resolver.go, three assertions), and the served template example, which needs kind:"template" like every other template now does. 19 -> 12 failing subtests, no panic.
…rmat
Mirroring type_settings on PATCH took the icon off the patch surface
entirely — properties.icon_emoji was the only way to set one, and §2b moved
it to the typed envelope member. That was a capability hole, not a cleanup,
so the patch body gains `icon`.
It is decoded by handing a minimal type document to anyblockjson's own
importer and reading back the details it set, rather than by restating which
of iconEmoji / iconImage / iconName / iconOption each variant writes. That
table is the format's, and §2b exists precisely because those nine flat keys
had no single owner; a copy here would be the same bug one layer up.
Validation issues are re-addressed onto /icon so the caller is told about
the thing they sent.
Two test-side consequences, both taken honestly:
- the slug-spelling table moves to {"icon":{…}} and
{"type_settings":{"layout":…}}, and drops its camelCase case — nothing
shipped, so there is no old spelling to keep working.
- the duplicate-spelling case is removed rather than contrived. With
`properties` down to name and description, both single-spelled, a
duplicate of anything else is refused as not-updatable first — and that
is the better error. The guard stays for the key that gains a second
spelling; the comment says why it currently cannot fire.
Also reverted an over-reach from the previous commit: two /key assertions in
TestV2CreateProperty are the PROPERTY surface's own body field, not the
type envelope, and were correct as they were.
12 -> 8 failing subtests.
…is now refused - The GET-type corpse test decodes type_settings.property_definitions with `property` instead of a top-level type_properties with `key`. - A create body still carrying an envelope `key`. - The apiObjectKey forgery case now asserts a REFUSAL, not a drop. §2a made apiObjectKey a type_settings member, so the format rejects it in `properties` before the create path ever sees it — one layer earlier and stricter than the API's own drop. The invariant is unchanged (a forged slug never reaches the mint); only the mechanism moved, so the test asserts the mechanism that now enforces it, including the sharp case where the forged value would have been the only apiObjectKey. 8 -> 5 failing subtests.
- ListViews: a block fragment is an ENVELOPE now, carrying the §9a typed legends beside its blocks, not a bare array. This surface serves views only and their option values are already names, so the legends are read past rather than forwarded. - The id-shapes fixture never put its link target in the store. §9's missing-reference rule rewrites a reference the space does not hold to `_missing_object`, and storeresolver answers that question — so the read was correctly saying "missing" to a question these subtests are not asking. Checked before assuming a wiring gap: missingFromSpace fails SAFE (no existence resolver means "not missing"), so this was only ever a fixture hole, never a production one. - The eval testdata carried heading1 and isHeader — the same two renames as the Go fixtures, in JSON the earlier sweeps did not reach. go test ./core/api/... and ./pkg/lib/anyblockjson/... are green, go build ./... is clean, and vet and gofmt are clean on both trees.
…o longer exists
Two silent breakages in the layer on top of the API, neither caught by
tests — because the tests stub the API with the same stale shapes the code
expects, so both sides drifted away from production together.
- describe decoded a type document as {key, typeProperties[{key}]}. §2a/§2e
moved all three. json.Unmarshal leaves absent members zero, so describe
answered "this type has no properties" — the one answer an agent acts on
without questioning. It now reads type_settings.property_definitions and
each entry's `property`, falling back to internal_key.
- the A2 option-name guard keyed on format "multiSelect", which the format
now spells multi_select, so the guard silently stopped firing for every
multi-select property.
To stop the class rather than the instances, fixture.stub now validates any
stubbed body that IS an AnyBlock document against the format, and fails the
test naming it: a stub the API could never serve tests nothing. v2's own
envelope additions (etag, warnings, outline, markdown) are stripped first
and partial ?block= reads are skipped — those are v2's contract, not the
format's, and validating them as the format's would fail on our own design.
The guard immediately found four more stale stubs (heading1) the earlier
sweeps had not reached.
…true
Setting a select value to a name the property does not hold used to MINT
that option silently. The value names a label, not a create, and an
unmatched one is far more often a typo or a hallucinated label than a
deliberate new option — while a minted option joins the property's
vocabulary for every object and every member of the space, with no delete
surface in v2. Airtable's `typecast` and monday's `create_labels_if_missing`
both default off for the same reason.
Now: `?create_options=true` is the consent, parsed group-wide like dry_run
(one parse, one closed value set, one refusal, and a route added later
inherits the gate). Default off refuses, naming the property, the value,
and both ways forward.
The refusal lives PRE-LOCK, in guardCreateMissing, which already walks the
op payloads before any RPC — so a refused PATCH touches nothing. The
resolver keeps a backstop for the create paths, which have no such guard,
and both call one optionConsentError so the rule cannot be worded two ways.
The probe deliberately still RECORDS would-be creations instead of
refusing: it exists to hand the guard the whole pending list, and a probe
that refused would leave the guard blind — which is exactly what my first
attempt did.
Using an option that already exists needs no flag: the gate is about
minting, not about referencing, or the flag would tax every ordinary write.
Threaded explicitly through all seven resolver call sites rather than
defaulted, so each states its answer: CreateCollection passes none (its
body is {name, items} — no property values can reach the resolver) and the
read-only format resolver denies on principle.
Tool layer: the CLI's --create-missing and the wrapper's AllowNewOptions now
send the flag as well as skipping the wrapper's own A2 pre-validation.
Without that second half the flag would only have moved the refusal from
the client to the server.
SKILL.md, the OpenAPI descriptions and the CLI help all say the new rule.
`create_options` read as "create options", which is what POST /properties does deliberately. This flag is narrower: it only reaches names a property does NOT already hold — an existing option resolves without it — and the name should say that, since the whole point of the gate is that a caller understands what they are consenting to. It also matches the vocabulary already in the code (guardCreateMissing) and on the CLI (--create-missing), so the wire name, the Go identifiers and the user-facing flag are now one word apart instead of three different ones. Renamed through the middleware, the handlers, the service signatures, the resolver's refusal hint, the OpenAPI @PARAM docs and SKILL.md — the Go identifiers alongside the wire name, so a reader never has to map between them.
requilence
force-pushed
the
go-7383-anyblockjson
branch
from
August 27, 2026 09:06
971894d to
52df8b7
Compare
… v2 branch # Conflicts: # .gitignore
requilence
force-pushed
the
go-7383-apiv2-phase0
branch
from
August 27, 2026 09:29
acc1226 to
3a138f9
Compare
Coverage provided by https://github.com/seriousben/go-patch-cover-action |
Contributor
Author
|
closed in favor of #3266 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #3241 (AnyBlock JSON). Review that one first — this branch is the consumer; the diff shown here includes its base until #3241 merges.
A local JSON HTTP API designed for LLM agents, serving and accepting AnyBlock JSON documents. It shares nothing with v1 at the type level —
core/api/v2is its own package tree with its own OpenAPI document — so v1 is unchanged and its generated docs are byte-identical throughout.Specs (read in this order)
core/api/APIV2.mdcore/api/APIV2_PLAN.mdcore/api/APIV2_TOKENS.mdcore/api/APIV2_SURFACES.mdpkg/lib/anyblockjson/ADDRESSING.mdcore/api/APIV2_OBJECT_DELETE.mddocs/DerivedDeleteConsistency.mdWhat's built
Read, create, edit (a closed, id-addressed op set), query with a compact filter DSL, chats, the space periphery, the view family, scoped API keys with a fail-closed route registry, and the identity layer (one slug vocabulary, both directions, derived from the bundle).
Two agent-facing surfaces sit on top: a task-tool wrapper (12 tools, CLI verbs and an MCP stdio server, two model tiers) and
SKILL.mdguides for direct HTTP use.cmd/apiv2evalis an evaluation harness that runs real local models through the real API. It measured most of what shaped this branch — and found defects three rounds of code review had missed.Changes outside
core/apiThese are the ones worth calling out, because they touch shared or shipped code:
pb.Change.integrationName(field 10) — the raw, user-chosen name of the paired API key, stamped on an object's creating change and used to enforce own-output-only deletion. Immutable for the same reasoncreatedDateis: it rides a signed, CID-addressed, append-only change. Stored raw and compared exactly — normalization was tried and rejected, because it made visibly different app names into one principal. Additive on the wire; old clients ignore an unknown field, and nothing re-marshals a change in a real tree.core/block/editor/smartblock,state,source— carry that stamp, per-apply only: a later local edit must not inherit it.core/application/sessions.go,core/session,core/wallet— API-key issuance now requires a non-empty app name and bounds it (there was no bound anywhere before).core/block/object/objectcreator— mint-time uniqueness forapiObjectKey, checking stored slugs, stored keys and the bundled-derived vocabulary.space/internal/components/migration/apiobjectkey— backfillsapiObjectKeywhere it is absent.pkg/lib/bundle/apislug.go— the bundled key ↔ slug table, both directions, built from the bundle (a case transform does not round-trip; this is a table on purpose).core/block/detailservice/set_details.go— archive success is judged over the requested ids, not over a cascade that may have succeeded while the target was refused.pkg/lib/localstore/objectstore/spaceindex/relations.go—ListRelationOptionsexcludes uninstalled options.util/anonymize— covers the new field, so an app name cannot ride a debug export verbatim.core/api/service/cache_manager.go(v1) — indexes a type under its derived key as well as its served key. Without this, an integration holding a custom type key breaks after a restart once a slug is stamped.Regenerating
make openapi— two documents,core/api/docs/{v1,v2}. Both are committed; v1 must stay byte-identical.make protos-goafter touchingpb/protos.make test-deps.Not in this branch
Locator slices 2.1c/2.1d, the
?mode=read collapse, Phase 8 surface completion, and the format-level pin work — all staged inAPIV2_PLAN.mdand intended for follow-up branches.Three defects in
developwere found while auditing delete semantics and filed rather than fixed here: GO-7443, GO-7444, GO-7445.