@agent-native/core 0.154.3 → 0.154.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/content/.agents/skills/content/SKILL.md +2 -2
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
- package/corpus/templates/content/actions/_blocks-field-identity.ts +464 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
- package/corpus/templates/content/actions/_database-utils.ts +55 -3
- package/corpus/templates/content/actions/_property-utils.ts +141 -31
- package/corpus/templates/content/actions/add-database-item.ts +62 -240
- package/corpus/templates/content/actions/configure-document-property.ts +170 -1
- package/corpus/templates/content/actions/delete-document-property.ts +56 -17
- package/corpus/templates/content/actions/delete-document.ts +3 -0
- package/corpus/templates/content/actions/edit-document.ts +18 -1
- package/corpus/templates/content/actions/export-document.ts +41 -0
- package/corpus/templates/content/actions/remove-database-items.ts +10 -0
- package/corpus/templates/content/actions/set-document-property.ts +140 -3
- package/corpus/templates/content/actions/update-database-item.ts +62 -0
- package/corpus/templates/content/actions/update-document.ts +24 -0
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +99 -11
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +6 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
- package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
- package/corpus/templates/content/app/hooks/use-document-properties.ts +4 -1
- package/corpus/templates/content/app/i18n-data.ts +20 -0
- package/corpus/templates/content/changelog/2026-08-10-database-blocks-fields-now-keep-logical-block-identity-throu.md +6 -0
- package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
- package/corpus/templates/content/changelog/2026-08-13-blocks-field-conflicts-now-keep-the-newer-saved-version-visi.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.block.md +5 -4
- package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +9 -4
- package/corpus/templates/content/docs/product/encyclopedia.md +4 -4
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/server/db/schema.ts +100 -3
- package/corpus/templates/content/server/plugins/db.ts +85 -0
- package/corpus/templates/content/shared/api.ts +78 -2
- package/corpus/templates/content/shared/blocks-field-identity.ts +554 -0
- package/corpus/templates/content/shared/document-export.ts +33 -1
- package/dist/action.d.ts +22 -0
- package/dist/action.js +27 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/url-safety.d.ts +7 -2
- package/dist/extensions/url-safety.js +27 -12
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-client/oauth-client.d.ts +16 -1
- package/dist/mcp-client/oauth-client.js +177 -83
- package/dist/mcp-client/remote-store.js +46 -20
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/oauth-tokens/index.d.ts +2 -1
- package/dist/oauth-tokens/index.js +2 -1
- package/dist/oauth-tokens/lifecycle.d.ts +87 -0
- package/dist/oauth-tokens/lifecycle.js +434 -0
- package/dist/oauth-tokens/migrations.js +10 -0
- package/dist/oauth-tokens/store.d.ts +22 -0
- package/dist/oauth-tokens/store.js +141 -18
- package/dist/observability/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/action-routes.js +10 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +2 -1
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type ReactNode,
|
|
17
17
|
type PointerEvent as ReactPointerEvent,
|
|
18
18
|
} from "react";
|
|
19
|
+
import { toast } from "sonner";
|
|
19
20
|
|
|
20
21
|
import {
|
|
21
22
|
documentPropertiesResponseMatchesScope,
|
|
@@ -52,6 +53,25 @@ interface DocumentBlockFieldsProps {
|
|
|
52
53
|
primaryEditor: ReactNode;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
function isBlocksFieldRevisionConflict(error: unknown): boolean {
|
|
57
|
+
if (error instanceof Error) {
|
|
58
|
+
return error.message.includes("Blocks field revision conflict");
|
|
59
|
+
}
|
|
60
|
+
if (!error || typeof error !== "object") return false;
|
|
61
|
+
const candidate = error as {
|
|
62
|
+
status?: unknown;
|
|
63
|
+
message?: unknown;
|
|
64
|
+
error?: unknown;
|
|
65
|
+
cause?: unknown;
|
|
66
|
+
};
|
|
67
|
+
if (candidate.status === 409) return true;
|
|
68
|
+
return [candidate.message, candidate.error, candidate.cause].some(
|
|
69
|
+
(value) =>
|
|
70
|
+
typeof value === "string" &&
|
|
71
|
+
value.includes("Blocks field revision conflict"),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
export function blockFieldsFromProperties(
|
|
56
76
|
properties: DocumentProperty[],
|
|
57
77
|
): DocumentProperty[] {
|
|
@@ -657,24 +677,55 @@ export function useBlockFieldEditor({
|
|
|
657
677
|
documentId,
|
|
658
678
|
propertyId,
|
|
659
679
|
initialContent,
|
|
680
|
+
initialRevision,
|
|
660
681
|
save,
|
|
682
|
+
onRevisionConflict,
|
|
661
683
|
}: {
|
|
662
684
|
documentId: string;
|
|
663
685
|
propertyId: string;
|
|
664
686
|
initialContent: string;
|
|
687
|
+
initialRevision: number;
|
|
665
688
|
save: (request: {
|
|
666
689
|
documentId: string;
|
|
667
690
|
propertyId: string;
|
|
668
691
|
value: string;
|
|
692
|
+
expectedBlocksFieldRevision: number;
|
|
669
693
|
}) => Promise<unknown>;
|
|
670
|
-
|
|
694
|
+
onRevisionConflict?: () => void;
|
|
695
|
+
}): {
|
|
696
|
+
content: string;
|
|
697
|
+
editorResetVersion: number;
|
|
698
|
+
onChange: (markdown: string) => void;
|
|
699
|
+
} {
|
|
671
700
|
const key = `${documentId}:${propertyId}`;
|
|
672
701
|
|
|
673
702
|
// The shared controller calls the freshest save impl through a per-key ref. The
|
|
674
703
|
// save TARGET (documentId:propertyId) is fixed by the key; only the function
|
|
675
704
|
// identity changes per mount, never the field it writes to.
|
|
676
705
|
const implRef = blockFieldSaveImplRef(key);
|
|
677
|
-
|
|
706
|
+
const revisionRef = useRef(initialRevision);
|
|
707
|
+
const rejectedRevisionRef = useRef<number | null>(null);
|
|
708
|
+
const onRevisionConflictRef = useRef(onRevisionConflict);
|
|
709
|
+
onRevisionConflictRef.current = onRevisionConflict;
|
|
710
|
+
if (initialRevision > revisionRef.current) {
|
|
711
|
+
revisionRef.current = initialRevision;
|
|
712
|
+
}
|
|
713
|
+
implRef.current = async (value: string) => {
|
|
714
|
+
const response = await save({
|
|
715
|
+
documentId,
|
|
716
|
+
propertyId,
|
|
717
|
+
value,
|
|
718
|
+
expectedBlocksFieldRevision: revisionRef.current,
|
|
719
|
+
});
|
|
720
|
+
const nextRevision = (
|
|
721
|
+
response as DocumentPropertiesResponse
|
|
722
|
+
)?.properties?.find((candidate) => candidate.definition.id === propertyId)
|
|
723
|
+
?.blocksField?.revision;
|
|
724
|
+
if (typeof nextRevision === "number") revisionRef.current = nextRevision;
|
|
725
|
+
return response;
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const controllerRef = useRef<BlockFieldSaveController | null>(null);
|
|
678
729
|
|
|
679
730
|
// Build (but do not yet ref-count) the controller factory for this key. The
|
|
680
731
|
// formal acquire/release happens in the effect below; we only need the factory
|
|
@@ -686,12 +737,24 @@ export function useBlockFieldEditor({
|
|
|
686
737
|
// saves across ALL editor instances for the key (there is only ever one
|
|
687
738
|
// controller per key), so no cross-instance serialization lane is needed.
|
|
688
739
|
save: (value) => implRef.current(value),
|
|
689
|
-
onError: (error) =>
|
|
740
|
+
onError: (error) => {
|
|
741
|
+
if (isBlocksFieldRevisionConflict(error)) {
|
|
742
|
+
rejectedRevisionRef.current = revisionRef.current;
|
|
743
|
+
// A stale payload is not a retryable draft. If this editor unmounts
|
|
744
|
+
// before the invalidated query returns, flush must not replay it
|
|
745
|
+
// against the newly advanced revision and overwrite the winner.
|
|
746
|
+
// The request can reject after this hook unmounts. The per-field
|
|
747
|
+
// controller outlives that mount, so clear the rejected payload at
|
|
748
|
+
// the shared boundary instead of relying on the instance ref.
|
|
749
|
+
peekBlockFieldSaveController(key)?.discardPending();
|
|
750
|
+
onRevisionConflictRef.current?.();
|
|
751
|
+
}
|
|
690
752
|
console.error("Failed to save Blocks field content", {
|
|
691
753
|
documentId,
|
|
692
754
|
propertyId,
|
|
693
755
|
error,
|
|
694
|
-
})
|
|
756
|
+
});
|
|
757
|
+
},
|
|
695
758
|
});
|
|
696
759
|
|
|
697
760
|
// Acquire the ONE shared controller for this field key, and release it on
|
|
@@ -706,7 +769,6 @@ export function useBlockFieldEditor({
|
|
|
706
769
|
// `factory`/`implRef` are intentionally not effect deps: the impl ref is updated
|
|
707
770
|
// every render, and the identity key forces a remount for a DIFFERENT key, so
|
|
708
771
|
// within one mount the key is stable and we acquire exactly once.
|
|
709
|
-
const controllerRef = useRef<BlockFieldSaveController | null>(null);
|
|
710
772
|
useEffect(() => {
|
|
711
773
|
controllerRef.current = acquireBlockFieldSaveController(key, factory);
|
|
712
774
|
return () => {
|
|
@@ -741,6 +803,7 @@ export function useBlockFieldEditor({
|
|
|
741
803
|
}
|
|
742
804
|
return initialContent;
|
|
743
805
|
});
|
|
806
|
+
const [editorResetVersion, setEditorResetVersion] = useState(0);
|
|
744
807
|
|
|
745
808
|
// Adopt fresh server content when it is a GENUINELY newer external update (e.g.
|
|
746
809
|
// an agent edit) — not when it is merely stale server props lagging a local
|
|
@@ -761,6 +824,27 @@ export function useBlockFieldEditor({
|
|
|
761
824
|
useEffect(() => {
|
|
762
825
|
const controller = controllerRef.current;
|
|
763
826
|
if (!controller) return;
|
|
827
|
+
const rejectedRevision = rejectedRevisionRef.current;
|
|
828
|
+
if (rejectedRevision !== null && initialRevision > rejectedRevision) {
|
|
829
|
+
if (controller.pending !== controller.lastSaved) {
|
|
830
|
+
// The user typed again while the winner was being refetched. Preserve
|
|
831
|
+
// that newer draft; revisionRef already reflects initialRevision, so
|
|
832
|
+
// its pending save will use the newly accepted boundary.
|
|
833
|
+
rejectedRevisionRef.current = null;
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
// The server rejected our stale revision and the invalidated query has
|
|
837
|
+
// now delivered the winner. The rejected draft is not retryable against
|
|
838
|
+
// the new baseline: adopt the accepted value and clear the dirty latch.
|
|
839
|
+
setContent(initialContent);
|
|
840
|
+
controller.mark(initialContent);
|
|
841
|
+
rejectedRevisionRef.current = null;
|
|
842
|
+
// VisualEditor owns an imperative TipTap instance. Reset it only for this
|
|
843
|
+
// explicit conflict recovery so the accepted server value replaces the
|
|
844
|
+
// rejected draft without disrupting ordinary save cursor/undo state.
|
|
845
|
+
setEditorResetVersion((version) => version + 1);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
764
848
|
// Never adopt over a dirty local edit.
|
|
765
849
|
if (controller.pending !== controller.lastSaved) return;
|
|
766
850
|
if (initialContent === controller.lastSaved) {
|
|
@@ -780,10 +864,10 @@ export function useBlockFieldEditor({
|
|
|
780
864
|
}
|
|
781
865
|
// else: server props are stale, lagging a local save the server hasn't
|
|
782
866
|
// echoed yet. Keep showing lastSaved and wait for the echo above to clear
|
|
783
|
-
// the latch.
|
|
784
|
-
//
|
|
867
|
+
// the latch. Concurrent writes are guarded by the field revision; a
|
|
868
|
+
// rejected stale write follows the explicit conflict branch above.
|
|
785
869
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
786
|
-
}, [initialContent]);
|
|
870
|
+
}, [initialContent, initialRevision]);
|
|
787
871
|
|
|
788
872
|
function onChange(markdown: string) {
|
|
789
873
|
setContent(markdown);
|
|
@@ -792,7 +876,7 @@ export function useBlockFieldEditor({
|
|
|
792
876
|
controllerRef.current?.change(markdown);
|
|
793
877
|
}
|
|
794
878
|
|
|
795
|
-
return { content, onChange };
|
|
879
|
+
return { content, editorResetVersion, onChange };
|
|
796
880
|
}
|
|
797
881
|
|
|
798
882
|
/**
|
|
@@ -811,6 +895,7 @@ function AdditionalBlockEditor({
|
|
|
811
895
|
property: DocumentProperty;
|
|
812
896
|
canEdit: boolean;
|
|
813
897
|
}) {
|
|
898
|
+
const t = useT();
|
|
814
899
|
const setProperty = useSetDocumentProperty(
|
|
815
900
|
documentId,
|
|
816
901
|
property.definition.databaseId!,
|
|
@@ -819,16 +904,19 @@ function AdditionalBlockEditor({
|
|
|
819
904
|
const propertyId = property.definition.id;
|
|
820
905
|
const initialContent =
|
|
821
906
|
typeof property.value === "string" ? property.value : "";
|
|
822
|
-
const { content, onChange } = useBlockFieldEditor({
|
|
907
|
+
const { content, editorResetVersion, onChange } = useBlockFieldEditor({
|
|
823
908
|
documentId,
|
|
824
909
|
propertyId,
|
|
825
910
|
initialContent,
|
|
911
|
+
initialRevision: property.blocksField?.revision ?? 0,
|
|
826
912
|
save: setProperty.mutateAsync,
|
|
913
|
+
onRevisionConflict: () =>
|
|
914
|
+
toast.error(t("editor.blocksFieldRevisionConflict")),
|
|
827
915
|
});
|
|
828
916
|
|
|
829
917
|
return (
|
|
830
918
|
<VisualEditor
|
|
831
|
-
key={propertyId}
|
|
919
|
+
key={`${propertyId}:${editorResetVersion}`}
|
|
832
920
|
documentId={documentId}
|
|
833
921
|
content={content}
|
|
834
922
|
onChange={onChange}
|
|
@@ -40,6 +40,8 @@ export interface BlockFieldSaveController {
|
|
|
40
40
|
cancel(): void;
|
|
41
41
|
/** Adopt `content` as the confirmed-saved baseline (no save scheduled). */
|
|
42
42
|
mark(content: string): void;
|
|
43
|
+
/** Drop a rejected pending write so an unmount flush cannot retry it. */
|
|
44
|
+
discardPending(): void;
|
|
43
45
|
/** The value last CONFIRMED persisted. */
|
|
44
46
|
readonly lastSaved: string;
|
|
45
47
|
/** The latest value the user has typed (may differ from lastSaved). */
|
|
@@ -161,6 +163,10 @@ export function createBlockFieldSaveController(args: {
|
|
|
161
163
|
// hasn't echoed — so server props are no longer "behind" this controller.
|
|
162
164
|
hasSavedLocally = false;
|
|
163
165
|
},
|
|
166
|
+
discardPending() {
|
|
167
|
+
clearTimer();
|
|
168
|
+
pending = lastSaved;
|
|
169
|
+
},
|
|
164
170
|
get lastSaved() {
|
|
165
171
|
return lastSaved;
|
|
166
172
|
},
|
|
@@ -1631,6 +1631,11 @@ function DatabaseTable({
|
|
|
1631
1631
|
) {
|
|
1632
1632
|
if (!databaseId) return null;
|
|
1633
1633
|
if (isWorkspaceCatalog) return null;
|
|
1634
|
+
const mutationContract = data?.mutationContract;
|
|
1635
|
+
if (!mutationContract) {
|
|
1636
|
+
toast.error(dbText("failedToCreateRow"));
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1634
1639
|
const propertyValues = {
|
|
1635
1640
|
...databasePropertyValuesForNewItem(filters, properties, filterMode),
|
|
1636
1641
|
...propertyValueOverrides,
|
|
@@ -1638,8 +1643,10 @@ function DatabaseTable({
|
|
|
1638
1643
|
let response;
|
|
1639
1644
|
try {
|
|
1640
1645
|
response = await addItem.mutateAsync({
|
|
1641
|
-
|
|
1642
|
-
|
|
1646
|
+
target: mutationContract.target,
|
|
1647
|
+
expectedSchemaRevision: mutationContract.schemaRevision,
|
|
1648
|
+
idempotencyKey: crypto.randomUUID(),
|
|
1649
|
+
...(title.trim() ? { title } : {}),
|
|
1643
1650
|
propertyValues:
|
|
1644
1651
|
Object.keys(propertyValues).length > 0 ? propertyValues : undefined,
|
|
1645
1652
|
});
|
|
@@ -1650,12 +1657,7 @@ function DatabaseTable({
|
|
|
1650
1657
|
});
|
|
1651
1658
|
return null;
|
|
1652
1659
|
}
|
|
1653
|
-
const createdItem =
|
|
1654
|
-
databaseId,
|
|
1655
|
-
parentDocument: document,
|
|
1656
|
-
title,
|
|
1657
|
-
propertyValues,
|
|
1658
|
-
});
|
|
1660
|
+
const createdItem = response.createdItem ?? null;
|
|
1659
1661
|
const needsPreview =
|
|
1660
1662
|
!!createdItem &&
|
|
1661
1663
|
databaseCreatedItemNeedsPreview(items, createdItem, options);
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
CancelPreparedBuilderSourceUpdateResponse,
|
|
13
13
|
ChangeContentDatabaseSourceRoleRequest,
|
|
14
14
|
ContentDatabaseResponse,
|
|
15
|
+
ContentDatabaseRowMutationResult,
|
|
15
16
|
ContentDatabaseSourceAttachmentAck,
|
|
16
17
|
ContentDatabaseSourceAttachmentResult,
|
|
17
18
|
ContentDatabaseItemsPageResponse,
|
|
@@ -819,40 +820,40 @@ export function useTrashedContentDatabases() {
|
|
|
819
820
|
|
|
820
821
|
export function useAddDatabaseItem(documentId: string) {
|
|
821
822
|
const queryClient = useQueryClient();
|
|
822
|
-
return useActionMutation<
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
)
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
823
|
+
return useActionMutation<
|
|
824
|
+
ContentDatabaseRowMutationResult,
|
|
825
|
+
AddDatabaseItemRequest
|
|
826
|
+
>("add-database-item", {
|
|
827
|
+
skipActionQueryInvalidation: true,
|
|
828
|
+
onSuccess: (data) => {
|
|
829
|
+
if (data.createdItem) {
|
|
830
|
+
queryClient.setQueriesData<ContentDatabaseResponse>(
|
|
831
|
+
{
|
|
832
|
+
queryKey: ["action", "get-content-database"],
|
|
833
|
+
predicate: (query) => {
|
|
834
|
+
if (
|
|
835
|
+
!isContentDatabaseQueryForDocument(query.queryKey, documentId)
|
|
836
|
+
) {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
const params = query.queryKey[2] as {
|
|
840
|
+
tableQuery?: unknown;
|
|
841
|
+
};
|
|
842
|
+
return params.tableQuery === undefined;
|
|
842
843
|
},
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
844
|
+
},
|
|
845
|
+
(current) =>
|
|
846
|
+
applyOptimisticItemToContentDatabase(current, data.createdItem!),
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
queryClient.invalidateQueries({
|
|
850
|
+
queryKey: contentDatabaseQueryKey(documentId),
|
|
851
|
+
});
|
|
852
|
+
queryClient.invalidateQueries({
|
|
853
|
+
queryKey: ["action", "list-documents"],
|
|
854
|
+
});
|
|
854
855
|
},
|
|
855
|
-
);
|
|
856
|
+
});
|
|
856
857
|
}
|
|
857
858
|
|
|
858
859
|
export function useSubmitContentDatabaseForm(documentId: string) {
|
|
@@ -132,7 +132,7 @@ export function useSetDocumentProperty(
|
|
|
132
132
|
);
|
|
133
133
|
return { previous };
|
|
134
134
|
},
|
|
135
|
-
onError: (_error,
|
|
135
|
+
onError: (_error, variables, context) => {
|
|
136
136
|
const rollback = context as
|
|
137
137
|
| {
|
|
138
138
|
previous?: Array<[readonly unknown[], unknown]>;
|
|
@@ -141,6 +141,9 @@ export function useSetDocumentProperty(
|
|
|
141
141
|
for (const [queryKey, data] of rollback?.previous ?? []) {
|
|
142
142
|
queryClient.setQueryData(queryKey, data);
|
|
143
143
|
}
|
|
144
|
+
queryClient.invalidateQueries({
|
|
145
|
+
queryKey: documentPropertiesQueryKey(variables.documentId, databaseId),
|
|
146
|
+
});
|
|
144
147
|
},
|
|
145
148
|
onSuccess: (data, variables) => {
|
|
146
149
|
const savedValue =
|
|
@@ -3119,6 +3119,8 @@ const enUS = {
|
|
|
3119
3119
|
searchLanguages: "Search languages...",
|
|
3120
3120
|
noBlocksFields: "No Blocks fields. Add one from the property menu.",
|
|
3121
3121
|
noDocumentSelected: "No document selected",
|
|
3122
|
+
blocksFieldRevisionConflict:
|
|
3123
|
+
"This Blocks field changed elsewhere. Your edit wasn't saved; the latest version is now shown.",
|
|
3122
3124
|
couldNotReadLocalSourceFile: "Could not read local source file",
|
|
3123
3125
|
couldNotSaveLocalFile: "Could not save local file",
|
|
3124
3126
|
collabConnectingReadOnly:
|
|
@@ -5619,6 +5621,8 @@ const databaseExactEnglishMessagesByLocale = {
|
|
|
5619
5621
|
const editorMessagesByLocale = {
|
|
5620
5622
|
"zh-CN": {
|
|
5621
5623
|
noDocumentSelected: "未选择文档",
|
|
5624
|
+
blocksFieldRevisionConflict:
|
|
5625
|
+
"此 Blocks 字段已在其他位置更改。你的编辑未保存;现已显示最新版本。",
|
|
5622
5626
|
collabConnectingReadOnly: "正在连接实时编辑器。显示只读快照。",
|
|
5623
5627
|
liveDocumentSaveBeforeSyncFailed: "实时文档无法在同步前保存。",
|
|
5624
5628
|
builderBodySyncing: "内容仍在从 Builder 同步",
|
|
@@ -5966,6 +5970,8 @@ const editorMessagesByLocale = {
|
|
|
5966
5970
|
},
|
|
5967
5971
|
"es-ES": {
|
|
5968
5972
|
noDocumentSelected: "Ningún documento seleccionado",
|
|
5973
|
+
blocksFieldRevisionConflict:
|
|
5974
|
+
"Este campo de bloques cambió en otro lugar. Tu edición no se guardó; ahora se muestra la versión más reciente.",
|
|
5969
5975
|
collabConnectingReadOnly:
|
|
5970
5976
|
"Conectando el editor en vivo. Mostrando una instantánea de solo lectura.",
|
|
5971
5977
|
liveDocumentSaveBeforeSyncFailed:
|
|
@@ -6326,6 +6332,8 @@ const editorMessagesByLocale = {
|
|
|
6326
6332
|
},
|
|
6327
6333
|
"fr-FR": {
|
|
6328
6334
|
noDocumentSelected: "Aucun document sélectionné",
|
|
6335
|
+
blocksFieldRevisionConflict:
|
|
6336
|
+
"Ce champ de blocs a été modifié ailleurs. Votre modification n’a pas été enregistrée ; la dernière version est maintenant affichée.",
|
|
6329
6337
|
collabConnectingReadOnly:
|
|
6330
6338
|
"Connexion de l'éditeur en direct. Affichage d'un instantané en lecture seule.",
|
|
6331
6339
|
liveDocumentSaveBeforeSyncFailed:
|
|
@@ -6691,6 +6699,8 @@ const editorMessagesByLocale = {
|
|
|
6691
6699
|
},
|
|
6692
6700
|
"de-DE": {
|
|
6693
6701
|
noDocumentSelected: "Kein Dokument ausgewählt",
|
|
6702
|
+
blocksFieldRevisionConflict:
|
|
6703
|
+
"Dieses Blocks-Feld wurde an anderer Stelle geändert. Deine Bearbeitung wurde nicht gespeichert; jetzt wird die neueste Version angezeigt.",
|
|
6694
6704
|
collabConnectingReadOnly:
|
|
6695
6705
|
"Live-Editor wird verbunden. Schreibgeschützte Momentaufnahme wird angezeigt.",
|
|
6696
6706
|
liveDocumentSaveBeforeSyncFailed:
|
|
@@ -7059,6 +7069,8 @@ const editorMessagesByLocale = {
|
|
|
7059
7069
|
},
|
|
7060
7070
|
"ja-JP": {
|
|
7061
7071
|
noDocumentSelected: "ドキュメントが選択されていません",
|
|
7072
|
+
blocksFieldRevisionConflict:
|
|
7073
|
+
"このブロックフィールドは別の場所で変更されました。編集内容は保存されず、最新バージョンが表示されています。",
|
|
7062
7074
|
collabConnectingReadOnly:
|
|
7063
7075
|
"ライブエディターに接続中。読み取り専用のスナップショットを表示しています。",
|
|
7064
7076
|
liveDocumentSaveBeforeSyncFailed:
|
|
@@ -7416,6 +7428,8 @@ const editorMessagesByLocale = {
|
|
|
7416
7428
|
},
|
|
7417
7429
|
"ko-KR": {
|
|
7418
7430
|
noDocumentSelected: "선택한 문서가 없습니다.",
|
|
7431
|
+
blocksFieldRevisionConflict:
|
|
7432
|
+
"이 블록 필드가 다른 곳에서 변경되었습니다. 편집 내용은 저장되지 않았으며 최신 버전이 표시됩니다.",
|
|
7419
7433
|
collabConnectingReadOnly:
|
|
7420
7434
|
"라이브 편집기에 연결하는 중입니다. 읽기 전용 스냅샷을 표시합니다.",
|
|
7421
7435
|
liveDocumentSaveBeforeSyncFailed:
|
|
@@ -7772,6 +7786,8 @@ const editorMessagesByLocale = {
|
|
|
7772
7786
|
},
|
|
7773
7787
|
"pt-BR": {
|
|
7774
7788
|
noDocumentSelected: "Nenhum documento selecionado",
|
|
7789
|
+
blocksFieldRevisionConflict:
|
|
7790
|
+
"Este campo de blocos foi alterado em outro lugar. Sua edição não foi salva; a versão mais recente agora está sendo exibida.",
|
|
7775
7791
|
collabConnectingReadOnly:
|
|
7776
7792
|
"Conectando o editor ao vivo. Exibindo um instantâneo somente leitura.",
|
|
7777
7793
|
liveDocumentSaveBeforeSyncFailed:
|
|
@@ -8134,6 +8150,8 @@ const editorMessagesByLocale = {
|
|
|
8134
8150
|
},
|
|
8135
8151
|
"hi-IN": {
|
|
8136
8152
|
noDocumentSelected: "कोई दस्तावेज़ चयनित नहीं",
|
|
8153
|
+
blocksFieldRevisionConflict:
|
|
8154
|
+
"यह ब्लॉक फ़ील्ड कहीं और बदल गया है। आपका संपादन सहेजा नहीं गया; अब नवीनतम संस्करण दिखाया जा रहा है।",
|
|
8137
8155
|
collabConnectingReadOnly:
|
|
8138
8156
|
"लाइव संपादक कनेक्ट हो रहा है। केवल-पठन स्नैपशॉट दिखाया जा रहा है।",
|
|
8139
8157
|
liveDocumentSaveBeforeSyncFailed:
|
|
@@ -8486,6 +8504,8 @@ const editorMessagesByLocale = {
|
|
|
8486
8504
|
},
|
|
8487
8505
|
"ar-SA": {
|
|
8488
8506
|
noDocumentSelected: "لم يتم تحديد أي مستند",
|
|
8507
|
+
blocksFieldRevisionConflict:
|
|
8508
|
+
"تم تغيير حقل الكتل هذا في مكان آخر. لم يتم حفظ تعديلك؛ ويظهر الآن أحدث إصدار.",
|
|
8489
8509
|
collabConnectingReadOnly:
|
|
8490
8510
|
"جارٍ الاتصال بالمحرر المباشر. يتم عرض لقطة للقراءة فقط.",
|
|
8491
8511
|
liveDocumentSaveBeforeSyncFailed: "تعذّر حفظ المستند المباشر قبل المزامنة.",
|
|
@@ -6,7 +6,7 @@ name: "Blocks"
|
|
|
6
6
|
user_promise: "A Block is a stable addressable unit of rich content inside its owning field."
|
|
7
7
|
primary_user_job: "Edit and point to a meaningful part of content without fragile position-only anchors."
|
|
8
8
|
kind: "primitive"
|
|
9
|
-
state: "
|
|
9
|
+
state: "in_progress"
|
|
10
10
|
publicness: "public"
|
|
11
11
|
availability: "universal"
|
|
12
12
|
dependencies: []
|
|
@@ -21,9 +21,10 @@ proof_requirements:
|
|
|
21
21
|
"Comment anchors retain historical target context",
|
|
22
22
|
"Shared Action/UI editing, conflict, undo, and reload behavior",
|
|
23
23
|
]
|
|
24
|
-
evidence:
|
|
24
|
+
evidence:
|
|
25
|
+
["shared/blocks-field-identity.ts", "actions/blocks-seeding.db.test.ts"]
|
|
25
26
|
superseded_by: null
|
|
26
|
-
last_reviewed: "2026-
|
|
27
|
+
last_reviewed: "2026-08-10"
|
|
27
28
|
---
|
|
28
29
|
|
|
29
30
|
# Blocks
|
|
@@ -62,7 +63,7 @@ Given a Block reference in another Page, when an authorized reader opens it, the
|
|
|
62
63
|
|
|
63
64
|
## Current evidence
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
Database Blocks fields now have a field-scoped ordered identity sidecar with deterministic legacy IDs, persisted revisions, and bounded tombstone recovery. Deterministic tests cover editing, reorder, insertion, deletion, recovery, reload, and field independence. Agent-facing exact Block actions, reference/comment anchors, actor-aware history, and real-interface proof remain incomplete, so this is `in_progress`, not verified.
|
|
66
67
|
|
|
67
68
|
## Proof plan
|
|
68
69
|
|
|
@@ -6,7 +6,7 @@ name: "Blocks fields"
|
|
|
6
6
|
user_promise: "Every editable rich-content body uses one Blocks-field grammar and keeps its own stable revision boundary."
|
|
7
7
|
primary_user_job: "Write rich content in Pages and collaboration surfaces without each body inventing incompatible editing and history rules."
|
|
8
8
|
kind: "primitive"
|
|
9
|
-
state: "
|
|
9
|
+
state: "in_progress"
|
|
10
10
|
publicness: "public"
|
|
11
11
|
availability: "universal"
|
|
12
12
|
dependencies: ["content.object.block"]
|
|
@@ -24,9 +24,14 @@ proof_requirements:
|
|
|
24
24
|
"Owner-scoped access and typed rendering including unavailable content",
|
|
25
25
|
"Shared Action/UI behavior for concurrency, history, and portable output",
|
|
26
26
|
]
|
|
27
|
-
evidence:
|
|
27
|
+
evidence:
|
|
28
|
+
[
|
|
29
|
+
"server/db/schema.ts",
|
|
30
|
+
"actions/_blocks-field-identity.ts",
|
|
31
|
+
"actions/blocks-seeding.db.test.ts",
|
|
32
|
+
]
|
|
28
33
|
superseded_by: null
|
|
29
|
-
last_reviewed: "2026-
|
|
34
|
+
last_reviewed: "2026-08-10"
|
|
30
35
|
---
|
|
31
36
|
|
|
32
37
|
# Blocks fields
|
|
@@ -65,7 +70,7 @@ Given a Page with two Blocks fields, when an authorized editor restores one fiel
|
|
|
65
70
|
|
|
66
71
|
## Current evidence
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
Primary and additional database Blocks properties now retain distinct field identities, ordered Block identities, and independent monotonic revisions around their existing Markdown stores. Export reports each field and its identity status without changing plain NFM. Comment/Discussion owners, attributable history, arbitrary restore, shared mutation actions, and real-interface proof remain incomplete, so this is `in_progress`, not verified.
|
|
69
74
|
|
|
70
75
|
## Proof plan
|
|
71
76
|
|
|
@@ -16,8 +16,8 @@ This index summarizes the atomic product contracts beneath the public roadmap. E
|
|
|
16
16
|
| Verified | 3 |
|
|
17
17
|
| Failing | 1 |
|
|
18
18
|
| Stale | 0 |
|
|
19
|
-
| In Progress |
|
|
20
|
-
| Approved Shape |
|
|
19
|
+
| In Progress | 18 |
|
|
20
|
+
| Approved Shape | 89 |
|
|
21
21
|
| Exploring | 8 |
|
|
22
22
|
| Deferred | 0 |
|
|
23
23
|
| Superseded | 5 |
|
|
@@ -358,8 +358,8 @@ graph LR
|
|
|
358
358
|
|
|
359
359
|
| Capability | State | User promise |
|
|
360
360
|
| -------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
361
|
-
| [Blocks](capabilities/content.object.block.md) |
|
|
362
|
-
| [Blocks fields](capabilities/content.object.blocks-field.md) |
|
|
361
|
+
| [Blocks](capabilities/content.object.block.md) | In Progress | A Block is a stable addressable unit of rich content inside its owning field. |
|
|
362
|
+
| [Blocks fields](capabilities/content.object.blocks-field.md) | In Progress | Every editable rich-content body uses one Blocks-field grammar and keeps its own stable revision boundary. |
|
|
363
363
|
| [Databases](capabilities/content.object.database.md) | Verified | Database as a Page-backed typed collection |
|
|
364
364
|
| [Multiple Database memberships](capabilities/content.object.multi-membership.md) | In Progress | One Page can belong to several Databases without copies or a hidden primary home. |
|
|
365
365
|
| [Pages](capabilities/content.object.page.md) | Verified | A durable Page keeps its identity, body, properties, access, discussion, and portable representation wherever it appears. |
|
|
@@ -9,7 +9,7 @@ This generated matrix tracks whether high-value Content UI operations use the sa
|
|
|
9
9
|
| database.lifecycle-and-trash | database | Create, soft-delete, restore, list, and inspect content databases | action-backed | `create-content-database`, `create-inline-content-database`, `delete-content-database`, `get-content-database`, `list-content-databases`, `list-trashed-content-databases`, `restore-content-database` | `app/components/editor/SlashCommandMenu.tsx`, `app/hooks/use-content-database.ts`, `app/hooks/use-documents.ts` | Database pages and database records are created, read, soft-deleted, restored, and listed. | - | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts` | `database-source-scope` | - |
|
|
10
10
|
| database.private-preview-drafts | database | Persist and reconcile a user's private database-page preview draft | action-backed | `get-preview-document-draft`, `update-preview-document-draft` | `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-documents.ts` | A user's private preview draft is read, saved, conflict-checked, or deleted without changing the shared database page until the normal save flow applies it. | These per-user editor-state actions are intentionally hidden from agent tools because preview drafts are a private UI recovery mechanism. | - | P1 | covered | `actions/preview-document-draft.db.test.ts` | - | - |
|
|
11
11
|
| database.properties-and-view-config | database | Configure properties, values, ordering, and saved views | action-backed | `configure-document-property`, `delete-document-property`, `duplicate-document-property`, `get-content-database-personal-view`, `list-document-properties`, `reorder-document-property`, `set-document-property`, `update-content-database-personal-view`, `update-content-database-view` | `app/components/editor/DocumentProperties.tsx`, `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-document-properties.ts` | Property schemas, property values, property order, filters, sorts, grouping, hidden columns, view type, and view settings are stored. | - | - | P0 | covered | `actions/bind-content-database-source-field.db.test.ts`, `actions/content-database-source-actions.test.ts`, `actions/resync-content-database-source.db.test.ts` | `database-source-scope` | - |
|
|
12
|
-
| database.rows | database | Add, duplicate, move, open, edit, and remove database rows | action-backed | `add-database-item`, `upsert-database-item-by-key`, `remove-database-items`, `duplicate-database-items`, `duplicate-database-item`, `migrate-content-database-rows`, `move-database-item`, `set-document-property`
|
|
12
|
+
| database.rows | database | Add, duplicate, move, open, edit, and remove database rows | action-backed | `add-database-item`, `update-database-item`, `upsert-database-item-by-key`, `remove-database-items`, `duplicate-database-items`, `duplicate-database-item`, `migrate-content-database-rows`, `move-database-item`, `set-document-property` | `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx` | Database row memberships and ordering are created, duplicated, moved, edited, and removed without deleting the backing page; bounded migrations atomically update row bodies and properties through the same canonical data model. | - | - | P0 | covered | `actions/database-row-batch-actions.db.test.ts`, `actions/upsert-database-item-by-key.db.test.ts`, `actions/migrate-content-database-rows.db.test.ts`, `parity/__tests__/database-row-batch-reliability.test.ts` | `database-bulk-row-reliability` | - |
|
|
13
13
|
| database.table-query-page | database | Query one constrained page while retaining database metadata | action-backed | `query-content-database-items` | `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-content-database.ts` | - | This UI-only bounded projection is intentionally hidden with agentTool: false; agents use get-content-database for the complete database contract. | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts`, `app/hooks/use-content-database.test.ts` | - | - |
|
|
14
14
|
| editor.agent-assist-prompts | editor | Ask AI from slash generation or comment context | client-assist | - | `app/components/editor/SlashCommandMenu.tsx`, `app/components/editor/CommentsSidebar.tsx` | No direct durable mutation; the prompt asks the agent to use document actions when it decides to write. | - | - | P1 | none | - | - | - |
|
|
15
15
|
| editor.client-formatting-and-insertions | editor | Rich text formatting, selection state, slash block insertion, and copy actions | client-only-ephemeral | - | `app/components/editor/BubbleToolbar.tsx`, `app/components/editor/SlashCommandMenu.tsx`, `app/components/editor/DocumentToolbar.tsx` | - | - | - | P1 | none | - | - | - |
|
|
@@ -345,6 +345,7 @@ export const parityMatrix: ParityRow[] = [
|
|
|
345
345
|
status: "action-backed",
|
|
346
346
|
actions: [
|
|
347
347
|
"add-database-item",
|
|
348
|
+
"update-database-item",
|
|
348
349
|
"upsert-database-item-by-key",
|
|
349
350
|
"remove-database-items",
|
|
350
351
|
"duplicate-database-items",
|
|
@@ -360,6 +361,7 @@ export const parityMatrix: ParityRow[] = [
|
|
|
360
361
|
followUpPR: null,
|
|
361
362
|
coverageRefs: [
|
|
362
363
|
"actions/database-row-batch-actions.db.test.ts",
|
|
364
|
+
"actions/upsert-database-item-by-key.db.test.ts",
|
|
363
365
|
"actions/migrate-content-database-rows.db.test.ts",
|
|
364
366
|
"parity/__tests__/database-row-batch-reliability.test.ts",
|
|
365
367
|
],
|