We want diffs to render changes in the context where they were made. The API should continue to use the same diff query interface but return a response that groups changes under renderable relation types (starting with blocks).
- Keep the diff query surface area unchanged (same route + params).
- Support rendering diffs inline on an entity page (block changes shown in place).
- Group changes by relation type ID (e.g.
BLOCKS_ID). - Preserve existing flat
valuesandrelationsdiff lists for standard renderable changes.
- Adding new diff endpoints or query params.
- Changing the storage model in this RFC.
GET /versioned/entities/:id/diff returns:
{
"entityId": "...",
"name": "...",
"values": [...],
"relations": [...],
"blocks": [...]
}
Blocks are already derived from BLOCKS relations, but the response is flat and doesn’t provide a rendering structure that mirrors the page.
To support context-aware grouping beyond relation structure (e.g. edits spanning multiple nested contexts), we propose adding context metadata to edits.
Add a contexts dictionary to the edit header:
Context {
root_id: ID
edges: List<ContextEdge>
}
ContextEdge {
type_id: ID
to_entity_id: ID
}
Add to the Edit header:
context_count: varint
contexts: Context[]
Add an optional context_ref field to each Op payload:
context_ref: varint? // index into contexts[] (optional)
- If
context_refis omitted, the op is treated as having no explicit context. - Multiple ops can share a single context entry via
context_ref.
- Avoids repeating full context paths on every op.
- Allows a single edit to span many contexts.
- Keeps the diff API surface unchanged while enabling richer grouping.
Return a grouped diff object where the root node includes dynamic keys for relation-type groupings.
Example (blocks):
{
"entityId": "Byron",
"name": "Byron",
"values": [],
"relations": [],
"BLOCKS_ID": [
{
"id": "TextBlock_9",
"type": "textBlock",
"diff": [
{ "value": "old ", "removed": true },
{ "value": "new ", "added": true }
]
},
{
"id": "ImageBlock_2",
"type": "imageBlock",
"before": null,
"after": "https://..."
}
]
}- The grouping key is the relation type ID, not the relation ID.
- Items remain
BlockChangepayloads. - Ordering follows existing snapshot ordering for blocks.
- Root-level
valuesandrelationsremain unchanged for diffs not mapped to renderable relations.
export type GroupedChangeItem =
| TextBlockChange
| ImageBlockChange
| DataBlockChange
| EntityDiff;
export type GroupedEntityDiff = {
entityId: string;
name: string | null;
values: ValueChange[];
relations: RelationChange[];
// Optional discoverability for dynamic-only or hybrid keys
groupKeys?: string[];
// Optional static known keys (e.g. blocks) for hybrid mode
blocks?: GroupedChangeItem[];
} & Record<string, GroupedChangeItem[]>;- Compute the current
EntityDiffusing existing snapshot + diff logic. - Emit the grouped root with the standard fields (
entityId,name,values,relations). - For each supported relation type grouping (initially
BLOCKS_ID), map the grouped diff array onto[relationTypeId]. - Keep other changes in the flat
valuesandrelationsarrays. - If a change is grouped under a relation-type key, do not emit it as a standalone root in proposal diffs.
When edit context metadata is available, use it to route changes into relation-type groupings.
Inputs:
rootEntityIdfrom the diff requestentityDifffor the root (already computed)contextsfor edits in the diff window (GRC-20 context extension)changedEntityIdsderived from the diff (see below)
Steps:
- Build a set of
changedEntityIds:- Include any entity IDs referenced in
entityDiff.blocks(block entity IDs). - Include entity IDs from value/relations diffs if we later group non-block changes.
- Include any entity IDs referenced in
- For each
changedEntityId, find theContextit belongs to (from edit metadata):- Match if the context’s
root_idequalsrootEntityIdand the lastContextEdge.to_entity_idequalschangedEntityId.
- Match if the context’s
- Use the first edge in
Context.edgesto choose the grouping key:groupKey = edges[0].type_id
- Attach the change to
[groupKey]:- If
groupKeyequalsBLOCKS_ID, push the block diff item. - Otherwise, defer to future relation-type groupings.
- If
- If no matching context, leave the change in the flat
values/relationslists.
Notes:
- This keeps the API response shape unchanged while using context metadata to pick the correct relation-type bucket.
- The first edge represents the immediate child relation from the root (e.g. Byron --BLOCKS--> TextBlock_9), which is what we need to render inline.
Dynamic keys reduce strict typing. Two possible approaches:
- Dynamic-only keys (e.g.
BLOCKS_ID):- Include
groupKeys: string[]so clients know which dynamic fields are present.
- Include
- Hybrid keys:
- Use static named keys for known groups (e.g.
blocks). - Allow additional dynamic keys for other relation types when needed.
- Use static named keys for known groups (e.g.
Example (dynamic-only + groupKeys):
{
"entityId": "Byron",
"name": "Byron",
"values": [],
"relations": [],
"groupKeys": ["BLOCKS_ID"],
"BLOCKS_ID": [
{
"id": "TextBlock_9",
"type": "textBlock",
"diff": [
{ "value": "old ", "removed": true },
{ "value": "new ", "added": true }
]
}
]
}Example (hybrid keys):
{
"entityId": "Byron",
"name": "Byron",
"values": [],
"relations": [],
"groupKeys": ["SOME_OTHER_KEY"],
"blocks": [
{
"id": "TextBlock_9",
"type": "textBlock",
"diff": [
{ "value": "old ", "removed": true },
{ "value": "new ", "added": true }
]
}
],
"SOME_OTHER_KEY": [
{
"id": "OtherChild_1",
"type": "dataBlock",
"before": "Old",
"after": "New"
}
]
}Instead of using raw relation type IDs as dynamic keys, we could expose a stable set of
named group keys (e.g. blocks) and map those to relation type IDs internally. This
improves inspectability at the cost of an additional mapping layer.
Example (named key):
{
"entityId": "Byron",
"name": "Byron",
"values": [],
"relations": [],
"blocks": [
{
"id": "TextBlock_9",
"type": "textBlock",
"diff": [
{ "value": "old ", "removed": true },
{ "value": "new ", "added": true }
]
}
],
"SOME_OTHER_KEY": [
{
"id": "OtherChild_1",
"type": "dataBlock",
"before": "Old",
"after": "New"
}
]
}- Support grouping for additional relation types (e.g., tables, rows, cells).
- Persist edit-context metadata from GRC-20 edits in the indexer for UI grouping.