akari-video 1.5.0-beta.2 → 1.5.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/kits.mjs +51 -1
  3. package/src/store-command.mjs +74 -5
  4. package/vendor/.akari-capability-sources.json +4 -0
  5. package/vendor/docs/contract-2026-09-13-extension-kit-v0.md +6 -2
  6. package/vendor/docs/contract-2026-10-15-playbook-v0.md +8 -1
  7. package/vendor/docs/contract-2026-10-15-project-manifest-v0.md +186 -0
  8. package/vendor/docs/contract-2026-10-15-script-lines-v0.md +67 -0
  9. package/vendor/docs/contract-2026-10-16-automation-gates-v0.md +87 -0
  10. package/vendor/docs/contract-2026-10-16-recipes-and-connectors-v0.md +82 -0
  11. package/vendor/packages/akari-launcher/package.json +1 -1
  12. package/vendor/packages/edit-lint/src/lint/intake.mjs +10 -1
  13. package/vendor/packages/i18n/locales/en/annotations.json +1 -0
  14. package/vendor/packages/i18n/locales/en/project.json +1 -0
  15. package/vendor/packages/i18n/locales/en/report-project.json +2 -0
  16. package/vendor/packages/i18n/locales/en/surfaces.json +4 -0
  17. package/vendor/packages/i18n/locales/en/transcript.json +12 -0
  18. package/vendor/packages/i18n/locales/ja/annotations.json +1 -0
  19. package/vendor/packages/i18n/locales/ja/project.json +1 -0
  20. package/vendor/packages/i18n/locales/ja/report-project.json +2 -0
  21. package/vendor/packages/i18n/locales/ja/surfaces.json +4 -0
  22. package/vendor/packages/i18n/locales/ja/transcript.json +12 -0
  23. package/vendor/packages/project-scaffold/src/index.mjs +121 -23
  24. package/vendor/packages/schemas/bin/validate-kit-manifest.mjs +45 -1
  25. package/vendor/packages/schemas/kit-manifest.schema.json +11 -0
  26. package/vendor/packages/schemas/playbook.schema.json +20 -0
  27. package/vendor/packages/schemas/script.schema.json +31 -0
  28. package/vendor/packages/schemas/test/kit-playbooks.test.mjs +44 -0
  29. package/vendor/packages/schemas/test/validate-playbook.test.mjs +26 -0
  30. package/vendor/packages/schemas/test/validate-script.test.mjs +29 -0
  31. package/vendor/packages/schemas/validate-playbook.mjs +17 -0
  32. package/vendor/packages/schemas/validate-script.mjs +55 -0
  33. package/vendor/skills/record-playbook/SKILL.md +6 -5
  34. package/vendor/skills/record-playbook/bin/record-playbook.mjs +233 -12
  35. package/vendor/skills/record-playbook/test/record-beats.test.mjs +108 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.5.0-beta.2",
3
+ "version": "1.5.0-beta.4",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/kits.mjs CHANGED
@@ -257,6 +257,42 @@ export function linkKitSkills(kitDir, manifest, home, options = {}) {
257
257
  return { linked, blockers, warnings };
258
258
  }
259
259
 
260
+ export function linkKitPlaybooks(kitDir, manifest, home, options = {}) {
261
+ const linked = [];
262
+ const warnings = [];
263
+ for (const entry of manifest.playbooks ?? []) {
264
+ if (typeof entry?.dir !== 'string' || !/^playbooks\/[a-z0-9][a-z0-9-]*$/u.test(entry.dir)) {
265
+ throw new Error(cliText('cli.kits.error.pathOutside', { path: String(entry?.dir) }));
266
+ }
267
+ const id = path.posix.basename(entry.dir);
268
+ const source = safeKitPath(kitDir, entry.dir);
269
+ const realKit = realpathSync(kitDir);
270
+ const realSource = realpathSync(source);
271
+ if (!realSource.startsWith(`${realKit}${path.sep}`) || !statSync(realSource).isDirectory()) {
272
+ throw new Error(cliText('cli.kits.error.pathOutside', { path: entry.dir }));
273
+ }
274
+ const destination = path.join(home, 'playbooks', id);
275
+ try {
276
+ const stat = lstatSync(destination);
277
+ let owned = false;
278
+ if (stat.isSymbolicLink()) {
279
+ try { owned = realpathSync(destination) === realSource; } catch { /* Broken links are not owned. */ }
280
+ }
281
+ if (!owned) {
282
+ warnings.push(cliText('cli.kits.warning.directoryKept', { path: destination }));
283
+ continue;
284
+ }
285
+ } catch (error) {
286
+ if (error?.code !== 'ENOENT') throw error;
287
+ }
288
+ const result = replaceSymlink(source, destination, options);
289
+ if (result.status === 'linked') linked.push(id);
290
+ else warnings.push(cliText(result.status === 'permission-denied'
291
+ ? 'cli.kits.warning.symlinkFailed' : 'cli.kits.warning.directoryKept', { path: destination }));
292
+ }
293
+ return { linked, warnings };
294
+ }
295
+
260
296
  export function readKitsLedger(home) {
261
297
  const ledgerPath = path.join(home, 'kits', 'installed.json');
262
298
  if (!existsSync(ledgerPath)) return { schema: KITS_SCHEMA, kits: [] };
@@ -269,6 +305,12 @@ export function readKitsLedger(home) {
269
305
 
270
306
  export function writeKitsLedger(home, entry) {
271
307
  const ledger = readKitsLedger(home);
308
+ const previous = ledger.kits.find((kit) => kit.id === entry.id);
309
+ for (const id of previous?.playbooks ?? []) {
310
+ if (!(entry.playbooks ?? []).includes(id)) {
311
+ removeOwnedSymlink(path.join(home, 'playbooks', id), previous.kitDir);
312
+ }
313
+ }
272
314
  ledger.kits = [...ledger.kits.filter((kit) => kit.id !== entry.id), entry];
273
315
  const ledgerPath = path.join(home, 'kits', 'installed.json');
274
316
  mkdirSync(path.dirname(ledgerPath), { recursive: true });
@@ -283,6 +325,7 @@ export function removeKit(home, productId, env = process.env) {
283
325
  const entry = ledger.kits.find((kit) => kit.id === productId);
284
326
  if (!entry) return false;
285
327
  for (const name of entry.skills ?? []) removeOwnedSymlink(path.join(home, 'kits', 'plugin', 'skills', name), entry.kitDir);
328
+ for (const id of entry.playbooks ?? []) removeOwnedSymlink(path.join(home, 'playbooks', id), entry.kitDir);
286
329
  for (const asset of entry.assets ?? []) {
287
330
  for (const root of resolveAssetLibraryRoots({ ...env, AKARI_HOME: home }).read) removeOwnedSymlink(path.join(root, asset.category, asset.id), entry.kitDir);
288
331
  }
@@ -299,7 +342,14 @@ function removeOwnedSymlink(linkPath, kitDir) {
299
342
  try {
300
343
  if (!lstatSync(linkPath).isSymbolicLink()) return;
301
344
  const target = path.resolve(path.dirname(linkPath), readlinkSync(linkPath));
302
- if (target === path.resolve(kitDir) || target.startsWith(`${path.resolve(kitDir)}${path.sep}`)) rmSync(linkPath);
345
+ const root = path.resolve(kitDir);
346
+ let owned = target === root || target.startsWith(`${root}${path.sep}`);
347
+ try {
348
+ const realTarget = realpathSync(linkPath);
349
+ const realRoot = realpathSync(kitDir);
350
+ owned ||= realTarget === realRoot || realTarget.startsWith(`${realRoot}${path.sep}`);
351
+ } catch { /* Lexical ownership still handles a removed kit directory. */ }
352
+ if (owned) rmSync(linkPath);
303
353
  } catch (error) {
304
354
  if (error?.code !== 'ENOENT') throw error;
305
355
  }
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { spawnSync } from 'node:child_process';
2
3
  import {
3
4
  copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync,
@@ -32,6 +33,7 @@ import {
32
33
  enableHint,
33
34
  ensureKitsMarketplace,
34
35
  linkKitAssets,
36
+ linkKitPlaybooks,
35
37
  linkKitSkills,
36
38
  readKitManifest,
37
39
  readKitsLedger,
@@ -347,11 +349,75 @@ export async function runStoreCommand(args, options = {}) {
347
349
 
348
350
  if (sub === 'download') {
349
351
  const productId = args[1];
350
- if (!productId || productId.startsWith('--')) {
352
+ if (!isSafePathSegment(productId) || productId.startsWith('--')) {
351
353
  log(cliText('cli.store.help.downloadUsage'));
352
354
  return { exitCode: 1 };
353
355
  }
354
356
  const creds = readCredentials(env);
357
+ const publicBase = (options.baseUrl ?? creds?.url ?? DEFAULT_STORE_BASE_URL).replace(/\/$/, '');
358
+ let freeProduct;
359
+ try {
360
+ const listing = await fetchImpl(`${publicBase}/products`);
361
+ if (listing.ok) {
362
+ const data = await readJsonResponse(listing);
363
+ freeProduct = data?.products?.find(item => item.id === productId && item.tier === 'free'
364
+ && Number.isInteger(item.current_version) && item.current_version >= 1);
365
+ }
366
+ } catch { /* Paid download remains available when the public catalog is offline. */ }
367
+ if (!freeProduct) {
368
+ // Lab の無料の作り方(playbook)は商品データベースに載らず、無料 R2 の公開カタログ(/assets/catalog.json)にだけ載る。
369
+ // その行の files[0].key を /assets/<key> から取り、sha256 を照合して保存する。
370
+ try {
371
+ const catalogResponse = await fetchImpl(new URL('/assets/catalog.json', publicBase).href);
372
+ if (catalogResponse.ok) {
373
+ const catalog = await readJsonResponse(catalogResponse);
374
+ const row = catalog?.items?.find(item => item?.id === productId && item?.category === 'playbook' && item?.tier === 'free'
375
+ && Number.isInteger(item?.version) && item.version >= 1 && typeof item?.files?.[0]?.key === 'string');
376
+ const key = row?.files?.[0]?.key;
377
+ if (row && /^[a-z0-9][a-z0-9/._-]*\.zip$/.test(key) && !key.includes('..')) {
378
+ const response = await fetchImpl(new URL(`/assets/${key}`, publicBase).href);
379
+ if (!response.ok) {
380
+ log(cliText('cli.store.download.httpFailed', { status: response.status }));
381
+ return { exitCode: 1 };
382
+ }
383
+ const bytes = Buffer.from(await response.arrayBuffer());
384
+ const expected = row.files[0].sha256;
385
+ if (typeof expected === 'string' && createHash('sha256').update(bytes).digest('hex') !== expected) {
386
+ log(cliText('cli.store.download.failed', { message: 'sha256 mismatch' }));
387
+ return { exitCode: 1 };
388
+ }
389
+ const destDir = parseFlag(args, '--dest') ?? process.cwd();
390
+ mkdirSync(destDir, { recursive: true });
391
+ const filePath = path.join(destDir, `${productId}-v${row.version}.zip`);
392
+ writeFileSync(filePath, bytes);
393
+ log(cliText('cli.store.download.saved', { path: filePath }));
394
+ return { exitCode: 0, filePath };
395
+ }
396
+ }
397
+ } catch { /* 公開カタログが取れなければ従来の経路へ */ }
398
+ }
399
+ if (freeProduct) {
400
+ const version = freeProduct.current_version;
401
+ const assetUrl = new URL(`/assets/products/${productId}/v${version}/${productId}-v${version}.zip`, publicBase);
402
+ try {
403
+ const response = await fetchImpl(assetUrl.href);
404
+ if (response.ok) {
405
+ const destDir = parseFlag(args, '--dest') ?? process.cwd();
406
+ mkdirSync(destDir, { recursive: true });
407
+ const filePath = path.join(destDir, `${productId}-v${version}.zip`);
408
+ writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
409
+ log(cliText('cli.store.download.saved', { path: filePath }));
410
+ return { exitCode: 0, filePath };
411
+ }
412
+ if (response.status !== 404 || !creds) {
413
+ log(cliText('cli.store.download.httpFailed', { status: response.status }));
414
+ return { exitCode: 1 };
415
+ }
416
+ } catch (error) {
417
+ log(cliText('cli.store.download.failed', { message: error instanceof Error ? error.message : String(error) }));
418
+ return { exitCode: 1 };
419
+ }
420
+ }
355
421
  if (!creds) {
356
422
  log(cliText('cli.store.status.disconnectedGuide'));
357
423
  return { exitCode: 1 };
@@ -410,8 +476,8 @@ export async function runStoreCommand(args, options = {}) {
410
476
  log(cliText('cli.store.help.installUsage'));
411
477
  return { exitCode: 1 };
412
478
  }
413
- const hasFrom = args.includes('--from');
414
- const fromZip = parseFlag(args, '--from');
479
+ const hasFrom = args.includes('--from') || args.includes('--from-file');
480
+ const fromZip = parseFlag(args, '--from-file') ?? parseFlag(args, '--from');
415
481
  if (hasFrom && !fromZip) {
416
482
  log(cliText('cli.store.help.installUsage'));
417
483
  return { exitCode: 1 };
@@ -549,7 +615,7 @@ export async function runStoreCommand(args, options = {}) {
549
615
  return { exitCode: 1 };
550
616
  }
551
617
 
552
- const hadInstalledKit = readKitsLedger(home).kits.length > 0;
618
+ const hadSkillKit = readKitsLedger(home).kits.some(kit => Array.isArray(kit.skills) && kit.skills.length > 0);
553
619
  const assetLinks = linkKitAssets(kitDir, manifest, home, {
554
620
  env,
555
621
  assets: options.assets,
@@ -562,6 +628,8 @@ export async function runStoreCommand(args, options = {}) {
562
628
  for (const blocker of skillLinks.blockers) log(cliText('cli.store.install.blocked', { blocker }));
563
629
  return { exitCode: 1 };
564
630
  }
631
+ const playbookLinks = linkKitPlaybooks(kitDir, manifest, home, { platform: options.platform });
632
+ for (const warning of playbookLinks.warnings) log(cliText('cli.store.install.warning', { warning }));
565
633
  registerKitAssets(home, manifest, kitDir, assetLinks.items, env);
566
634
  writeKitsLedger(home, {
567
635
  id: manifest.id,
@@ -569,10 +637,11 @@ export async function runStoreCommand(args, options = {}) {
569
637
  installedAt: new Date().toISOString(),
570
638
  kitDir,
571
639
  skills: skillLinks.linked,
640
+ playbooks: playbookLinks.linked,
572
641
  assets: assetLinks.linked
573
642
  });
574
643
  ensureKitsMarketplace(home);
575
- if (!hadInstalledKit) log(enableHint());
644
+ if (skillLinks.linked.length > 0 && !hadSkillKit) log(enableHint());
576
645
  }
577
646
  const packPath = findFile(destDir, 'PACK.json');
578
647
  if (packPath) {
@@ -91,6 +91,10 @@
91
91
  "docs/contract-2026-10-11-ui-i18n.md",
92
92
  "docs/contract-2026-10-12-caption-look-v1.md",
93
93
  "docs/contract-2026-10-15-playbook-v0.md",
94
+ "docs/contract-2026-10-15-project-manifest-v0.md",
95
+ "docs/contract-2026-10-15-script-lines-v0.md",
96
+ "docs/contract-2026-10-16-automation-gates-v0.md",
97
+ "docs/contract-2026-10-16-recipes-and-connectors-v0.md",
94
98
  "packages/akari-ear/package.json",
95
99
  "packages/akari-ear/README.md",
96
100
  "packages/akari-launcher/package.json",
@@ -8,6 +8,7 @@
8
8
  ~/.akari/assets/store/<productId>/
9
9
  ├── manifest.json
10
10
  ├── skills/<skill-name>/SKILL.md
11
+ ├── playbooks/<id>/{playbook.json, playbook.md, preview/}
11
12
  ├── templates/<name>.json
12
13
  ├── assets/<category>/<id>/{meta.json, …}
13
14
  ├── docs/*.md
@@ -35,6 +36,7 @@
35
36
  "products": ["akari-pop-motion-set"]
36
37
  },
37
38
  "skills": [{ "dir": "skills/design-world", "name": "design-world" }],
39
+ "playbooks": [{ "dir": "playbooks/world-intro" }],
38
40
  "templates": [{ "path": "templates/paper-to-browser.json", "for": "world-map", "label": "紙の地図 → ブラウザの中" }],
39
41
  "assets": [{ "category": "overlay", "id": "world-far-bands" }],
40
42
  "docs": [{ "path": "docs/world-hen.md", "label": "ワールド編(抜粋)" }],
@@ -44,6 +46,7 @@
44
46
  ```
45
47
 
46
48
  必須フィールドは `schemaVersion`、`id`、`kind`、`name`、`version`、`requires.cli`、`license`。`kind` は v0 では `kit` のみ。`validate-kit-manifest.mjs` はスキル実体、素材メタデータ、テンプレート用途語彙を含めて検査する。
49
+ 任意の `playbooks[]` は `playbooks/<id>` を指し、各ディレクトリに playbook-v0 形式の `playbook.json` と `playbook.md`、任意の `preview/` を置く。
47
50
 
48
51
  ## 3. 展開
49
52
 
@@ -53,8 +56,9 @@
53
56
  2. `assets[]` を `~/.akari/assets/<category>/<id>` へ相対 symlink で公開する。各素材はリンク前に `validate-asset.mjs` で検査する。
54
57
  素材の実体ファイルと checksum は `~/.akari/assets/installed.json` にも登録し、素材 id から解決できるようにする。
55
58
  3. `skills[]` を `~/.akari/kits/plugin/skills/<name>` へ相対 symlink で公開する。
56
- 4. `~/.akari/kits/installed.json` に id、version、導入日時、展開先、スキル、素材を記録する。
57
- 5. `templates[]` は移動せず、CLI が各展開先の manifest を列挙して読む。
59
+ 4. `playbooks[]` を `~/.akari/playbooks/<id>` へ相対 symlink で公開する。同じ id が手元に実体であれば上書きせず警告して飛ばす。
60
+ 5. `~/.akari/kits/installed.json` に id、version、導入日時、展開先、スキル、素材、公開した作り方を記録する。uninstall では記録した作り方の symlink も外す。
61
+ 6. `templates[]` は移動せず、CLI が各展開先の manifest を列挙して読む。
58
62
 
59
63
  manifest が無い商品は従来の素材商品として扱う。検査器や runtime registry が npm 配布物に同梱されておらず照合できない場合は、その検査だけを警告付きでスキップする。`akari store uninstall <productId>` は台帳に記録した symlink と台帳エントリを外し、再導入用の展開ディレクトリは残す。
60
64
 
@@ -48,7 +48,10 @@ updated: 2026-10-15
48
48
  | 確認シート | `<project>/.akari/reports/playbook-sheet.html`(読み取り専用・派生) | `akari new --from` |
49
49
 
50
50
  - 一覧の解決順は **チャンネル → 手元** の 2 択だけ(`create-project` の雛形解決と同じ規律。他の経路を作らない)
51
- - `preview/` は任意。`cover.png` か `sample.mp4` を 1 つまで。無ければ画面は文字だけで出す
51
+ - Lab で入れた作り方は手元の棚に symlink で入る。解決順は 2 択のまま、画面の入手先は `~/.akari/kits/installed.json` の `playbooks` に載っていれば Lab と表示する。
52
+ - Lab の無料の作り方は商品データベースに載せない。無料 R2 の公開カタログ(`/assets/catalog.json`)の `category: playbook` の行の `files[0].key` を `/assets/<key>` から取り、`sha256` を照合してから入れる(`akari store install <id>`)
53
+ - `preview/` は任意(下の項)
54
+ - `preview` のパスはすべて **playbook.json からの相対**。棚(手元・チャンネル)では `preview/cover.png`(任意)と `preview/frames/01.png`〜`05.png`(重要な場面のコマ 3〜5 枚・長辺 640px 以下・作り方の画面比)を置く。表紙が無ければ `frames` の 1 枚目を表紙にする。動画(sample.mp4)は持たない(重いため)。プロジェクトの中では `planning/playbook-preview/frames/` に置く
52
55
  - `<id>` は英小文字・数字・ハイフン。同じ `id` は版で区別する(`version` 整数)
53
56
 
54
57
  ## 2. 形 — playbook.json
@@ -94,6 +97,8 @@ updated: 2026-10-15
94
97
  - 必須: `version`(0 固定。原則 1)・`kind`(`"akari-playbook"`)・`id`・`name`・`playbookVersion`・`depth`・
95
98
  `category`・`steps[]`・`settings`・`license`・`provenance`。`tags` / `requires` / `placeholders` / `parts` は
96
99
  省略可(depth 1 では `placeholders` と `parts` を持たない)
100
+ - 任意: `preview`(`{ "cover"?: string, "frames"?: string[] }`。パスは上の規則)・`example`(`true` なら見本として配る例。画面に「例」の印を出す)
101
+ - 任意: `beats`(`{ id, at, duration, title, note?, placeholder? }[]`・秒。作り方の内訳。始めたとき空のキャンバスと台本の予定の行になる。script-lines-v0 §5)
97
102
  - `category` は 1 つだけ。語彙は閉じる: `film` / `short-drama` / `mv` / `explainer` / `product` / `vlog` / `cm` /
98
103
  `education` / `clip` / `logo`。足すときは本表に追記してから
99
104
  - `tags` の 8 軸は開いた語彙(自由に足せる)。軸の名前だけ固定: `form` / `length` / `technique` / `tools` /
@@ -118,6 +123,7 @@ updated: 2026-10-15
118
123
  ## 流れ ← steps を番号つきで。確認点は「☑ ここで確認」と明記
119
124
  ## 設定値 ← settings を表で(キー / 値 / 意味)
120
125
  ## 必要な素材(仮枠) ← placeholders を表で(役割 / 種類 / 尺の目安 / 見本)
126
+ ## 内訳 ← beats を表で(時刻 / 見出し / メモ / 仮枠)
121
127
  ## 使った部品 ← parts を表で(種類 / 名前 / 入手先 / ライセンス)
122
128
  ## 要る道具 ← requires(CLI の版・道具・スキル)。無いときの代案を 1 行
123
129
  ## ライセンス ← license と、外部由来を仮枠にした旨
@@ -125,6 +131,7 @@ updated: 2026-10-15
125
131
  ```
126
132
 
127
133
  章の追加は末尾のみ(additive-only)。章の名前は上の表記で固定し、読み手はこの見出しで章を引く。
134
+ 例外として「内訳」の 1 章だけは「必要な素材(仮枠)」の直後に置く。
128
135
 
129
136
  ## 4. 記録する — プロジェクト → 作り方(`record-playbook` スキル)
130
137
 
@@ -0,0 +1,186 @@
1
+ ---
2
+ lifecycle: accepted
3
+ created: 2026-10-15
4
+ updated: 2026-10-16
5
+ ---
6
+
7
+ # project-manifest-v0(プロジェクトの依存グラフ)契約
8
+
9
+ - 日付: 2026-10-15
10
+ - 状態: **accepted**(2026-10-16。同日の裁定で「作品」の器を廃し、プロジェクトで閉じる形に改訂。開けた問い 2 つは §8 の既定で確定)
11
+ - 前提: `contract-2026-07-17-data-contract-versioning.md`(版管理三原則。本契約は初版から適用する)/
12
+ `contract-2026-09-02-asset-reference-model.md`(プロジェクトの参照台帳 `.akari/asset-references.json`)/
13
+ `contract-2026-09-24-style-v0.md`(スタイルの利用台帳 `.akari/style-usage.json`)/
14
+ `contract-2026-10-10-channel-people-design-v0.md`(チャンネルの人物 `people.json`・デザイン素材)/
15
+ `contract-2026-07-25-project-structure-v0.md`(プロジェクト内の置き場所)/
16
+ `contract-2026-09-13-world-map-v0.md`(ワールドマップ `planning/world-map.json`。世界観がプロジェクトの中に置かれる先例)
17
+ - 発端: 1 本の動画を超えるもの(複数のシーン・ワールド・人物・外の道具の出力を束ねるもの)を
18
+ 扱うとき、「何が何を使っているか」だけを 1 枚で持ちたい。edit.json は 1 本の並びの正本であり、
19
+ シーンをまたぐ広がりは持たない。参照台帳と利用台帳は素材・スタイルに限られ、シーン・ワールド・
20
+ 人物・道具の出力を表せない。**単位は増やさない**: 器は従来どおりプロジェクト 1 つで、
21
+ シーンはその中に 1 段だけ入れ子にする(「作品」という別の器は作らない)
22
+ - スコープ: プロジェクト単位の依存グラフ `manifest.json` の**置き場・形・語彙・解決規則・派生物**と、
23
+ シーンを持つプロジェクトの最小の構成(§1)。既存の edit.json / 参照台帳 / 利用台帳は変えない(§5)。
24
+ **相関図や構成の木の UI 実装、Lab への公開形式、外の道具との自動連携、既存プロジェクトの一括移行、
25
+ アプリが入れ子の edit.json を開く実装は非スコープ**(§7)
26
+
27
+ ## 0. 位置づけ — 持つのは「関係」だけ
28
+
29
+ manifest は「このプロジェクトに、何が、どの版で関わっているか」の目録である。実体(動画・3D・
30
+ Blender のファイル)は持たず、編集の中身(カットの位置・字幕)も持たない。
31
+
32
+ | 正本 | 持つもの |
33
+ |---|---|
34
+ | `edit.json` | 1 本(1 シーン)の中の並び(カット・素材の使用範囲・字幕の時刻) |
35
+ | `.akari/manifest.json`(本契約) | プロジェクトに関わるものの一覧(nodes)と、誰が誰を使うかの線(edges) |
36
+ | `.akari/asset-references.json` | 参照する素材の `{category, id}`(従来どおり) |
37
+ | `.akari/style-usage.json` | 当てたスタイルの `style_uid` と `revision`(従来どおり) |
38
+
39
+ manifest から派生するもの(読み取り専用。正本に書き戻さない):
40
+ 相関図(HTML・inline SVG)/ 構成の木(プロジェクト → シーン → カット)/ 取り込む前の確認シート
41
+ (件数・入手先・要る道具・大きさ)/ 壊れ検査(§4)/ 作り方(playbook)の「使った部品」の章。
42
+
43
+ ## 1. 置き場 — プロジェクトで閉じる
44
+
45
+ ```text
46
+ <project>/ ← 器はこれ 1 つ(従来どおり)
47
+ ├─ edit.json captions.json ← 1 本のプロジェクトはここに(従来どおり・変更なし)
48
+ ├─ planning/ assets/ .akari/ ← project-structure-v0 のまま
49
+ ├─ scenes/ ← シーンを持つプロジェクトだけ。入れ子はこの 1 段だけ
50
+ │ ├─ scene-01/ edit.json captions.json assets/ .akari/ ← 中身は 1 本のプロジェクトと同じ構成
51
+ │ ├─ scene-02/ …
52
+ │ └─ scene-03/ … assets/scene3d/<id>/{scene.py, params} ← 道具のレシピ(手で開く道具だけ tools/)
53
+ ├─ world/<id>/world.json ← 世界観(置き場・小物・色。中身は別契約・未起票)
54
+ ├─ people.json ← このプロジェクトの人物。無ければチャンネルの people.json
55
+ └─ .akari/manifest.json ← 本契約。プロジェクトの外を指さない
56
+ ```
57
+
58
+ - manifest は `<project>/.akari/manifest.json` の 1 箇所。**プロジェクトの外(他のプロジェクト・
59
+ チャンネルの別フォルダ)を `where` で指さない**。プロジェクトをまたいで共有したいもの
60
+ (世界・人物・スタイル・キット)はライブラリかチャンネル経由の参照(`source`)で持ち、
61
+ 実体はプロジェクトに取り込むか、ライブラリで解決する
62
+ - `scenes/<scene>/` の中は 1 本のプロジェクトと同じ構成で、`edit.json` を持つ。
63
+ **`scenes/` の下にさらに `shots/` `layers/` のような層を作らない**(入れ子は 1 段まで)
64
+ - **シーンを開く**: アプリはシーンのフォルダ `scenes/<scene>/` をそのまま作業フォルダとして開く(中身が 1 本の
65
+ プロジェクトと同じ構成なので、edit.json・captions.json・assets/ の解決は 1 本のときと同じ)。
66
+ containment の根は**シーンのフォルダ**のまま(1 本のプロジェクトと同じ。`..` で外へ出る参照は従来どおり拒否)。
67
+ プロジェクト共通の `assets/` の素材をシーンで使うときは、そのシーンの `assets/` へ**ハードリンクで取り込む**
68
+ (同じボリュームでなければ複写)。manifest には `scene:<id>` → `asset:<category>/<id>` の線を 1 本足し、
69
+ どのシーンが共通の素材を使っているかは manifest で見る(`../../assets/` での直接参照は v0 では作らない)。
70
+ シーンのフォルダかどうかは「親の親に `.akari/` があり、親の名前が `scenes`」で判定する
71
+ - 1 本のプロジェクトは `scenes/` を持たず、従来どおりルートの `edit.json` で動く。
72
+ manifest は無くてもよい(§5)
73
+ - 外の道具は 2 種に分ける。**エージェントが回せる道具**(Blender の bpy・生成 AI の API・ffmpeg)は
74
+ レシピが正本で、出力は再生成できるキャッシュ(`contract-2026-07-14-3d-bake-recipe.md`。
75
+ レシピは `assets/scene3d/<id>/`、出力は通常の素材)。**人が手で開く道具**だけ
76
+ `<project または scene>/tools/<tool>/` に入力の覚え書き・出力ファイル(またはそのリンク)・記録を
77
+ 置いて往復する。どちらも道具の作業フォルダ本体は置かない
78
+ - `assets/` は平らに置く。AI がフォルダを切らない。意味づけ・分析は `analysis.json` と `meta.json` の側に持ち、
79
+ 画面の分類はそこから派生で描く
80
+
81
+ ## 2. 形
82
+
83
+ ```json
84
+ {
85
+ "version": 0,
86
+ "kind": "akari-project-manifest",
87
+ "project": { "id": "hoshi-no-oji", "name": "星の王子さま", "revision": 3 },
88
+ "nodes": [
89
+ { "id": "world:desert", "kind": "world", "rev": 2, "where": "world/desert" },
90
+ { "id": "person:prince", "kind": "person", "rev": 5, "where": "people.json#p-prince" },
91
+ { "id": "scene:01", "kind": "scene", "rev": 1, "where": "scenes/scene-01" },
92
+ { "id": "asset:still/rose", "kind": "asset", "rev": 1, "source": "lab", "category": "still" },
93
+ { "id": "style:caption-a", "kind": "style", "rev": 2, "uid": "st_01HZ…" },
94
+ { "id": "tool:blender/s01", "kind": "tool-output", "rev": 1, "tool": "Blender 4.2",
95
+ "where": "scenes/scene-01/assets/generated/<id>.mp4" }
96
+ ],
97
+ "edges": [
98
+ { "from": "scene:01", "uses": "world:desert" },
99
+ { "from": "scene:01", "uses": "person:prince" },
100
+ { "from": "scene:01", "uses": "tool:blender/s01" },
101
+ { "from": "tool:blender/s01", "uses": "person:prince", "note": "レンダーに使ったモデル" }
102
+ ]
103
+ }
104
+ ```
105
+
106
+ - `version`: 整数。初版は 0(原則 1)。`kind` は固定文字列 `"akari-project-manifest"`
107
+ - `project`: 任意。`id` は英小文字・数字・ハイフン。`revision` はプロジェクトの版(整数・増えるだけ)
108
+ - `nodes[]`: 必須。各要素は `id`・`kind`・`rev` が必須。`where` は**プロジェクトルートからの相対パス**
109
+ (`..` を含めない。`people.json#<id>` のようなフラグメント指定を許す)。
110
+ `source` は `builtin` / `lab` / `own` / `site` / `ai` / `channel`(入手先。参照台帳の
111
+ `sourceKind` と同じ語彙 + `ai` + `channel`)。`tool` は道具の名前と版の自由文。`tool-output` は任意で `recipe`(作ったレシピの `<id>@<版>`)・
112
+ `connector`(呼んだ接続〈サービス・ソフトウェア〉の名前と版)・`inputs_sha256`(入力のハッシュ)を持てる(再現と壊れ検査用)
113
+ - `edges[]`: 必須(空でもよい)。`from` と `uses` は nodes の `id`。`note` は任意。
114
+ 同じ `{from, uses}` の重複は持たない。自己参照と循環は持たない(§4 で検査)
115
+ - 並び: nodes は `kind` → `id`、edges は `from` → `uses` の安定ソート。書き込みは
116
+ tmp + rename の原子的更新。読み手は tolerant reader(未知フィールドは保持、欠損は既定値)
117
+
118
+ ## 3. 語彙(kind と id の接頭辞)
119
+
120
+ | kind | id の形 | 指すもの | 版の正本 |
121
+ |---|---|---|---|
122
+ | `project` | `project:<id>` | プロジェクトそのもの(親を指すときだけ使う) | manifest の `project.revision` |
123
+ | `scene` | `scene:<id>` | `scenes/<id>/`(中は 1 本のプロジェクトと同じ構成) | そのシーンの `edit.json` の更新 |
124
+ | `world` | `world:<id>` | 世界観(場所・小物・色の束。`world/<id>/`) | `world.json` の revision(別契約・未起票) |
125
+ | `person` | `person:<id>` | `people.json` の 1 件(人・アバター・団体)。プロジェクトに無ければチャンネル | `people.json` の `edited` |
126
+ | `asset` | `asset:<category>/<id>` | 素材(参照台帳と同じ `{category, id}`) | `.meta.json` の版 |
127
+ | `style` | `style:<id>` | スタイル(`style_uid` と `revision`) | `style.json` の revision |
128
+ | `kit` | `kit:<channel>` | チャンネルのキット(将来) | kit の revision |
129
+ | `playbook` | `playbook:<id>` | 作り方(将来) | playbook の version |
130
+ | `tool-output` | `tool:<tool>/<id>` | 外の道具が出したファイル(レシピから焼いたものは provenance を持つ) | レシピ由来は provenance の版、手作業は `rev` を手で上げる |
131
+ | `doc` | `doc:<name>` | 台本・ストーリーボード・企画書など読む文書 | ファイルの mtime |
132
+
133
+ 新しい kind は**この表に追記してから**使う。未知の kind を読んだ読み手は捨てず保持し、
134
+ 相関図では「その他」として描く(原則 3)。
135
+
136
+ ## 4. 解決と検査
137
+
138
+ - 解決: `where` はプロジェクトルートからの相対で解決する。`person` と `kit` だけは、プロジェクトに
139
+ 無ければチャンネル(`channels/<channel>/`)で再解決してよい(`source: channel`)。`asset` は
140
+ 参照台帳と同じ順(プロジェクト実体 → 共有ライブラリ)で解決する。どの経路でも字句・realpath の
141
+ containment を満たさないパスは fail-closed で拒否する
142
+ - 検査(`akari project check` を想定。exit 非 0 は要修正):
143
+ 1. `edges` の `from` / `uses` が nodes に無い → 欠落
144
+ 2. `where` の実体が無い → 未取得(`asset` で `source: lab` なら「未取得」、それ以外は「欠落」)
145
+ 3. 参照先の版が node の `rev` より新しい → 古い(情報。自動では直さない)
146
+ 4. 自己参照・循環 → 壊れ
147
+ 5. `source: site` または `own` で外部の実体を含む node が、公開用の写し(§6)に残っている → 公開不可
148
+ - 検査結果は `.akari/reports/manifest-check.json` に書き、HTML の派生と画面の帯はそこから描く
149
+
150
+ ## 5. 既存ファイルとの関係(追加のみ・移行なし)
151
+
152
+ - `.akari/asset-references.json` と `.akari/style-usage.json` は**そのまま**。manifest の
153
+ `asset` / `style` の nodes は、この 2 つから機械的に生成できる(`akari project sync`。
154
+ シーンを持つプロジェクトでは各 `scenes/<id>/.akari/` の台帳を束ねる)。
155
+ 手で足した nodes(world・person・tool-output・doc)は sync で消さない
156
+ - `edit.json` は変えない。`scene` の node から `scenes/<id>/edit.json` を開けばカットの並びが見える
157
+ - 1 本のプロジェクトでは manifest を作らなくてもよい(無ければ 2 つの台帳だけで従来どおり動く)
158
+
159
+ ## 6. 公開用の写し
160
+
161
+ 作り方やプロジェクトを手元の外へ出すときは、manifest の**公開用の写し**を別に作る。写しでは
162
+ `where` を落として `id`・`kind`・`rev`・`source` だけを残し、`source: own` / `site` の
163
+ node は仮枠(`placeholder: true`)に置き換える。写しの形は本契約と同じ(`version: 0`)。
164
+ どの node を仮枠にするかの規則と、見本の扱いは作り方(playbook)側の契約で定める(未起票)。
165
+
166
+ ## 7. 非スコープ
167
+
168
+ - 相関図・構成の木の画面(派生の描き方は別途)
169
+ - アプリが `scenes/<id>/edit.json` を開く実装(現状はルートの `edit.json` のみ)
170
+ - Lab での公開形式・取り込みの UI
171
+ - 外の道具(Blender 等)との自動連携・起動
172
+ - 既存プロジェクトへの manifest の一括生成
173
+ - `world` の中身の契約(`world.json`)と `kit` / `playbook` の契約
174
+
175
+ ## 8. 裁定済みと開けた問い
176
+
177
+ - 裁定(2026-10-15): 器はプロジェクト 1 つ。「作品」という別の器は作らず、シーンは `scenes/` に
178
+ 1 段だけ入れ子にする。manifest はプロジェクトの外を指さない
179
+ - 確定(2026-10-16。次の既定を採用):
180
+ 1. `person` の版は v0 では `people.json` の `edited`(ISO 時刻)を整数に写さず、node に `edited` を
181
+ そのまま持たせて比べる(`rev` は sync が `edited` の変化ごとに 1 上げる)。専用の revision は足さない
182
+ 2. `tool-output` の `rev` は手で上げてよいが、node に任意の `sha256` を持たせ、sync は実体のハッシュが
183
+ 変わったら `rev` を 1 上げる(手作業の上げ忘れを拾う)
184
+ - CLI: `akari project sync`(台帳 → nodes の生成・手で足した nodes は保持)/ `akari project check`
185
+ (§4 の検査 → `.akari/reports/manifest-check.json`・exit 非 0 は要修正)/ `akari project scene add <id>`
186
+ (`scenes/<id>/` を 1 本のプロジェクトと同じ雛形で作り、manifest に `scene` の node を足す)
@@ -0,0 +1,67 @@
1
+ ---
2
+ lifecycle: accepted
3
+ created: 2026-10-15
4
+ updated: 2026-10-15
5
+ ---
6
+
7
+ # script-lines-v0(台本の「予定」と「メモ」の行)契約
8
+
9
+ - 日付: 2026-10-15
10
+ - 状態: **v0 受理(2026-10-15 オーナー GO)**
11
+ - 前提: `contract-2026-07-25-project-structure-v0.md`(`planning/` は企画・プランの層)/ `contract-2026-09-24-caption-runs-v0.md`(字幕は `captions.json`)/
12
+ `contract-2026-10-15-playbook-v0.md`(作り方。本契約の「予定」は作り方の内訳 `beats` から作られる)/ `contract-2026-09-25-canvas-v0.md`(空のキャンバス = 区間を持つ仮枠)
13
+ - 発端: 台本パネルは字幕の行しか持たず、「ここに何を入れる予定か」「気づいたメモ」を時刻つきで残せない。作り方の内訳(何秒に何を)も、台本とタイムラインに出す場所が無い
14
+ - スコープ: 予定とメモの行の **置き場・形・台本パネルとタイムラインでの見せ方・作り方からの生成**
15
+ - 非スコープ: 字幕(`captions.json`)の形の変更 / 予定の行から自動でカットを作ること / 複数人の同時編集
16
+
17
+ ## 0. 位置づけ
18
+
19
+ 台本の行は 3 種類。**字幕**(`captions.json`。今のまま)・**予定**(ここに何を入れるつもりか)・**メモ**(気づき・指示)。
20
+ 予定とメモは `planning/script.json` に持ち、台本パネルは 3 種を時刻順に 1 本の列で見せる。字幕は今までどおり `captions.json` が正本で、本契約は触らない。
21
+
22
+ ## 1. 置き場
23
+
24
+ - `<project>/planning/script.json`(無ければ予定もメモも無い。台本パネルは字幕だけで今までどおり動く)
25
+ - シーンを持つプロジェクト(project-manifest v0)では `scenes/<scene>/planning/script.json`。台本パネルは開いているシーンの 1 本を読む
26
+
27
+ ## 2. 形
28
+
29
+ ```json
30
+ {
31
+ "version": 0,
32
+ "kind": "akari-script",
33
+ "lines": [
34
+ { "id": "p-hook", "kind": "plan", "at": 0, "duration": 3, "text": "つかみ: 結論を先に言う", "placeholder": "talking-head", "beat": "hook" },
35
+ { "id": "p-body", "kind": "plan", "at": 3, "duration": 40, "text": "本題: 3 つのポイント", "placeholder": "screen", "done": false },
36
+ { "id": "m-01", "kind": "memo", "at": 12.5, "text": "ここで図解を差し込む" }
37
+ ]
38
+ }
39
+ ```
40
+
41
+ - `version`: 整数 0(原則 1)。`kind`: 固定 `"akari-script"`
42
+ - `lines[]`: 必須(空でよい)。各行は `id`(ファイル内で一意)・`kind`(`plan` / `memo`)・`at`(秒・0 以上)・`text`(1〜500 文字)が必須
43
+ - 任意: `duration`(秒・0 より大。予定の区間)/ `placeholder`(作り方の仮枠の `id`)/ `beat`(作り方の内訳の `id`)/ `done`(予定が済んだか)/ `author`(`user` / `ai` / `playbook`)
44
+ - 並び: `at` → `id` の安定ソートで保存。書き込みは tmp + rename。読み手は tolerant(未知フィールドは保持、壊れた行は捨てて残りを読む)
45
+ - スキーマ: `packages/schemas/script.schema.json`(closed object・additive-only)と `validate-script.mjs`
46
+
47
+ ## 3. 台本パネル(右ドックの「台本」)
48
+
49
+ - 字幕・予定・メモを `at` の順に 1 本の列で出す。行の頭に種類の印(字幕 / 予定 / メモ)。予定は `duration` があれば区間(`0:00–0:03`)を出し、`done` はチェックで切り替える
50
+ - 「+ メモ」で今の再生位置にメモを足す(`author: user`)。予定・メモの文字はその場で直せる。削除はメニューから
51
+ - 行を押すとその時刻へ移動(字幕の行と同じ)。字幕の既存の操作(字幕を作る・カットを整える・QC)は変えない
52
+ - 表示の切り替え: 「予定とメモを出す」のオン / オフ(既定オン)。オフなら今までの字幕だけの台本
53
+
54
+ ## 4. タイムライン
55
+
56
+ - タイムラインの最上段に「台本」の段を 1 本足し、予定を区間の帯、メモを点の印で出す(`script.json` が無ければ段を出さない)。押すとその時刻へ移動。ここでは編集しない(直すのは台本パネル)
57
+
58
+ ## 5. 作り方からの生成
59
+
60
+ - 作り方(playbook.json)の任意項目 `beats[]`(`{ id, at, duration, title, note?, placeholder? }`・秒)を、`applyPlaybook` が
61
+ 予定の行(`kind: plan`・`text` = `title`(`note` があれば「title: note」)・`author: playbook`)として `planning/script.json` に書く。
62
+ 同時に、内訳 1 つ = 空のキャンバス 1 つ(名前 = title・`canvas.intent` = note か title)にして仮枠の段に並べる(`beats` が無いときは従来どおり仮枠ごとに 1 つ)
63
+ - `record-playbook` は、終わったプロジェクトの `planning/script.json` の予定の行(あれば)と章・見出しから `beats` を起こす
64
+
65
+ ## 6. 開けた問い
66
+
67
+ 1. 予定の行を「済」にしたら、対応する空のキャンバスを畳むか(v0 は何もしない)