Apsule Project形式
配布単位と作業レイアウト
正規の共有・import単位は単一の.apsuleファイルです。これは、文書化された内部レイアウトを持つZIP containerです。開発ツールは展開済みdirectoryを直接扱って構いませんが、packによってproject semanticsが変わってはいけません。
MyApsule/
├─ app.json
├─ theme.json
├─ views/
│ ├─ home.json
│ ├─ detail.json
│ └─ settings.json
├─ components/
│ ├─ glass-card.json
│ └─ item-row.json
├─ scripts/
│ ├─ main.js
│ ├─ api.js
│ └─ bluetooth.js
├─ assets/
│ ├─ icon.png
│ └─ images/
└─ data/development folderはAI、Builder、iPhone Hostが共有するsource treeです。pack済み.apsuleにはapplication code/assetsのみを含め、project Storage、Database、Files sandbox、Keychain dataは含めません。user dataのbackup/exportは別機能です。
実行可能なJSON Schemaはschemas/にあります。examples/配下のreference projectはnpm run validateによってそれらのschemaに対して検証されます。
Spec/sdk/配下のshared SDKが、load、validation、safe path、file/content hash、pack/unpack、diff、runtime compatibility checkに関するcanonical tooling implementationです。CLI、CI、将来のBuilder integrationはProject contractを再実装せず、このlogicを再利用する必要があります。
cross-platform CLIはnpm run apsule -- <command>で利用できます。現在のcommandはcreate、validate、pack、unpack、inspect、diff、compatible、devices、push、run、stopです。device/sync commandはConductorの通常のloopback discoveryと正規化されたapsule.* capabilityを使い、Relayを直接呼び出しません。
app.json
推奨v1形式:
{
"schemaVersion": "1.0",
"id": "01KAPSULEPROJECTEXAMPLE",
"name": "Example",
"entry": "home",
"script": "scripts/main.js",
"runtime": {
"minimum": "1.0"
},
"routes": {
"home": "views/home.json",
"detail": "views/detail.json",
"settings": "views/settings.json"
},
"presentation": {
"hostChrome": "visible",
"ignoresSafeArea": false
},
"permissions": {
"network": true,
"storage": true,
"bluetooth": false,
"location": "none",
"camera": false,
"microphone": false,
"photos": false,
"notifications": false,
"liveActivities": false,
"background": false,
"push": false,
"speech": false,
"contacts": false,
"calendars": false,
"reminders": false,
"motion": false,
"biometrics": false,
"nfc": false,
"health": false,
"home": false,
"localNetwork": false,
"music": false,
"authentication": false,
"weather": false,
"translation": false,
"nearbyInteraction": false,
"spotlight": false,
"wallet": false
}
}ルール
schemaVersion: Apsule Project schemaのversion。id: opaqueで、自動生成される安定したproject identifier。reverse-domain形式のsemantic contractではなく、userが選ぶ値でもありません。安全なcross-platform storageのため、1〜128文字のASCII英字・数字・.・_・-に制限します。表示名を変えてもidは変えません。name: user-facing name。entry: initial route。script: 任意のapplication bootstrap JavaScript。runtime.minimum: 必要な最小Host runtime。routes: 安定したroute nameからview JSON pathへの対応。permissions: projectが要求できるHost API family。presentation.hostChrome: project実行中にHostのproject title/navigation chromeを表示するか。hiddenはimmersive full-screen project向けです。shake-to-exitは引き続き利用できます。presentation.ignoresSafeArea: immersive project contentをsystem safe areaの下まで拡張できます。system privacy indicatorは引き続きiOSが管理します。
permission keyはadditiveで、存在しない場合はdenied/noneがdefaultです。permissionを宣言しても、userがProject permission更新を承認した後にHostがそのfamilyを要求・利用できるようになるだけで、対応するiOS privacy promptやApple signing/App Service requirementを回避するものではありません。現在のboolean familyはnetwork、storage、bluetooth、camera、microphone、photos、notifications、liveActivities、background、push、speech、contacts、calendars、reminders、motion、biometrics、nfc、health、home、localNetwork、music、authentication、weather、translation、nearbyInteraction、spotlight、walletです。locationはnone | whenInUse | alwaysのままです。
Identityとprovenance
Apsuleではidentityとprovenanceを分離します。
userId: 自動生成される不変のApsule user identity。user-facing nameは独立して設定・変更できます。- manifest
id(project ID): 新規project作成時に自動生成し、rename、restyle、updateでは再生成しません。 - 同じproject IDをimportした場合はupdate candidateとして扱い、projectをduplicateした場合は新しいproject IDを生成します。
- 元のauthorが分かる場合は表示して構いません。
- revisionには、そのidentityが分かる場合、revisionをsubmittedした認証済みuserを記録します。すべての編集を実際に入力した人間だと主張するものではありません。
- revision/provenance metadataはApsule sync/tooling layerが生成し、COS、Codex、他のAI、IDE、手書きproject codeが自己申告した値を信頼しません。
- UIにはcreator、revision timestamp、submitted-by identityを表示できます。不明なprovenanceは推測せず不明のまま扱います。
これにより、特定の1ツールへtrustworthy metadataの書き込みを依存せず、COS、Codex、その他agent、サードパーティIDE、手動編集からproject formatを利用できます。
View node model
通常のUI nodeはすべて一貫したshapeを使います。
{
"id": "refreshButton",
"type": "Button",
"props": {
"text": "Refresh"
},
"style": {
"padding": 16,
"glass": true
},
"events": {
"tap": {
"action": "refresh"
}
},
"children": []
}Stable ID
編集可能なnodeはすべて安定したidを持つ必要があります。これにより次が可能になります。
- visual builderでの選択。
- AIによるtargeted edit。
- 意味のあるdiff。
- Undo/Redo。
- runtime inspection。
- 将来のcollaborative editing。
nodeが移動しただけでIDを再生成してはいけません。
Data binding
文字列interpolationには次のようなexpressionを使います。
{
"type": "Text",
"props": {
"text": "{{state.player.name}}"
}
}bindingはstate、global、route parameter、対応するiteration contextから読み取れます。任意のJavaScriptをJSON内へ埋め込まず、複雑なlogicはscript/actionに置きます。
Repetition
collectionではnodeを複製せずForEach componentを使います。実行可能なv1 semanticsは次のとおりです。
{
"id": "matchRows",
"type": "ForEach",
"props": {
"items": "{{state.matches}}",
"item": "match",
"key": "id"
},
"children": [
{
"type": "MatchCard",
"props": {
"map": "{{match.map}}",
"score": "{{match.score}}"
}
}
]
}props.itemsはruntime stateまたは現在のiteration scope内のarrayへ解決される必要があります。props.itemは子孫から参照できるalias名、props.keyはobject item上のstable fieldを指定します。keyが存在しない場合、Hostはarray indexへfallbackします。子孫から呼ばれたactionは現在のiteration scopeを最初の引数として受け取ります。
Custom component
再利用するUIはcomponents/へ置きます。componentは明示的なpropsを受け取り、通常のview nodeへ展開されます。
{
"component": "MatchCard",
"props": ["map", "score"],
"view": {
"id": "root",
"type": "VStack",
"children": [
{
"id": "map",
"type": "Text",
"props": { "text": "{{props.map}}" }
},
{
"id": "score",
"type": "Text",
"props": { "text": "{{props.score}}" }
}
]
}
}大規模projectが反復的なJSONだらけになるのを防ぐため、Custom componentは重要です。
theme.json
Themeはdesign tokenを集約します。
{
"spacing": {
"sm": 8,
"md": 16,
"lg": 24
},
"radius": {
"card": 20
},
"typography": {
"title": {
"font": "title",
"fontWeight": "bold"
}
}
}Viewはたとえば"$spacing.md"のようにtokenを参照できます。raw valueの繰り返しよりもこの方法を優先します。将来のBuilderやAIがproject全体を一貫してrestyleできるためです。
Preview data
Builderは実APIを呼ばずにmock stateを与えられる必要があります。
{
"state": {
"player": {
"name": "R7",
"rank": "Diamond 2"
}
}
}Preview stateはdevelopment metadataであり、暗黙にproduction persistent dataへ変えてはいけません。