Skip to content
Merged

Dev #275

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
44 changes: 44 additions & 0 deletions .agents/skills/docs-write-guide/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: write-guide
description: |
Generates technical guides that teach real-world use cases through progressive examples.

**Auto-activation:** User asks to write, create, or draft a guide or tutorial. Also use when converting feature documentation, API references, or skill knowledge into step-by-step learning content.

**Input sources:** API documentation, existing code examples, or user-provided specifications.
---

# Writing Guides

## Goal

Produce a technical guide that teaches a real-world use case through progressive examples. Concepts are introduced only when the reader needs them.

Each guide solves **one specific problem**. Not a category of problems. If the outline has 5+ steps or covers multiple approaches, split it.

### Workflow

1. **Research**: Read existing docs for context and linking opportunities.
2. **Plan**: Outline sections. Verify scope. Each step needs a friction point and resolution.
3. **Write**: Follow the template above. Apply the rules below.
4. **Review**: Re-read the rules, verify, then present.

## Rules

1. **Progressive disclosure.** Start with the smallest working example. Introduce complexity only when the example breaks. Name concepts at the moment of resolution, after the reader has felt the problem. Full loop: working → new requirement → something breaks → explain why → name the fix → apply → verify with proof → move on.
2. **One friction point per step.** If a step has multiple friction points, split it.
3. **No em dashes.** Use periods, commas, or parentheses instead.
4. **Mechanical, observable language.** Describe what happens, not how it feels.
5. **No selling, justifying, or comparing.** No "the best way," no historical context, no framework comparisons.

| Don't | Do |
| ---------------------------------------------------- | -------------------------------------------------------- |
| "creates friction in the pipeline" | "blocks the response" |
| "needs dynamic information" | "depends on request-time data" |
| "requires dynamic processing" | "output can't be known ahead of time" |
| "The component blocks the response — causing delays" | "The component blocks the response. This causes delays." |


## References

We write docs on `apps/web/content/docs/**/*`, any reference point or example can be found there. Any new docs should also be added there.
28 changes: 26 additions & 2 deletions packages/ducjs/src/restore/restoreElements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,12 +751,36 @@ const restoreElement = (
case "doc": {
const docElement = element as DucDocElement;

// Migration: old format stored the compiled PDF on fileId and the Typst
// source in referencedFileIds. New format stores the Typst source on
// fileId and the PDF cache in referencedFileIds with a doc_pdf_cache_ prefix.
let migratedFileId = (isValidString(docElement.fileId) as ExternalFileId) || null;
let migratedReferencedFileIds = docElement.referencedFileIds ?? [];

if (migratedFileId && typeof migratedFileId === "string" && migratedFileId.startsWith("doc_pdf_")) {
const typstSourceId = migratedReferencedFileIds.find(
(rid) => typeof rid === "string" && rid.startsWith("doc_typst_source_"),
);
if (typstSourceId) {
// Move old PDF cache to referencedFileIds with new prefix
const elementId = docElement.id ?? "";
const newCacheId = `doc_pdf_cache_${elementId}` as ExternalFileId;
migratedReferencedFileIds = migratedReferencedFileIds.filter(
(rid) => rid !== typstSourceId,
);
if (!migratedReferencedFileIds.includes(newCacheId)) {
migratedReferencedFileIds = [...migratedReferencedFileIds, newCacheId];
}
migratedFileId = typstSourceId as ExternalFileId;
}
}

return restoreElementWithProperties(
element,
{
text: isValidString(docElement.text),
fileId: (isValidString(docElement.fileId) as ExternalFileId) || null,
referencedFileIds: docElement.referencedFileIds || [],
fileId: migratedFileId,
referencedFileIds: migratedReferencedFileIds,
gridConfig: restoreDocumentGridConfig(docElement.gridConfig),
},
localState
Expand Down
90 changes: 60 additions & 30 deletions packages/ducjs/src/types/elements/typeChecks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,34 @@ import { assertNever } from "../../utils";
import { Bounds, LineSegment, TuplePoint } from "../geometry.types";
import type { MarkNonNullable } from "../utility.types";
import type {
DucArrowElement,
DucBindableElement,
DucDocElement,
DucElbowArrowElement,
DucElement,
DucElementType,
DucEllipseElement,
DucEmbeddableElement,
DucFlowchartNodeElement,
DucFrameElement,
DucFrameLikeElement,
DucFreeDrawElement,
DucIframeLikeElement,
DucImageElement,
DucLinearElement,
DucNonSelectionElement,
DucPdfElement,
DucPlotElement,
DucPointBinding,
DucPolygonElement,
DucTableElement,
DucTextContainer,
DucTextElement,
DucTextElementWithContainer,
FixedPointBinding,
InitializedDucImageElement,
NonDeleted
DucArrowElement,
DucBindableElement,
DucDocElement,
DucElbowArrowElement,
DucElement,
DucElementType,
DucEllipseElement,
DucEmbeddableElement,
DucFlowchartNodeElement,
DucFrameElement,
DucFrameLikeElement,
DucFreeDrawElement,
DucIframeLikeElement,
DucImageElement,
DucLinearElement,
DucNonSelectionElement,
DucPdfElement,
DucPlotElement,
DucPointBinding,
DucPolygonElement,
DucTableElement,
DucTextContainer,
DucTextElement,
DucTextElementWithContainer,
ExternalFileId,
FixedPointBinding,
InitializedDucImageElement,
NonDeleted
} from "./";

export const isInitializedImageElement = (
Expand Down Expand Up @@ -58,6 +59,35 @@ export const isPdfLikeElement = (
return !!element && (element.type === "pdf" || element.type === "doc");
};

/**
* Resolves the file ID used for rendering a PDF-like element.
*
* For `pdf` elements, this is simply `element.fileId`.
* For `doc` elements, the `fileId` holds the Typst source file, and the
* compiled PDF cache is stored in `referencedFileIds` with a
* `doc_pdf_cache_` prefix. This function returns that cache file ID, or
* falls back to `element.fileId` for backward compatibility with older
* drawings where `fileId` was the PDF itself.
*/
const DOC_PDF_CACHE_FILE_ID_PREFIX = "doc_pdf_cache_";

export const getRenderablePdfFileId = (
element: DucElement,
): ExternalFileId | null => {
if (element.type === "pdf") {
return element.fileId;
}
if (element.type === "doc") {
const cacheId = `${DOC_PDF_CACHE_FILE_ID_PREFIX}${element.id}` as ExternalFileId;
if (element.referencedFileIds?.includes(cacheId)) {
return cacheId;
}
// Fallback for legacy drawings where fileId was the PDF
return element.fileId;
}
return null;
};

export const isEmbeddableElement = (
element: DucElement | null | undefined,
): element is DucEmbeddableElement => {
Expand Down Expand Up @@ -284,9 +314,9 @@ export const isDucElement = (
case "model":
case "plot":
case "pdf":
{
return true;
}
{
return true;
}
default: {
assertNever(type, null);
return false;
Expand Down
2 changes: 1 addition & 1 deletion packages/ducrs/src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ fn write_element_file_ids(tx: &Transaction, element_id: &str, file_ids: &[String

fn write_doc_referenced_file_ids(tx: &Transaction, element_id: &str, file_ids: &[String]) -> SerializeResult<()> {
let mut stmt = tx.prepare_cached(
"INSERT INTO doc_element_referenced_files (element_id, file_id, sort_order) VALUES (?1, ?2, ?3)"
"INSERT OR IGNORE INTO doc_element_referenced_files (element_id, file_id, sort_order) VALUES (?1, ?2, ?3)"
)?;
for (i, fid) in file_ids.iter().enumerate() {
stmt.execute(params![element_id, fid, i as i32])?;
Expand Down
Loading