コース進捗 コース目次 18レッスン中 18件を公開中
第I部:描く前に描画面を選ぶ——プロダクト、ピクセル、座標
第II部:ピクセル世界に頭脳を与える——モデル、スケジューリング、入力、ツール
第III部:「ドラッグできる」から「信頼できる」へ——操作、文字、Asset、復旧
第IV部:マスターの判断——Performance、Worker、GPU、SDK、共同編集、AI
5歳児にもわかるゲームから始めよう
本章で伝える唯一の真実:Library選定はProduct、Architecture、License、Maintenance、Exit Costをまとめた意思決定であり、最も格好よいDemoを競うContestではありません。
机の上で乗り物を作る方法は3つあります。1つ目の箱に入っているのはSteel PlateとScrewだけです。Steering Wheel、Brake、Wheelをすべて自分で作ります。Mars Roverを作れるほど自由ですが、壊れたScrewはすべて自分で直します。2つ目の箱には完成したSchool Busが入っています。Seat、Seat Belt、Doorはすでに動くので、すぐ人を運べますが、Vendorの寸法と利用契約に従わなければなりません。3つ目の方法では成熟したChassisを買い、Body、Route Sign、Passenger Listは自分で管理します。
まず予想してください。Teamが製鉄を学んだからといって、明日の登校便を始める前に鉱石を掘るべきでしょうか。違います。では、来年School BusのVendorがConnectorを変え、Passenger ListがVendor Dashboardにしか存在しない場合、乗り物を簡単に替えられるでしょうか。替えられません。本当に問うべきなのは、何を自分たちで所有すべきか、何を買えるか、契約Costはいくらか、故障または離脱時にPassengerとRouteをどう持ち出すかです。
- 目的地を書き出す最初にProduct Capabilityを定義する
- Chassisを比較するSpeedだけを比べない
- 利用契約を読むProduction Licenseを予算化する
- 自分たちのConnectorを付けるDomain Adaptation Layer
- 乗り換えを予行演習するすべてのDataを壊さず持ち出せる
本章で評価するのは正確にtldraw@5.3.2であり、latestではありません。VersionをPinすれば、Code、Migration、License Review、Regression Baselineが同じ対象を参照します。二度とUpgradeしないという意味ではなく、すべてのUpgradeを明示的なProjectとして扱うという意味です。
おもちゃをCanvasに翻訳する
| おもちゃの乗り物に関する判断 | Canvas Labでの判断 | 必ず答えること |
|---|---|---|
| Steelから乗り物全体を作る | Build / Raw Canvas | TeamがEditor Kernelを永久に所有するか |
| 完成したSchool Busを買う | Buy / SDK | 既存Capabilityが適合し、LicenseとUpgradeを受け入れられるか |
| Chassisを買ってBodyを作る | Hybrid | SDKとBusiness Domainの境界はどこか |
| PassengerとRoute | Domain Model | Business Backendが所有するLong-lived Fact |
| Dashboard内部のSignal | SDK Store | Shape、Selection、Tool、Renderingに必要なRecord |
| Connector Adapter | Domain Adapter | Bidirectional Conversion、ID Mapping、Fault Isolation |
| 利用契約 | License | Development、Trial、Commercial、Hobby Productionの条件 |
| 交換用のDoor | Exit Strategy | Export、Replacement Renderer、Data Repatriation、Rehearsal |
| 年次点検 | Upgrade Regression | Custom Shape/Tool/Binding/MigrationのRegression |
比喩には限界があります。SDKは完全に閉じたSchool Busではありません。tldrawのSourceは閲覧でき、多数のExtension Pointがあります。ここでいう「Buy」は主に、Licenseを得てMaintenance ResponsibilityをSDKへ委ねることです。Adaptation LayerもCouplingを魔法のようにすべて取り除くわけではありません。Selectionの操作感、Clipboard Format、Collaboration Protocol、Export PixelもProduct Dependencyを作るため、Exit Checklistへ含めなければなりません。
まず誤った直感を捨てる
- 「tldrawには『draw』とあるので、すべてのShapeをCanvas 2D Bitmapへ描く」。5.3.2ではShapeUtilの
component()がReact Elementを返します。主要ShapeはReact Component LayerとDOM/HTML/SVGを使います。Selection Outline、Brush、SnapなどのOverlayはOverlayUtilによってCanvas 2Dへ描かれる場合があります。「すべてがCanvas 2D」ではありません。 - 「tldraw syncはBuilt-in Yjsだ」。誤りです。tldraw syncはAuthoritative Server、Client Pending/Confirmed Layer、Diff Reconciliationを使います。公式DocumentationはCRDT/Yjsではないと明記しています。Yjsは別のBackend Optionとして個別にIntegrationできます。
- 「SourceがGitHubにあるので、Productionも常に無料だ」。誤りです。Version 5.3.2にはtldraw SDK Licenseが適用されます。DevelopmentにはKeyが不要ですが、Productionには有効なTrial、Commercial、Hobby Keyが必要です。Commercial ProductionにはCommercial Licenseが必要で、HobbyはNoncommercial用途でWatermarkが残ります。
- 「ReadonlyならPermissionも安全だ」。ReadonlyはClientのInteraction Modeです。悪意のあるClientはNetwork/APIを直接呼べます。ServerがIdentity、Object Permission、すべてのWriteを検証しなければなりません。
- 「Business TableをTLStoreと同一にするのが最も簡単だ」。SDK RecordとProduct DomainではVersion、ID、Lifecycleが異なります。Storeを唯一のBackend Modelにすると、ExitとReportingがSDKに固定されます。
- 「5.xのUpgradeにはPatch互換の変更しか入らない」。tldrawはSemVerを使用しないと公式に述べています。通常のMinor ReleaseにもBreaking Changeが入り得ます。正確なVersionをPinし、Release Noteを読み、MigrationとRegressionを行います。
- 「Libraryが人気なのでBuild/Buyの判断は終わっている」。判断InputはUser Task、Object Complexity、Accessibility、License、Collaboration、Team Capability、Exit Costです。
本番用バックパック
前提
第1章ですでにRenderer/Product Boundaryを定義しています。第15章には実際のPerformance Traceがあります。第16章ではWorker/GPUが必要かを証明しました。このEvidenceなしに、「将来10万Objectになるかもしれない」という理由だけで複雑なTechnologyを正当化できません。第5章と第13章のDomain DocumentとMigrationは、SDK選定にかかわらずLong-term Factとして残ります。
正式な知識
選定Matrix。 Raw Canvas 2Dは、高度なCustomization、単純なObject、特殊なDrawing Algorithm、Geometry、Selection、Accessibility、Text、History、Clipboard、Collaboration、Migrationのすべてを所有する意思があるTeamに向きます。SVG/DOMは、Text、Semantics、Accessibility、CSS、Layoutが重要で、Object数が中程度で、Native Browser Interactionに価値があるProductに向きます。Konva型Scene Graphは、Canvas 2Dを使いたい一方、すべてのNode、Event、Transformを一から保守したくないTeamに向きます。GPU 2Dは、多数のSprite、Dynamic Effect、高Fill Rate、Custom Shaderへ向きますが、ProfileでGPU Pathの効果を証明した後に限ります。Three.jsは、本物のScene/Camera/Mesh/Material、Perspective、Depth、Lighting、Raycastingへ向き、普通の2D Infinite CanvasのDefault Upgradeではありません。
tldrawはWhiteboard、Diagram、Workflow、Infinite Canvas、複雑なSelection、Tool、History、Text、Binding、Export、Collaborationに向きます。5.3.2では、主要Shape RenderingはReact Component Hierarchyが駆動し、ShapeUtilはHTML/SVGを返せます。新しいOverlayUtil SystemはTransientなSelection/Snapping UIをCanvas 2Dへ直接描きます。したがって正確な説明は「主にReact/DOM/SVGで、一部OverlayがCanvas 2Dを使う」です。どちらかの絶対的なSloganではありません。
DependencyをPinする。 npm install --save-exact tldraw@5.3.2 @tldraw/sync@5.3.2を使い、LockfileをCommitし、CIでPackage VersionをAssertします。^5.3.2は決して使いません。ClientとSync Serverを一緒にReleaseします。Custom Shape/Binding SchemaとMigrationを両方へRegisterします。Lockfileを変える前に、隔離したCompatibility環境で古いSnapshotを開き、Golden DocumentをExportし、Browser RegressionとVisual Regressionを実行します。
Production License。 tldraw@5.3.2はSource-availableであり、MITやApacheのようなPermissive Open Sourceではありません。2026-08-29時点の公式条件を確認します。DevelopmentにはKeyが不要で、Productionには有効なLicense Keyが必要です。100日間のTrialはEvaluation用です。Commercial Productと社内Enterprise ProductionにはCommercial Licenseを使います。Noncommercial Userは、“made with tldraw” Watermarkが付く裁量的なHobby Licenseへ申請できます。KeyはClient内でLocal Validationされ、Publicでも構いませんが、Allowed Host、Type、Expiryを含みます。PriceはProcurement Inputなので、ADRで固定額を推測してはいけません。Sales Quote、Renewal、Domain、Expiry、Legal Approvalを記録します。License TelemetryもPrivacy Reviewへ含めます。現在の公式License key Pageでは、Commercial KeyはDataを送らず、Trial/HobbyはLicense ID、SDK Version、Page URLを送ると説明しています。Canvas Contentではありませんが、URLにはSensitiveなPathやQuery Parameterが含まれる場合があります。Production DomainとRouteをTestし、Legal/Privacy Ownerに確認してもらいます。tldraw syncにはSDK Licenseが適用されますが、無料のHosted Serviceになるわけではありません。公式ProjectはProduction SyncをHostしません。Team自身がSelf-hostしてPersistenceを用意します。
StoreはDomainではない。 SDKはShape/Page/Binding/AssetのCanvas Representation、Tool State、Internal Migrationを所有します。Business BackendはWorkItem、Permission、Approval State、Audit、Business Version、Asset Policy、Stable Business IDを所有します。DomainAdapter.toCanvasはBusiness ObjectをShapeへProjectionします。fromCanvasCommandは許可されたEditをBusiness Commandへ変換します。Shapeのmeta.domainIdはReferenceにすぎず、Salary、Secret、完全なBusiness PayloadをStoreへ入れてはいけません。
Extension Surface。 Custom ShapeはProps Validator、Migration、Geometry、React Component、Indicator、Resize Behaviorを提供します。Custom ToolはHierarchical StateNodeを使います。Custom BindingはBindingUtilが保守するDirectional Relationshipを表します。Custom Recordではdocument/session/presence Scope、Validator、Migrationを宣言しなければなりません。Domain AdapterはAnti-corruption Layerです。Visual ExportにはSDKのSVG/PNG Pipelineを使い、Semantic Exportには自分たちのDomain JSONを使います。Readonly ModeはViewerに使えますが、Permission-Aware ControlsでもCapabilityに応じてCommandをHide/Disableし、ServerがすべてのWriteを再検証しなければなりません。
根拠と互換性(2026-08-29確認)
npm Registry Releaseのtldraw 5.3.2をPinし、対応する@tldraw/sync 5.3.2を確認します。React Component HierarchyとGeometry契約は公式Shapes Documentationを、Canvas 2D Transient Layerの例外はOverlay utilsを参照します。Store Scope、Custom Record、MigrationはStoreとPersistenceを参照します。tldraw syncのAuthoritative-server ModelはCollaborationを確認します。公式Projectはtldraw syncがYjs/CRDTではないとも述べています。Production Key、License Type、現在のData Transmissionについては、より具体的なLicense key Pageに従い、完全なLicenseも読みます。Procurement前にはLegal Counselが実際の契約をReviewしなければなりません。本TutorialはLegal Adviceではありません。
この章のエンジニアリング増分
開始点: Canvas LabのCustom Kernelには完全なIntellectual Ownershipと学習価値がありますが、Whiteboard級InteractionをDeliveryするCostは高い状態です。到達点: 5.3.2へPinしたHybrid SpikeでBusiness Card Shape、Creation Tool、Dependency Binding、Custom Record、Domain Adapter、Migration、Dual Export、Readonly、Permission Controlを再構築し、実行可能なExit Planを持つADRを書きます。
次のFileとInterfaceを追加します。
src/sdk/tldraw/domain.tsとDomainAdapter.ts:唯一のBusiness Boundary。src/sdk/tldraw/TaskShapeUtil.tsx:Custom Shape + Props Migration。src/sdk/tldraw/CreateTaskTool.ts:Custom Tool。src/sdk/tldraw/DependencyBindingUtil.ts:Custom Binding。src/sdk/tldraw/schema.ts:Document-scoped Custom Record。src/sdk/tldraw/CanvasSdkView.tsx:License、Readonly、Permission Control。src/sdk/tldraw/export.ts:Domain JSONとVisual Export。docs/adr/017-canvas-sdk.md:Build/Buy/Hybridの判断とExit Rehearsal。
次は実行可能なCustom Shape、Migration、Tool、Domain Adapter、Permission Shellです。Business Taskには意図的にtldraw importを含めません。
import { useMemo } from 'react';
import {
BaseBoxShapeUtil,
HTMLContainer,
Rectangle2d,
StateNode,
T,
Tldraw,
TLBaseShape,
TLCreateShapePartial,
TLShapeId,
createShapeId,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
exportAs,
resizeBox,
} from 'tldraw';
import 'tldraw/tldraw.css';
import { createCanvasStore } from './schema';
export type Task = Readonly<{
id: string;
title: string;
status: 'todo' | 'doing' | 'done';
revision: number;
}>;
type TaskShape = TLBaseShape<
'task-card',
{ w: number; h: number; domainId: string; title: string; status: Task['status'] }
>;
const versions = createShapePropsMigrationIds('task-card', { AddStatus: 1 });
const migrations = createShapePropsMigrationSequence({
sequence: [
{
id: versions.AddStatus,
up: (props: Record<string, unknown>) => {
props.status ??= 'todo';
},
down: (props: Record<string, unknown>) => {
delete props.status;
},
},
],
});
export class TaskShapeUtil extends BaseBoxShapeUtil<TaskShape> {
static override type = 'task-card' as const;
static override props = {
w: T.number,
h: T.number,
domainId: T.string,
title: T.string,
status: T.literalEnum('todo', 'doing', 'done'),
};
static override migrations = migrations;
override getDefaultProps(): TaskShape['props'] {
return { w: 240, h: 112, domainId: '', title: '新しいTask', status: 'todo' };
}
override getGeometry(shape: TaskShape) {
return new Rectangle2d({ width: shape.props.w, height: shape.props.h, isFilled: true });
}
override component(shape: TaskShape) {
return (
<HTMLContainer
style={{
pointerEvents: 'all',
border: '2px solid #334155',
borderRadius: 12,
background: '#fff',
padding: 12,
}}
>
<strong>{shape.props.title}</strong>
<p>{shape.props.status}</p>
</HTMLContainer>
);
}
override getIndicatorPath(shape: TaskShape) {
const path = new Path2D();
path.rect(0, 0, shape.props.w, shape.props.h);
return path;
}
override onResize(shape: TaskShape, info: Parameters<typeof resizeBox>[1]) {
return resizeBox(shape, info);
}
}
export const DomainAdapter = {
toShape(
task: Task,
existingId?: TLShapeId,
): TLCreateShapePartial<TaskShape> & { id: TLShapeId; props: TaskShape['props'] } {
return {
id: existingId ?? createShapeId(task.id),
type: 'task-card',
x: 0,
y: 0,
props: { w: 240, h: 112, domainId: task.id, title: task.title, status: task.status },
};
},
toDomainPatch(
shape: Pick<TaskShape, 'props'>,
before: Task,
): Pick<Task, 'id' | 'title' | 'status' | 'revision'> {
if (shape.props.domainId !== before.id) throw new Error('DOMAIN_ID_MISMATCH');
return {
id: before.id,
title: shape.props.title,
status: shape.props.status,
revision: before.revision,
};
},
};
export class CreateTaskTool extends StateNode {
static override id = 'create-task';
override onEnter() {
this.editor.setCursor({ type: 'cross', rotation: 0 });
}
override onPointerDown() {
const p = this.editor.inputs.getCurrentPagePoint();
const id = createShapeId();
this.editor.createShape<TaskShape>({ id, type: 'task-card', x: p.x, y: p.y });
this.editor.select(id);
if (!this.editor.getInstanceState().isToolLocked) this.editor.setCurrentTool('select');
}
override onExit() {
this.editor.setCursor({ type: 'default', rotation: 0 });
}
}
type Capabilities = Readonly<{ edit: boolean; export: boolean }>;
const tools = [CreateTaskTool];
export function CanvasSdkView({
capabilities,
licenseKey,
}: {
capabilities: Capabilities;
licenseKey: string;
}) {
const store = useMemo(createCanvasStore, []);
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw
licenseKey={licenseKey}
store={store}
tools={tools}
onMount={(editor) => {
editor.updateInstanceState({ isReadonly: !capabilities.edit });
const onExport = async () => {
if (!capabilities.export) throw new Error('EXPORT_FORBIDDEN');
await exportAs(editor, [...editor.getCurrentPageShapeIds()], { format: 'svg' });
};
(globalThis as typeof globalThis & { exportCanvas?: () => Promise<void> }).exportCanvas =
onExport;
return () => {
delete (globalThis as typeof globalThis & { exportCanvas?: () => Promise<void> })
.exportCanvas;
};
}}
/>
</div>
);
}
このSpikeではDemonstrationのためだけにExport FunctionをGlobalへ公開します。Production UIでは明示的なComponent/Serviceを通してInjectすべきです。Server Permissionが本当のSecurity Boundaryであることは変わりません。Custom BindingとCustom Recordに必要なData Contractは次のとおりです。ClientとSync Serverは同じValidator/MigrationをRegisterしなければなりません。
import {
BaseRecord,
BindingUtil,
CustomRecordInfo,
RecordId,
T,
TLBaseBinding,
createCustomRecordId,
createCustomRecordMigrationIds,
createCustomRecordMigrationSequence,
createTLStore,
} from 'tldraw';
import { TaskShapeUtil } from './TaskShapeUtil';
export type DependencyBinding = TLBaseBinding<'task-dependency', { kind: 'blocks' | 'relates' }>;
export class DependencyBindingUtil extends BindingUtil<DependencyBinding> {
static override type = 'task-dependency' as const;
static override props = { kind: T.literalEnum('blocks', 'relates') };
override getDefaultProps(): DependencyBinding['props'] {
return { kind: 'blocks' };
}
}
export interface CanvasNote extends BaseRecord<'canvas-note', RecordId<CanvasNote>> {
taskId: string;
text: string;
createdAt: number;
}
const noteVersions = createCustomRecordMigrationIds('canvas-note', { AddCreatedAt: 1 });
export const canvasNoteInfo: CustomRecordInfo = {
scope: 'document',
validator: T.object({
id: T.string,
typeName: T.literal('canvas-note'),
taskId: T.string,
text: T.string,
createdAt: T.number,
}),
migrations: createCustomRecordMigrationSequence({
sequence: [
{
id: noteVersions.AddCreatedAt,
up: (record) => {
record.createdAt ??= 0;
},
down: (record) => {
delete record.createdAt;
},
},
],
}),
createDefaultProperties: () => ({ taskId: '', text: '', createdAt: 0 }),
};
export const newCanvasNote = (taskId: string): CanvasNote =>
({
id: createCustomRecordId('canvas-note'),
typeName: 'canvas-note',
taskId,
text: '',
createdAt: Date.now(),
}) as CanvasNote;
export const createCanvasStore = () =>
createTLStore({
shapeUtils: [TaskShapeUtil],
bindingUtils: [DependencyBindingUtil],
records: { 'canvas-note': canvasNoteInfo },
});
School Busへ戻ると、TaskはPassenger Record、TaskShapeはSeat Cardです。Seat CardのLibraryを置き換えてもPassengerのIdentityは変えてはいけません。Dependency BindingはSeat間のRoute Hintです。Readonlyが覆うのはFront-door Buttonだけで、Policeではありません。Serverは引き続きTicketを確認します。Custom Recordが本当にRegisterされるのは、createTLStore({ records })へ配置したときだけです。Validator ObjectをExportするだけで<Tldraw>にDefault Storeを作らせると、Testは一度もそれを通りません。ClientとSync Serverは同じSchema Optionを使わなければなりません。
TestでRound-trip Adaptation、UIから独立したPermission、古いSnapshotを開くMigrationを証明します。
import { expect, test } from 'vitest';
import { DomainAdapter } from '../DomainAdapter';
test('Domain → Shape → DomainでBusiness Identityを保持する', () => {
const task = { id: 'task-42', title: '契約をReviewする', status: 'doing' as const, revision: 7 };
const shape = DomainAdapter.toShape(task);
expect(shape.props.domainId).toBe('task-42');
expect(
DomainAdapter.toDomainPatch({ ...shape, props: { ...shape.props, status: 'done' } }, task),
).toEqual({ ...task, status: 'done' });
});
test('Readonlyを迂回したUnauthorized WriteをServerが拒否する', () => {
const authorize = (canEdit: boolean, command: { type: string }) =>
canEdit && ['UpdateTask', 'MoveTask'].includes(command.type);
expect(authorize(false, { type: 'MoveTask' })).toBe(false);
expect(authorize(true, { type: 'DeleteWorkspace' })).toBe(false);
});
npm ls tldraw @tldraw/syncを実行します。別VersionへDeduplicateされず、両方が正確に5.3.2と表示されることを期待します。npm exec vitest run src/sdk/tldrawを実行し、Domain Round-trip、v0 Shape Migration、Unknown Record、Permission、Visual/Semantic Exportが通ることを確認します。npm exec playwright test tests/tldraw-sdk.spec.tsを実行し、PinしたBrowser MatrixでCustom Tool/Shape/Binding、Readonly、Keyboard、Exportが通ることを確認します。
ADR-017:Build / Buy / Hybridの判断
Status: SpikeとしてAccepted。License Quote、Performance/Accessibility Regression、Exit Rehearsalが通った後でのみProductionへ入れます。
ContextとConstraint: ProductはVisual Operations Canvasであり、Text、Connector、Multiplayer、History、Clipboard、Migrationを必要とし、3か月以内にDeliveryしなければなりません。Business Task/Permission/AuditはBackend-authoritativeである必要があります。3Dではありません。第15章のVisible-object ScaleはDOM/React Cullingに適し、GPUの必要性は証明されていません。
Option: Build(現在のRaw Canvas Kernel)は最もControlできますが、Deliveryと長期的なTable-stakes Costが最大です。Buy(すべてのDataをTLStoreへ直接入れる)は最速ですが、BusinessとExitのCouplingが許容できません。Hybrid(tldrawがEditor Experienceを所有し、私たちがDomain/Permission/Auditを所有する)はTimeとControlの要件を満たします。判断はHybridで、tldraw 5.3.2へPinします。
Ownership: SDK StoreがCanvas Projection、Page/Shape/Binding/Asset Reference、Editor Historyを所有します。Business BackendがTask、Stable Business ID、Permission、Audit、Asset Authorization、Domain Versionを所有します。Session/Presenceは第13章と第18章のとおり別Scopeにします。Domain WriteはAdapter/Command APIだけを通ります。
Export: 法務上・長期的なExportはVersion付きDomain JSON + Asset Manifestです。Visual ExportはSDK SVG/PNGを使い、Golden Testを設けます。NeutralなReplacement-Renderer FormatとしてCanvasPortableV1(Position、Dimension、Text、Connection、Asset ID)を保持します。SDK Snapshotだけを唯一のBackupにしてはいけません。
MigrationとTesting: Domain SchemaとSDK Schemaを別々のMigration ChainとしてTestし、実際のAnonymized Golden Documentを使います。すべてのUpgradeでTypecheck、Migration Fixture、Adapter Round-trip、Custom Shape/Tool/Binding、Keyboard/Screen Reader、Visual/Export、Performance、Sync Version-skew Testを実行します。ClientとServerは一緒にReleaseします。前BuildとRead-only Downgradeを保持します。
License Cost: DevelopmentにはKeyが不要です。Productionでは、Trial/Commercial/HobbyのEligibility、Quote、Renewal Date、Allowed Host、License Type別Data-transmission Behavior、Legal/Privacy Review、Expiry FallbackをAnnual TCOへ含めます。Commercial ProjectはCommercial Licenseを使い、Hobby LicenseでFeeを回避しません。Priceを推測せず、実際のContractをADRへ添付します。VersionまたはTerms変更時に再確認します。本章の日付は永久のPromiseではありません。
Exit Strategy: Domain Layerはtldrawをimportしません。すべてのStable IDをMapできます。Domain JSON/PortableV1を毎日生成します。Compatibility RendererがDocumentをRead-onlyで開き、基本編集を行えます。Quarterlyに、隔離環境でGolden DocumentをReplacement RendererへImportし、非対応Featureを記録します。Exit Sequenceは、新しいSDK FeatureをFreeze→ExportしてValidate→Binding/Text/AssetをConvert→Dual-read Comparison→Low-traffic Switch→Read-only Legacy Viewerを保持→LicenseをRevoke、です。
SDKがMaintenanceされなくなった場合: 監査済みVersionをPinし、Build Assetを保存します。Security RiskとBrowser-degradation Windowを評価します。最初にCompatibility Rendererを有効にし、PortableV1を通してMigrationします。Forkは一時的なRisk Mitigationであり、TeamがSDK全体を永久に保守できると仮定してはいけません。
UpgradeがCustom Shapeを壊した場合: 古いSnapshotを上書きしません。Shadow環境で新VersionにCopyを読ませ、Shape Props MigrationとVisual/Semantic Diffを実行します。Adapter、Geometry、Export、Sync Gateのいずれかが失敗したら、LockfileとServerをRollbackします。Releaseを避けられなければ、Migration前にDual-read/Old-writeを使います。旧Clientが読めない不可逆な新Formatを直接書いてはいけません。
結果: 成熟したInteractionを得て、Table-stakes実装を減らせます。一方でLicense、React/DOM Rendering Characteristic、Version Upgrade、Sync OperationへのDependencyを受け入れます。Adapter、PortableV1、Quarterly Exit Rehearsalは、このDependencyを受け入れるための必須条件であり、飾りのDocumentationではありません。
わざと壊す
| 注入する故障 | 症状 | 根拠 | 修正 | Regression Test | 復旧 |
|---|---|---|---|---|---|
| TLStoreをBusiness Databaseとして扱う | Reporting/ExitがSDK Fieldに依存する | Import GraphとBackup Format | Domain + Adapter | Architecture-boundary Test | DomainからProjectionを再構築する |
| 5.3.2からUpgradeしてShapeが壊れる | 古いCardが空白になる、またはResizeする | Golden Migration/Visual Diff | Props MigrationとPinning | 実Snapshot | Client/ServerをRollbackする |
| Sync ServerとClientのVersionが異なる | RoomがDataを拒否または破損する | Handshake/Schema Log | 同時Releaseする | Version-skew Suite | Read-onlyにしてRefreshを要求する |
| Production License Keyが無効/期限切れ | SDKをProductionで利用できない | License DiagnosticsとExpiry Ledger | 正しいKeyとRenewal Process | Staging Host/Expiry Test | Compatibility Viewer |
| Delete ButtonだけをHideする | APIからObjectを削除できる | Server Audit | すべてのCommandをAuthorizeする | UI-bypass Request | 拒否してRevisionを保持する |
| Custom BindingがServerに未Register | Sync/Validationが失敗する | Schema Mismatch | 両側で同じSchemaを使う | Connect Fixture | WriteをBlockしLocalに保持する |
| ExportにSDK Snapshotしか残さない | 新LibraryがSemanticsを復元できない | Exit DrillにTaskがない | Domain JSON + PortableV1 | Quarterly Import | Backend Domainを読む |
| SDKがMaintenanceされなくなる | 新Browser Regressionを誰も直さない | Risk Register | Freeze、Replace、Limited Fork | Compatibility Matrix | 段階的にMigrationする |
根拠を示して合格する
| 自動化された根拠 | 手動の根拠 | 合格条件 |
|---|---|---|
| Exact-version Assert、Adapter Round-trip、Migration/Golden、Permission、Export、Sync-skew、Performance Regression | Keyboard/Screen Reader Task、Production-license Staging、Quarterly Exit Rehearsal | すべてのAutomated Gateが通り、PortableV1によってAlternative ViewerでCore Contentを開ける |
| Import GraphによってDomainからtldrawへのDependencyを禁止する | Architecture、Legal、Procurement、SecurityがADRを共同Reviewする | Ownership、TCO、Allowed Host、Renewal、Exit OwnerがSign-offされる |
| 判断の問い | 本章の回答と根拠 |
|---|---|
| なぜSDKを選ぶか | WhiteboardのTable StakesがDelivery WindowとBenchmark Scaleに合うため。人気だからではない |
| 誰がDataを所有するか | SDKはCanvas Projection、Business BackendはDomain/Permission/Auditを所有する |
| どうExport/Migrationするか | Domain JSON + PortableV1 + Visual Export。両Migration ChainにGolden Testを用意する |
| どうTestするか | Custom Extension、Accessibility、Visual、Export、Performance、Sync SkewをEnd-to-endで検証する |
| License Costはいくらか | Commercial Quote/RenewalをTCOへ含める。Tutorialで数値を推測しない |
| どうExitするか | Adapter、Stable ID、Neutral Format、Compatibility Renderer、Quarterly Rehearsal |
- Dependencyを正確に
tldraw@5.3.2と対応するSync PackageへPinする。 - 「主にReact/DOM/SVG Shapeで、Canvas 2D Overlayは例外」と正確に説明し、すべてがCanvas 2Dだとは言わない。
- tldraw syncをYjs/CRDTではなくServer-authoritativeと正確に説明する。
- Production License Key、Commercial/Hobby条件、Renewal、Legal ReviewをADRに記載する。
- Custom Shape、Tool、Binding、Record、Migration、Export、Readonly、Permission ControlにEvidenceがある。
- Business ModelはSDK Storeではなく、Exit Rehearsalは仮定上のものではない。
- 最終的なBuild / Buy / Hybrid判断を出し、棄却Optionの実Costを説明する。
5歳児に説明する
「SDK」「Adapter」「License」「Migration」という言葉を使わずに説明してください。Wheelの作り方を知っていても、移動のたびにSteelを製錬する必要がないのはなぜでしょう。Chassisを買うときにPassenger Listを自分で保持すべきなのはなぜでしょう。VendorがConnectorを変えたり閉業したりしたら、どうやって乗り物を家へ持ち帰るのでしょう。
専門用語を使わない合格回答
子どもを学校へ運ぶにはSeat Belt、Brake、Doorが必要です。成熟したChassisは難しい問題をすでに解いているため、早く出発できます。Routeが本当に特殊で、すべてのScrewを永久に修理する覚悟がTeamにある場合だけ、全部を自作します。子どもの名前、Permission、Routeは自分たちの台帳へ残し、表示と運転に必要なSeat CardだけをDashboardへ置き、検査可能なConnectorで両者を翻訳します。購入前に契約、年間Cost、Expiryを書き出し、別の単純な乗り物でも定期的に台帳をTestします。VendorがConnectorを変えたりBusの修理をやめたりしても、Passenger全員を壊さず持ち出せます。