JEPA4Japan · チュートリアル

第17章:車を自作するか、実績あるシャーシを買うか

6,311文字 19分で読めます #Canvas#Frontend Engineering#Infinite Canvas#ELI5

固定したtldraw 5.xを題材に、Raw Canvas、SVG、Konva、GPU、Three.js、tldrawのBuild/Buy/Hybrid ADRを完成させます。

コース進捗 コース目次 18レッスン中 18件を公開中

第I部:描く前に描画面を選ぶ——プロダクト、ピクセル、座標

  1. 01 第1章:まだ描かない——Canvasはプロダクト設計ではない 公開中
  2. 02 第2章:すぐに記憶を失うピクセルの紙 公開中
  3. 03 第3章:お絵描きを再現可能なレシピにする 公開中
  4. 04 第4章:4枚の地図と1台のカメラ 公開中

第II部:ピクセル世界に頭脳を与える——モデル、スケジューリング、入力、ツール

  1. 05 第5章:ピクセル世界に台帳を作る 公開中
  2. 06 第6章:ランプが点いたときだけ描き直す——Render SchedulerとReactの境界 公開中
  3. 07 第7章:マウス、指、ペンに同じ言葉を話してもらう 公開中
  4. 08 第8章:細い縁を調べる前に大きな箱を探す 公開中
  5. 09 第9章:ツールは信号機であり、Booleanの袋ではない 公開中

第III部:「ドラッグできる」から「信頼できる」へ——操作、文字、Asset、復旧

  1. 10 第10章:触って気持ちよいエディターにする 公開中
  2. 11 第11章:描かれた文字は編集できる文字ではない 公開中
  3. 12 第12章:借りた画像を勝手に箱へ詰めてはいけない 公開中
  4. 13 第13章:タイムマシンと古い箱 公開中
  5. 14 第14章:正しく見えることと、本当に正しいことは違う 公開中

第IV部:マスターの判断——Performance、Worker、GPU、SDK、共同編集、AI

  1. 15 第15章:1万人を一人ずつ探さない 公開中
  2. 16 第16章:受付を厨房へ入れない——WorkerとGPUへの更新 公開中
  3. 17 第17章:車を自作するか、実績あるシャーシを買うか 現在のレッスン
  4. 18 第18章:人と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をどう持ち出すかです。

  1. 目的地を書き出す最初にProduct Capabilityを定義する
  2. Chassisを比較するSpeedだけを比べない
  3. 利用契約を読むProduction Licenseを予算化する
  4. 自分たちのConnectorを付けるDomain Adaptation Layer
  5. 乗り換えを予行演習するすべてのDataを壊さず持ち出せる
先に選んでください。Routeは決まっていて、多数のWhiteboard Capabilityが必要で、Launchを急いでいます。しかしBusiness RecordはBackendの管理下に残さなければなりません。答えは通常Hybridです。すべてを無条件に自作することでも、すべてを引き渡すことでもありません。

本章で評価するのは正確にtldraw@5.3.2であり、latestではありません。VersionをPinすれば、Code、Migration、License Review、Regression Baselineが同じ対象を参照します。二度とUpgradeしないという意味ではなく、すべてのUpgradeを明示的なProjectとして扱うという意味です。

おもちゃをCanvasに翻訳する

おもちゃの乗り物に関する判断Canvas Labでの判断必ず答えること
Steelから乗り物全体を作るBuild / Raw CanvasTeamがEditor Kernelを永久に所有するか
完成したSchool Busを買うBuy / SDK既存Capabilityが適合し、LicenseとUpgradeを受け入れられるか
Chassisを買ってBodyを作るHybridSDKとBusiness Domainの境界はどこか
PassengerとRouteDomain ModelBusiness Backendが所有するLong-lived Fact
Dashboard内部のSignalSDK StoreShape、Selection、Tool、Renderingに必要なRecord
Connector AdapterDomain AdapterBidirectional Conversion、ID Mapping、Fault Isolation
利用契約LicenseDevelopment、Trial、Commercial、Hobby Productionの条件
交換用のDoorExit StrategyExport、Replacement Renderer、Data Repatriation、Rehearsal
年次点検Upgrade RegressionCustom 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 FormatDomain + AdapterArchitecture-boundary TestDomainからProjectionを再構築する
5.3.2からUpgradeしてShapeが壊れる古いCardが空白になる、またはResizeするGolden Migration/Visual DiffProps MigrationとPinning実SnapshotClient/ServerをRollbackする
Sync ServerとClientのVersionが異なるRoomがDataを拒否または破損するHandshake/Schema Log同時ReleaseするVersion-skew SuiteRead-onlyにしてRefreshを要求する
Production License Keyが無効/期限切れSDKをProductionで利用できないLicense DiagnosticsとExpiry Ledger正しいKeyとRenewal ProcessStaging Host/Expiry TestCompatibility Viewer
Delete ButtonだけをHideするAPIからObjectを削除できるServer AuditすべてのCommandをAuthorizeするUI-bypass Request拒否してRevisionを保持する
Custom BindingがServerに未RegisterSync/Validationが失敗するSchema Mismatch両側で同じSchemaを使うConnect FixtureWriteをBlockしLocalに保持する
ExportにSDK Snapshotしか残さない新LibraryがSemanticsを復元できないExit DrillにTaskがないDomain JSON + PortableV1Quarterly ImportBackend Domainを読む
SDKがMaintenanceされなくなる新Browser Regressionを誰も直さないRisk RegisterFreeze、Replace、Limited ForkCompatibility Matrix段階的にMigrationする

根拠を示して合格する

自動化された根拠手動の根拠合格条件
Exact-version Assert、Adapter Round-trip、Migration/Golden、Permission、Export、Sync-skew、Performance RegressionKeyboard/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全員を壊さず持ち出せます。