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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ Include only the fields that help the reviewer:

## Loom video

<!-- Include a short Loom video for every PR (any change—UI, backend, refactors, infra, etc.).
<!-- Include a short Loom video for every PR (any change—UI, backend, refactors, infra, etc.).
This helps reviewers understand intent quickly and catches issues earlier
- Keep it under 2–3 minutes
- Show before/after for bug fixes
- Narrate key design or dev decisions
- Paste the Loom link in the PR body
-->


## Scope check

- [ ] Ran `$scope-check` against the ENG ticket and final diff.
Expand Down
6 changes: 5 additions & 1 deletion apps/roam/src/utils/__tests__/conceptConversion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ describe("discourseNodeSchemaToLocalConcept source slot", () => {
nodeType({ text: "Claim", type: "clm", format: "[[CLM]] - {content}" }),
);
expect(concept.local_reference_content).toBeUndefined();
expect(concept.literal_content).toEqual({ label: "Claim" });
expect(concept.literal_content).toEqual({
label: "Claim",
format: "[[CLM]] - {content}",
});
});

it("keeps the label and template it already carried", () => {
Expand All @@ -102,6 +105,7 @@ describe("discourseNodeSchemaToLocalConcept source slot", () => {
);
expect(concept.literal_content).toEqual({
label: "Evidence",
format: "[[EVD]] - {content} - {Source}",
template: "* Question:\n",
roles: ["sourceDocument"],
});
Expand Down
2 changes: 2 additions & 0 deletions apps/roam/src/utils/__tests__/publishNodesToGroups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ vi.mock("~/utils/roamToCrossAppConverters", () => ({
label: s.text,
authorId: "author-1",
createdAt: new Date("2026-01-01T00:00:00.000Z"),
format: s.format,
}),
reifiedRelationToCrossApp: vi.fn(),
relationTripleSchemaToCrossApp: vi.fn(),
Expand Down Expand Up @@ -172,6 +173,7 @@ describe("publishNodesToGroups", () => {
source_local_id: SCHEMA_UID,
is_schema: true,
name: "Claim",
literal_content: { format: "[[CLM]] - {content}" },
});
expect(data[1]).toMatchObject({
source_local_id: "node-1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ describe("nodeSchemaToCrossApp timestamps", () => {
});
});

describe("nodeSchemaToCrossApp format", () => {
it("carries the node type format", () => {
const schema = convertSchemaPull(schemaPull);
expect(schema?.format).toBe("[[EVD]] - {content} - {Source}");
});
});

describe("nodeSchemaToCrossApp source slot", () => {
it("adds a sourceDocument slot definition pointing at the Source node type", () => {
mockedGetDiscourseNodes.mockReturnValue([
Expand Down
5 changes: 4 additions & 1 deletion apps/roam/src/utils/conceptConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ export const discourseNodeSchemaToLocalConcept = (
): LocalConceptDataInput => {
const titleParts = node.text.split("/");
const label = titleParts[titleParts.length - 1] ?? node.text;
const literalContent: Record<string, Json> = { label };
const literalContent: Record<string, Json> = {
label,
format: node.format,
};
if (node.template !== undefined)
literalContent.template = templateToText(node.template);
const hasSourceSlot = schemaHasSourceSlot(node);
Expand Down
1 change: 1 addition & 0 deletions apps/roam/src/utils/roamToCrossAppConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export const nodeSchemaToCrossApp = (
authorId: userUid,
createdAt: new Date(createdTime),
modifiedAt: new Date(Math.max(pageEditTime, createdTime)),
format: s.format,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiscourseNode.format is marked deprecated in favor of specification, but that deprecation is about node identification. format is still the title pattern, and the title pattern is what ENG-2156 and ENG-2157 read from the schema row.

Comment thread
sid597 marked this conversation as resolved.
...(hasSourceSlot
? { slotDefinitions: { [SOURCE_SLOT]: sourceSlotSchemaId() } }
: {}),
Expand Down
2 changes: 0 additions & 2 deletions apps/website/content/roam/guides/using-the-canvas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,8 @@ To create a relation, click the discourse graph symbol in the lower toolbar and
/>
_Using the relation menu_


You can also create a relation on your canvas by clicking on the origin node until the grey drag handles appear and dragging the relation arrow to the target node. You will be prompted to select from one of the available relation types for those nodes.


<Image
src="/docs/roam/canvas-tut22.gif"
alt="dragging relations"
Expand Down
1 change: 1 addition & 0 deletions packages/database/doc/concept_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Residual (not otherwise accounted for) keys in Obsidian frontmatter are mapped t
| `label` | `name` | {} |
| `template` | `literal_content->template_content` | |
| `templateTitle` | `literal_content->template` | |
| `format` | `literal_content->format` | |
| - | `is_schema` | true |
| - | `schema_id` | null |
| - | `arity` | 0 |
Expand Down
1 change: 1 addition & 0 deletions packages/database/src/crossAppContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type CrossAppNodeSchema = CrossAppSchemaBase & {
label: string;
template?: string;
templateTitle?: string;
format?: string;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flat literal_content.format, per the decision on the ticket (MG, team chat 2026-08-19). Obsidian's schema parser already reads flat format as a fallback, so the Obsidian producer stays unchanged. A separate key also avoids template, which the sync producer already uses for the block template.

slotDefinitions?: Record<string, LocalId | undefined>;
};

Expand Down
29 changes: 29 additions & 0 deletions packages/database/src/lib/__tests__/crossAppConverters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,35 @@ const baseNode: CrossAppNode = {
};

describe("crossAppNodeSchemaToDbConcept", () => {
it("maps format to literal_content.format", () => {
const concept = crossAppNodeSchemaToDbConcept({
...baseSchema,
format: "[[CLM]] - {content}",
});
expect(concept.literal_content).toEqual({
format: "[[CLM]] - {content}",
});
});

it("keeps the template keys alongside format", () => {
const concept = crossAppNodeSchemaToDbConcept({
...baseSchema,
format: "[[CLM]] - {content}",
template: "* Evidence\n",
templateTitle: "Claim template",
});
expect(concept.literal_content).toEqual({
format: "[[CLM]] - {content}",
template: "Claim template",
template_content: "* Evidence\n",
});
});

it("omits literal_content when no keys are set", () => {
const concept = crossAppNodeSchemaToDbConcept(baseSchema);
expect(concept.literal_content).toBeUndefined();
});

it("stores slot definitions as roles plus local reference content", () => {
const result = crossAppNodeSchemaToDbConcept({
...baseSchema,
Expand Down
1 change: 1 addition & 0 deletions packages/database/src/lib/crossAppConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const crossAppNodeSchemaToDbConcept = (
const literalInfo = filterUndefined({
template: node.templateTitle,
template_content: node.template,
format: node.format,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The publish path writes only format here, no label or template. That asymmetry with the sync producer is pre-existing: publishNodesToGroups only upserts schemas that are not yet in my_concepts, and the next periodic sync rewrites the row with the full key set. Unifying the two shapes is out of scope for this ticket.

roles: slots.length > 0 ? slots : undefined,
});
const referenceContent = slots.length ? node.slotDefinitions : undefined;
Expand Down