akari-video 0.1.13 → 0.1.14

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 (42) hide show
  1. package/bin/akari.mjs +8 -3
  2. package/package.json +1 -1
  3. package/src/cli.mjs +12 -7
  4. package/src/messages.mjs +26 -2
  5. package/src/update-check.mjs +66 -25
  6. package/vendor/.akari-capability-sources.json +1 -0
  7. package/vendor/docs/contract-2026-07-25-r6-audio-tracks-and-trim.md +1 -1
  8. package/vendor/docs/contract-2026-08-23-stroke-persistence.md +75 -0
  9. package/vendor/packages/akari-launcher/package.json +1 -1
  10. package/vendor/packages/decision-cards/README.md +12 -0
  11. package/vendor/packages/decision-cards/package.json +3 -2
  12. package/vendor/packages/edit-lint/src/edit-lint.mjs +41 -60
  13. package/vendor/packages/edit-store/lib/cut-adjacency.d.ts +19 -0
  14. package/vendor/packages/edit-store/lib/cut-adjacency.js +28 -0
  15. package/vendor/packages/edit-store/lib/edit-v2-item-write.d.ts +53 -0
  16. package/vendor/packages/edit-store/lib/edit-v2-item-write.js +162 -0
  17. package/vendor/packages/edit-store/lib/index.d.ts +3 -0
  18. package/vendor/packages/edit-store/lib/index.js +3 -0
  19. package/vendor/packages/edit-store/lib/migrate/index.js +1 -1
  20. package/vendor/packages/edit-store/lib/track-transition-compatibility.d.ts +23 -0
  21. package/vendor/packages/edit-store/lib/track-transition-compatibility.js +76 -0
  22. package/vendor/packages/edit-store/lib/write-gate.d.ts +2 -0
  23. package/vendor/packages/edit-store/lib/write-gate.js +11 -1
  24. package/vendor/packages/overlay-runtime/package.json +2 -1
  25. package/vendor/packages/pen-visuals/package.json +1 -0
  26. package/vendor/packages/schemas/bin/validate-research-plan.mjs +55 -7
  27. package/vendor/packages/schemas/fixtures/review/valid-strokes/review.json +1 -1
  28. package/vendor/packages/schemas/research-plan.schema.json +24 -2
  29. package/vendor/packages/schemas/review.schema.json +18 -0
  30. package/vendor/packages/schemas/test/fixtures/research-plan/invalid-nested-cutaway/research-plan.json +25 -0
  31. package/vendor/packages/schemas/test/fixtures/research-plan/valid-legacy-without-shot-ids/research-plan.json +29 -0
  32. package/vendor/packages/schemas/test/fixtures/research-plan/valid-visual-storyboard/research-plan.json +72 -0
  33. package/vendor/packages/schemas/test/validate-research-plan.test.mjs +18 -0
  34. package/vendor/skills/address-review/bin/list.mjs +12 -0
  35. package/vendor/skills/address-review/dev-fixtures/fixture-project/review.json +3 -0
  36. package/vendor/skills/address-review/test/review-store.test.mjs +6 -0
  37. package/vendor/skills/compile-review-session/bin/core/compiler.mjs +5 -0
  38. package/vendor/skills/compile-review-session/test/compiler.test.mjs +9 -0
  39. package/vendor/skills/compile-review-session/test/ui-events.test.mjs +1 -0
  40. package/vendor/skills/edit-plan/beat-sync.md +17 -3
  41. package/vendor/skills/research-plan/SKILL.md +1 -1
  42. package/vendor/skills/research-plan/storyboard.md +30 -1
package/bin/akari.mjs CHANGED
@@ -11,14 +11,19 @@ import { runCapabilityCommand } from '../src/capability-command.mjs';
11
11
  import { runStoreCommand } from '../src/store-command.mjs';
12
12
  import { runAssetsCommand } from '../src/assets-command.mjs';
13
13
  import { runMigrateCommand } from '../src/migrate-command.mjs';
14
- import { maybeApplyPendingUpdateOnLaunch, readOwnVersion } from '../src/update-check.mjs';
15
- import { describeCliHelp } from '../src/messages.mjs';
14
+ import { maybeApplyPendingUpdateOnLaunch, resolveInstalledVersionInfo } from '../src/update-check.mjs';
15
+ import { describeCliHelp, describeInstalledVersions } from '../src/messages.mjs';
16
16
 
17
17
  // `akari --version` / `-v`: インストール済みの版を表示するだけの最小コマンド
18
18
  // (タスク契約 2026-08-11-update-u4-cli-self-update の受け入れ条件 —
19
19
  // `akari update` / `--rollback` 後にインストール先の版を観測する手段として必要)。
20
20
  async function printVersion() {
21
- console.log(`v${readOwnVersion()}`);
21
+ const versionInfo = resolveInstalledVersionInfo({ env: process.env });
22
+ // 1 行目は update / rollback の既存機械観測契約として CLI 版だけを維持する。
23
+ console.log(`v${versionInfo.cliVersion}`);
24
+ for (const line of describeInstalledVersions(versionInfo)) {
25
+ console.log(line);
26
+ }
22
27
  return { exitCode: 0 };
23
28
  }
24
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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/cli.mjs CHANGED
@@ -6,7 +6,7 @@ import { resolveLauncherAssets } from './repo-assets.mjs';
6
6
  import { detectProjectState } from './project-state.mjs';
7
7
  import { findClaudeExecutable, findOpencodeExecutable } from './path-lookup.mjs';
8
8
  import { loadTaskLabels } from './task-labels.mjs';
9
- import { describeInstalledVersions, describeIntake, claudeMissingGuidance, opencodeMissingGuidance, describeUpdateCommand, describeVersionStatus, formatUpdateNotice } from './messages.mjs';
9
+ import { describeForceReinstall, describeInstalledVersions, describeIntake, claudeMissingGuidance, opencodeMissingGuidance, describeUpdateCommand, describeVersionStatus, formatUpdateNotice } from './messages.mjs';
10
10
  import { resolveEffectiveProjectRoot } from './first-run.mjs';
11
11
  import { maybeShowAssetIntroNotice } from './sounds-setup.mjs';
12
12
  import {
@@ -14,9 +14,9 @@ import {
14
14
  compareVersions,
15
15
  isValidFeedShape,
16
16
  readCacheSync,
17
- readInstalledAppVersion,
18
17
  readOwnVersion,
19
18
  recordDismissalSync,
19
+ refreshUpdateFeed,
20
20
  resolveCachePath,
21
21
  resolveInstalledVersionInfo,
22
22
  triggerBackgroundRefresh
@@ -188,8 +188,8 @@ function defaultSpawnOpencode(opencodePath, args, projectRoot) {
188
188
  * 自己更新を試みず、キャッシュに載っている最新版の通知を今後出さないよう記録するだけ
189
189
  * (既存挙動を維持)。`--force` は同じ版の本体も再導入し、`--rollback` は直前 1 世代
190
190
  * (`~/.akari/app-previous/`)へ戻す。
191
- * ネットワークに触れるのは自己更新の DL 区間のみ — フィード自体は既存キャッシュ由来
192
- * (最新情報は `akari` 起動時のバックグラウンド fetch で更新される)。
191
+ * 通常時にネットワークへ触れるのは自己更新の DL 区間のみ。`--force` だけはキャッシュが
192
+ * 未取得なら、復旧経路を塞がないためフィードの同期取得を 1 回試す。
193
193
  */
194
194
  export async function runUpdateCommand(args, options = {}) {
195
195
  const log = options.log ?? ((line) => console.log(line));
@@ -197,7 +197,7 @@ export async function runUpdateCommand(args, options = {}) {
197
197
  const versionInfo = resolveCommandVersionInfo(options, env);
198
198
  const currentVersion = versionInfo.currentVersion;
199
199
  const cachePath = resolveCachePath(env);
200
- const cache = readCacheSync(cachePath);
200
+ let cache = readCacheSync(cachePath);
201
201
  const dismissRequested = args.includes('--dismiss');
202
202
  const rollbackRequested = args.includes('--rollback');
203
203
  const forceRequested = args.includes('--force');
@@ -219,10 +219,15 @@ export async function runUpdateCommand(args, options = {}) {
219
219
  return { exitCode: 0 };
220
220
  }
221
221
 
222
+ if (forceRequested && !cache?.feed) {
223
+ await (options.refreshUpdateFeed ?? refreshUpdateFeed)({ env, fetchImpl: options.fetchImpl });
224
+ cache = readCacheSync(cachePath);
225
+ }
226
+
222
227
  const feed = cache?.feed;
223
228
  const updateAvailable = isValidFeedShape(feed) && compareVersions(feed.product, currentVersion) > 0;
224
229
  const reinstallRequested = forceRequested && isValidFeedShape(feed) && compareVersions(feed.product, currentVersion) >= 0;
225
- const hasManagedApp = !!readInstalledAppVersion(env);
230
+ const hasManagedApp = versionInfo.managedApp === true;
226
231
  const selfUpdateEligible = (updateAvailable || reinstallRequested)
227
232
  && !!feed.components?.app?.url
228
233
  && !!feed.components?.app?.sha256
@@ -240,7 +245,7 @@ export async function runUpdateCommand(args, options = {}) {
240
245
  }
241
246
  log(`最新バージョン: v${feed.product}`);
242
247
  if (forceRequested) {
243
- log(`--force: 本体 v${currentVersion} を入れ直します。`);
248
+ log(describeForceReinstall(versionInfo, feed.product));
244
249
  }
245
250
 
246
251
  return (options.applySelfUpdate ?? applySelfUpdate)({
package/src/messages.mjs CHANGED
@@ -90,6 +90,14 @@ export function describeVersionStatus(versionOrInfo, cache) {
90
90
 
91
91
  export function describeInstalledVersions(versionOrInfo) {
92
92
  const info = normalizeVersionInfo(versionOrInfo);
93
+ if (info.installRefNeedsRepair) {
94
+ const installRefPath = info.installRefPath ?? '~/.akari/app/.akari-install-ref';
95
+ return [
96
+ `CLI バージョン: v${info.cliVersion}`,
97
+ `本体版を判定できません(\`${installRefPath}\` が壊れています)。`,
98
+ '修復するには `akari update --force` を実行してください。'
99
+ ];
100
+ }
93
101
  if (!info.appVersion) {
94
102
  return [
95
103
  `現在のバージョン: v${info.currentVersion}`,
@@ -104,6 +112,13 @@ export function describeInstalledVersions(versionOrInfo) {
104
112
  return lines;
105
113
  }
106
114
 
115
+ export function describeForceReinstall(versionOrInfo, targetVersion) {
116
+ const info = normalizeVersionInfo(versionOrInfo);
117
+ return info.installRefNeedsRepair
118
+ ? `--force: 版を判定できない本体 → v${targetVersion} を入れ直します。`
119
+ : `--force: 本体 v${info.currentVersion} → v${targetVersion} を入れ直します。`;
120
+ }
121
+
107
122
  function normalizeVersionInfo(value) {
108
123
  if (typeof value === 'string') {
109
124
  return { cliVersion: value, appVersion: null, currentVersion: value, mismatch: false };
@@ -112,11 +127,15 @@ function normalizeVersionInfo(value) {
112
127
  }
113
128
 
114
129
  function versionRelationLabel(info) {
115
- return compareVersions(info.appVersion, info.cliVersion) < 0 ? '本体が古い' : 'CLI と本体の版が不一致';
130
+ return compareVersions(info.appVersion, info.cliVersion) < 0 ? '本体が古い' : 'CLI が古い';
116
131
  }
117
132
 
118
133
  function formatVersionMismatch(info) {
119
- return `⚠ CLI v${info.cliVersion} / 本体 v${info.appVersion} → ${versionRelationLabel(info)}。\`akari update\` で本体を更新してください。`;
134
+ const relation = versionRelationLabel(info);
135
+ const guidance = relation === '本体が古い'
136
+ ? '`akari update` で本体を更新してください。'
137
+ : '`npm i -g akari-video@latest` で CLI を更新してください。';
138
+ return `⚠ CLI v${info.cliVersion} / 本体 v${info.appVersion} → ${relation}。${guidance}`;
120
139
  }
121
140
 
122
141
  /**
@@ -168,6 +187,11 @@ export function describeUpdateCommand({ currentVersion, versionInfo, cache, dism
168
187
  lines.push(`リリースノート: ${feed.notes_url}`);
169
188
  }
170
189
 
190
+ if (info.installRefNeedsRepair) {
191
+ lines.push('本体版を判定できないため、更新判定を行いません。');
192
+ return lines;
193
+ }
194
+
171
195
  if (compareVersions(feed.product, info.currentVersion) <= 0) {
172
196
  lines.push('お使いのバージョンは最新です。');
173
197
  return lines;
@@ -65,29 +65,49 @@ export function readOwnVersion() {
65
65
  return JSON.parse(raw).version;
66
66
  }
67
67
 
68
+ /** install-ref を「有効・未記録・破損」の 3 状態で読む内部表現。 */
69
+ function readInstalledAppVersionInfo(env = process.env) {
70
+ const path = resolveInstallRefPath(env);
71
+ try {
72
+ const raw = readFileSync(path, 'utf8').trim();
73
+ const match = raw.match(/^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
74
+ return match
75
+ ? { status: 'valid', version: match[1], path }
76
+ : { status: 'invalid', version: null, path };
77
+ } catch (error) {
78
+ return {
79
+ status: error?.code === 'ENOENT' ? 'missing' : 'invalid',
80
+ version: null,
81
+ path
82
+ };
83
+ }
84
+ }
85
+
68
86
  /**
69
87
  * 実際に render-cut / edit-lint を実行する本体の導入版。
70
- * install.sh / self-update.mjs が書く `vX.Y.Z` を読み、無い・壊れている場合は null
88
+ * install.sh / self-update.mjs が書く `vX.Y.Z` を読む。互換 API として版または null を返し、
89
+ * 未記録と破損の区別は `resolveInstalledVersionInfo()` が保持する。
71
90
  */
72
91
  export function readInstalledAppVersion(env = process.env) {
73
- try {
74
- const raw = readFileSync(resolveInstallRefPath(env), 'utf8').trim();
75
- const match = raw.match(/^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
76
- return match?.[1] ?? null;
77
- } catch {
78
- return null;
79
- }
92
+ return readInstalledAppVersionInfo(env).version;
80
93
  }
81
94
 
82
95
  /** 更新判定に使う版と、CLI / 本体のずれを一度に解決する。 */
83
96
  export function resolveInstalledVersionInfo({ env = process.env, cliVersion = readOwnVersion() } = {}) {
84
- const installedAppVersion = readInstalledAppVersion(env);
97
+ const installRef = readInstalledAppVersionInfo(env);
98
+ const installedAppVersion = installRef.version;
85
99
  const currentVersion = installedAppVersion ?? cliVersion;
86
100
  return {
87
101
  cliVersion,
88
102
  appVersion: installedAppVersion,
89
103
  currentVersion,
90
- source: installedAppVersion ? 'install-ref' : 'cli-fallback',
104
+ source: installRef.status === 'valid'
105
+ ? 'install-ref'
106
+ : installRef.status === 'invalid' ? 'invalid-install-ref' : 'cli-fallback',
107
+ installRefStatus: installRef.status,
108
+ installRefPath: installRef.path,
109
+ installRefNeedsRepair: installRef.status === 'invalid',
110
+ managedApp: installRef.status !== 'missing',
91
111
  mismatch: installedAppVersion !== null && compareVersions(cliVersion, installedAppVersion) !== 0
92
112
  };
93
113
  }
@@ -135,8 +155,18 @@ function writeCacheSync(cachePath, cache) {
135
155
  /**
136
156
  * キャッシュと現在版から、新版通知を出すべきかを判定する(同期・純粋関数・I/O なし)。
137
157
  */
138
- export function evaluateUpdateStatus({ currentVersion, cache, cliVersion = currentVersion, appVersion = null, source = 'cli-fallback', mismatch = false }) {
139
- const versionDetails = { currentVersion, cliVersion, appVersion, source, mismatch };
158
+ export function evaluateUpdateStatus({ currentVersion, cache, cliVersion = currentVersion, appVersion = null, source = 'cli-fallback', installRefStatus, installRefPath, installRefNeedsRepair, managedApp, mismatch = false }) {
159
+ const versionDetails = {
160
+ currentVersion,
161
+ cliVersion,
162
+ appVersion,
163
+ source,
164
+ installRefStatus,
165
+ installRefPath,
166
+ installRefNeedsRepair,
167
+ managedApp,
168
+ mismatch
169
+ };
140
170
  const feed = cache?.feed;
141
171
  if (!isValidFeedShape(feed)) {
142
172
  return { available: false, ...versionDetails };
@@ -215,16 +245,8 @@ export async function maybeStageInBackground({ env = process.env, feed, fetchImp
215
245
  return stageSelfUpdate({ env, feed, log: () => {}, fetchImpl, timeoutMs, extract });
216
246
  }
217
247
 
218
- /**
219
- * バックグラウンド fetch 本体。fetch 失敗・非 200・JSON パース失敗・スキーマ不明は
220
- * すべて沈黙して return する(何も throw しない)。`dismissed` は既存キャッシュから
221
- * 引き継ぐ(fetch のたびに既読状態が消えないように)。
222
- *
223
- * フィード取得・キャッシュ反映が成功した後、契約 §11 のバックグラウンド staging も
224
- * 同じ沈黙原則のもとで試みる(`maybeStageInBackground`)。成功したときだけ
225
- * キャッシュへ `staged`(版・sha256・staged_at)を追記する。
226
- */
227
- export async function runBackgroundFetch({ env = process.env, fetchImpl = globalThis.fetch, launcherRoot } = {}) {
248
+ /** フィードを 1 回取得し、正常なら既読状態を保ったままキャッシュへ反映する。 */
249
+ export async function refreshUpdateFeed({ env = process.env, fetchImpl = globalThis.fetch } = {}) {
228
250
  const feedUrl = resolveFeedUrl(env);
229
251
  const cachePath = resolveCachePath(env);
230
252
  try {
@@ -241,15 +263,34 @@ export async function runBackgroundFetch({ env = process.env, fetchImpl = global
241
263
  }
242
264
  const feed = await response.json();
243
265
  if (!isValidFeedShape(feed)) {
244
- return;
266
+ return null;
245
267
  }
246
268
  const existing = readCacheSync(cachePath);
247
- writeCacheSync(cachePath, {
269
+ const next = {
248
270
  schema: CACHE_SCHEMA,
249
271
  fetched_at: new Date().toISOString(),
250
272
  feed,
251
273
  dismissed: existing?.dismissed ?? {}
252
- });
274
+ };
275
+ writeCacheSync(cachePath, next);
276
+ return next;
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+
282
+ /**
283
+ * バックグラウンド fetch 本体。フィード取得失敗は沈黙し、成功後は契約 §11 の staging を
284
+ * 同じ沈黙原則で試す。成功したときだけキャッシュへ `staged` を追記する。
285
+ */
286
+ export async function runBackgroundFetch({ env = process.env, fetchImpl = globalThis.fetch, launcherRoot } = {}) {
287
+ const cachePath = resolveCachePath(env);
288
+ try {
289
+ const refreshed = await refreshUpdateFeed({ env, fetchImpl });
290
+ const feed = refreshed?.feed;
291
+ if (!feed) {
292
+ return;
293
+ }
253
294
 
254
295
  const staged = await maybeStageInBackground({ env, feed, fetchImpl, launcherRoot });
255
296
  if (staged?.ok) {
@@ -40,6 +40,7 @@
40
40
  "docs/contract-2026-08-13-avatar-drive-v0.md",
41
41
  "docs/contract-2026-08-14-avatar-vrm-v0.md",
42
42
  "docs/contract-2026-08-18-v1-render-parity.md",
43
+ "docs/contract-2026-08-23-stroke-persistence.md",
43
44
  "packages/akari-launcher/package.json",
44
45
  "packages/akari-launcher/README.md",
45
46
  "packages/akari-tools/package.json",
@@ -66,7 +66,7 @@
66
66
 
67
67
  ## 5. §2 追記 — sfx フェード(audio-clip-fades, 2026-08-18・オーナー裁定「クリップ主義」T2)
68
68
 
69
- BGM をクリップ化する裁定(内部リポ `tasks/2026-08-18-bgm-clip-placement-ruling`)に伴い、
69
+ BGM をクリップ化する裁定(内部リポ `akari-video-internal` の該当タスク)に伴い、
70
70
  「音楽をクリップ(audio.sfx[])として置いても BGM ベッドと同じフェード表現ができる」を
71
71
  満たすため、`sfxItem` に optional の `fade_in` / `fade_out`(秒・0 以上)を追加のみ拡張する
72
72
  (`version` 不変・`contract-2026-07-17-data-contract-versioning.md` の原則に従う)。
@@ -0,0 +1,75 @@
1
+ ---
2
+ lifecycle: implemented
3
+ created: 2026-08-23
4
+ updated: 2026-08-23
5
+ ---
6
+
7
+ # 注釈ストローク永続表示契約
8
+
9
+ - 日付: 2026-08-23
10
+ - 状態: **実装済み**
11
+ - 前提: `contract-2026-08-11-review-session-ui-events.md`、
12
+ `contract-2026-07-20-review-json-v1-annotation-model.md`
13
+ - スコープ: Theia shell の出力プレビュー、review session の `strokes.json` 読み出し、
14
+ compile-review-session と address-review の追跡導線
15
+
16
+ ## 1. セッション中の表示
17
+
18
+ - pen / rect は従来どおり正規化座標(プレビューフレーム左上を `(0, 0)`、右下を `(1, 1)`)で
19
+ 記録する。表示時に現在の content rect へ写像し直すため、ウィンドウのリサイズと出力比率の
20
+ 変更で座標は変わらない。
21
+ - pointerup 後は従来のグロー・きらめき・600 ms フェードをそのまま再生し、その後段の静的
22
+ ビットマップへ同じ正規化図形を残す。新しい録音セッションの開始時に前セッションの表示を
23
+ クリアし、録音終了では消さない。
24
+ - 描線 canvas は非描画モードで `pointer-events: none` とする。ペンまたは四角モードのドラッグ中
25
+ だけ既存どおり入力面になる。
26
+ - 注釈パネルの「描線を表示」チェックは既定 ON。OFF は残留描線と明示的に再表示した描線を隠し、
27
+ データを削除しない。ON に戻すと保持した正規化座標から即時再描画する。
28
+
29
+ ## 2. 既存セッションの読み出しと再表示
30
+
31
+ `readReviewSessionStrokes({projectRootUri, sessionId})` は
32
+ `review/sessions/<sessionId>/strokes.json` を読み、次を返す。
33
+
34
+ ```jsonc
35
+ {
36
+ "sessionId": "s-0001",
37
+ "strokes": [/* pen / rect。frame と recTStart/recTEnd を保持 */],
38
+ "warnings": []
39
+ }
40
+ ```
41
+
42
+ - `strokes.json` 欠落は `strokes: []` として正常終了する。
43
+ - 配列ルート、または `version: 1` でない `{strokes:[]}` は旧形式として寛容に読む。
44
+ - JSON 破損、未知要素、値域外要素は描線単位で除外して warning に残す。セッション一覧と他の
45
+ 描線を巻き込んで失敗させない。
46
+ - 注釈パネルの各録音済みセッションにある「描線」から再表示できる。表示メッセージには
47
+ `target.tab`(edit URI)と先頭ストロークの `target.recT` を添え、`frame.sourceT/cutIndex` で
48
+ プレビューを同じフレームへシークする。
49
+
50
+ ## 3. compile 後の原本参照
51
+
52
+ review.json の data `version` は 0 のまま据え置く。compile-review-session はペアになった pen / rect
53
+ へ、既存フィールドを変えず次の任意フィールドを追加する。
54
+
55
+ ```jsonc
56
+ "strokeRefs": [{
57
+ "sessionId": "s-0001",
58
+ "strokeId": "st-0001",
59
+ "sessionRef": "s-0001/st-0001"
60
+ }]
61
+ ```
62
+
63
+ - `strokeRefs` は `null`、省略、または 1 件以上の配列。欠落は従来データとして正常である。
64
+ - pen は従来どおり最大 100 点の `strokes[].points` も埋め込み、`strokeRefs` から無加工の原本へ
65
+ 戻れる。rect は従来どおり `targetKind:"region"` + `region.box` を埋め込み、同じ `strokeRefs`
66
+ から `strokes.json` 内の rect 原本へ戻れる。
67
+ - address-review の一覧は `strokeRefs` を
68
+ `review/sessions/<sessionId>/strokes.json#<strokeId>` として表示する。
69
+
70
+ ## 4. 互換性
71
+
72
+ - 追加フィールドのみを使い、既存フィールドの削除・意味変更・data version bump は行わない。
73
+ - 読み手は未知フィールドを保持し、任意フィールドの欠落を旧データとして扱い、既知より大きい
74
+ data version を推測変換しない。
75
+ - `packages/preview-server` は本契約の対象外であり、WebUI の表示挙動は変更しない。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout.]",
5
5
  "type": "module",
6
6
  "files": [
@@ -9,6 +9,7 @@
9
9
  |---|---|
10
10
  | `report-helper.mjs` | `127.0.0.1` のみにバインドするローカル HTTP ヘルパー。`report.html` の配信・`decisions.json` の read/write・commit を仲介する |
11
11
  | `report-template.html` | data 属性でカードを宣言する report.html の雛形。単選・真偽チェック・整数スライダ・配列型の複数選択に対応する。カード種別のハードコードなし。ヘルパー経由なら操作可能、`file://` 直開き等で fetch 失敗時は全操作ボタンが disabled になり案内文言を出す(安全劣化) |
12
+ | `render-research-plan-report.mjs` | `research-plan.json` から固定 5 面の自己完結 HTML を生成する。絵コンテ面は画像 / 文字プレースホルダーのカード面と、主軸 + カットアウェイの読み取り専用 SVG 構造面を切り替えられる |
12
13
  | `examples/report.html` + `examples/report.html.decisions.json` | 動作サンプル一式(既存 4 カード + `direction`)。AI 推奨を既定値に入れた `decisions.json` 雛形付き |
13
14
  | `test/direction-card.test.mjs` | ローカル Chrome を headless 起動して、演出カードの保存と既存 4 カードの非退行を実測するテスト |
14
15
 
@@ -54,6 +55,17 @@ curl -X POST http://127.0.0.1:8791/api/commit
54
55
  npm test --workspace @akari-video/decision-cards
55
56
  ```
56
57
 
58
+ ### research-plan のビジュアル絵コンテ
59
+
60
+ ```sh
61
+ node packages/decision-cards/render-research-plan-report.mjs \
62
+ planning/research-plan.json \
63
+ planning/research-plan-report.html
64
+ node packages/decision-cards/report-helper.mjs planning/research-plan-report.html
65
+ ```
66
+
67
+ 画像は生成時に data URI へ埋め込むため、出力 HTML は外部依存を持たない。`image_path` が無い、または画像を読めないショットは `shot_type` + `description` のプレースホルダーへ安全に劣化する。旧形式に `sequence` / `cutaway_of` が無い場合も生成は成功し、構造面だけが「構造情報なし」になる。
68
+
57
69
  ## edit-plan スキルからの参照方法
58
70
 
59
71
  公開リポの `.claude/skills/edit-plan/` は本パッケージを以下のように参照する想定:
@@ -2,7 +2,7 @@
2
2
  "name": "@akari-video/decision-cards",
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
- "description": "決定カード v0 ランタイム。report-helper(ローカル HTTP サーバ)+ report-template(カード付きレポート HTML の雛形)+ 動作サンプル。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: report-helper.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout.]",
5
+ "description": "決定カード v0 ランタイム。report-helper(ローカル HTTP サーバ)+ report-template(カード付きレポート HTML の雛形)+ 動作サンプル。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: report-helper.mjs is reference-only; render-research-plan-report.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout.]",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "start": "node report-helper.mjs",
@@ -11,7 +11,8 @@
11
11
  "akariVideoVendor": {
12
12
  "execution": "reference-only",
13
13
  "omittedBin": {
14
- "decision-cards-helper": "report-helper.mjs"
14
+ "decision-cards-helper": "report-helper.mjs",
15
+ "research-plan-report": "render-research-plan-report.mjs"
15
16
  },
16
17
  "guidance": "These CLI entrypoints are not included in the akari-video npm package. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout."
17
18
  }
@@ -19,6 +19,8 @@ import { musicGrid } from "../../audio-library-setup/shared/beat-grid.mjs";
19
19
  import { resolveFfmpeg, resolveFfprobe } from "../../media-bin/src/index.mjs";
20
20
 
21
21
  const {
22
+ areCutsAdjacent,
23
+ findUnsupportedDeclaredTrackTransitions,
22
24
  projectLegacyEdit,
23
25
  readInternalEdit,
24
26
  resolveCaptionDisplay,
@@ -253,7 +255,7 @@ export async function lintProject(input, options = {}) {
253
255
  validateStillImageCuts(edit, findings);
254
256
  const cutTrackSegments = computeCutTrackSegments(edit.cuts);
255
257
  for (const segment of findTrackOverlaps(cutTrackSegments)) {
256
- if (isDeclaredTransitionOverlap(edit.cuts, cutTrackSegments, segment)) continue;
258
+ if (isDeclaredTransitionOverlap(edit.cuts, cutTrackSegments, segment, edit.fps)) continue;
257
259
  addFinding(findings, {
258
260
  severity: "error",
259
261
  check: "cuts.track-overlap",
@@ -351,7 +353,11 @@ function collectAudioOnlySourceIds(edit) {
351
353
 
352
354
  function collectInternalAudioTrackRefs(internalEdit) {
353
355
  return new Set(internalEdit.tracks
354
- .filter(track => track.lane === "audio" && track.items.length > 0)
356
+ // v2 top-level audio は読み込み層が implicit audio track へ射影するが、元データに
357
+ // tracks[] 宣言は無い。これを実段として数えると declaration-missing が必ず出る。
358
+ // validateTimelineTracks が照合する相手は projectLegacyEdit の declaredTracks なので、
359
+ // 実データ側も同じ declared origin に限定して投影ノイズを除く。
360
+ .filter(track => track.origin === "declared" && track.lane === "audio" && track.items.length > 0)
355
361
  .map(track => Number.isInteger(track.legacy.ref) ? track.legacy.ref : 0));
356
362
  }
357
363
 
@@ -878,15 +884,18 @@ function findTrackOverlaps(segments) {
878
884
  // render correctly would still fail lint. Still rejects zero/negative overlap (a genuine gap --
879
885
  // no transition is physically possible) and overlap greater than declared (an unrelated shape
880
886
  // render-cut does not auto-adjust for -- see effectiveTransitionDurations' own comment).
881
- function isDeclaredTransitionOverlap(cuts, segments, current) {
887
+ function isDeclaredTransitionOverlap(cuts, segments, current, fps) {
882
888
  const previous = segments
883
889
  .filter(segment => segment.track === current.track && segment.index < current.index)
884
890
  .sort((left, right) => right.index - left.index)[0];
885
891
  if (!previous) return false;
886
892
  const duration = cuts?.[previous.index]?.transition_out?.duration;
887
893
  if (!isPositiveNumber(duration)) return false;
888
- const availableOverlap = previous.end - current.start;
889
- return availableOverlap > EPSILON && availableOverlap <= duration + EPSILON;
894
+ return areCutsAdjacent(
895
+ { tlEnd: previous.end, transitionOut: { duration } },
896
+ { tlStart: current.start },
897
+ fps,
898
+ );
890
899
  }
891
900
 
892
901
  function validateCutTrackFields(cuts, findings) {
@@ -1255,15 +1264,16 @@ function validateTimelineTracks(edit, findings, projectedAudioTracks = null) {
1255
1264
  const ref = item.kind === "audio" && !hasRef ? 0 : item.ref;
1256
1265
  if (ref === undefined) continue;
1257
1266
  declarations.add(`${item.kind}:${ref}`);
1258
- if (!actualTracks.get(item.kind)?.has(ref)) {
1259
- addFinding(findings, {
1260
- severity: "warning",
1261
- check: "timeline.tracks.ref-missing",
1262
- message: `${item.kind} timeline track ref ${ref} is not used by edit data`,
1263
- path: `${path}.ref`,
1264
- });
1265
- }
1266
1267
  }
1268
+ // ここには以前 `timeline.tracks.ref-missing`(宣言された段の ref が実データのどこにも
1269
+ // 現れなければ警告)があったが、2026-08-20 に撤去した。v2 では timeline.tracks[] の各段が
1270
+ // internal-model.ts の projectLegacyEdit を通じてそのまま legacy 射影され、ref は宣言順に
1271
+ // 毎回生成し直される連番なので、「宣言はあるが実データに現れない ref」は「段の中身が 0 個」
1272
+ // としか等価にならない。空の段は自動 prune せず残すのが正本(10番裁定 E)なので、この
1273
+ // チェックは空の段を持つ v2 プロジェクトのたびに必ず誤検知していた。v0/v1 は本体から既に
1274
+ // 除かれており(9番)、「(kind, ref) の参照」という v0/v1 由来の概念自体が v2 には無いため
1275
+ // 部分修正ではなく撤去する。撤去の証跡は edit-lint.test.mjs の
1276
+ // "空の段を持つ v2 プロジェクトは findings 0" で固定してある。
1267
1277
 
1268
1278
  for (const [kind, tracks] of actualTracks) {
1269
1279
  for (const ref of tracks) {
@@ -1295,56 +1305,27 @@ function validateTimelineTracks(edit, findings, projectedAudioTracks = null) {
1295
1305
  // too wide a blast radius to take on speculatively, especially with no evidence anyone needs the
1296
1306
  // combination. Reject it instead: it fails loudly and specifically, rather than rendering a
1297
1307
  // broken video with a phantom black flash that's very hard to trace back to its cause.
1298
- function validateTrackTransitionOutCompatibility(edit, findings) {
1299
- if (!Array.isArray(edit?.cuts)) return;
1300
- const tracks = edit?.timeline?.tracks;
1301
- if (!Array.isArray(tracks)) return; // malformed timeline.tracks is already reported by validateTimelineTracks
1302
- if (usesDefaultCompatibilityTrackOrder(tracks)) return;
1303
-
1304
- const cutsTrackRefs = new Set(
1305
- tracks
1306
- .filter((item) => isRecord(item) && item.kind === "cuts" && Number.isInteger(item.ref) && item.ref >= 0)
1307
- .map((item) => item.ref),
1308
- );
1309
- for (const ref of cutsTrackRefs) {
1310
- const trackCuts = edit.cuts
1311
- .map((cut, index) => ({ cut, index }))
1312
- .filter(({ cut }) => isRecord(cut) && (cut.track ?? 0) === ref);
1313
- // The last cut on a track has no following same-track cut to blend into, so its own
1314
- // transition_out (if any) never renders -- mirrors buildMultiSourceCutCommand's own
1315
- // hasAnyTransition check (plan.mjs) and predictedDuration's overlap accounting.
1316
- for (const { cut, index } of trackCuts.slice(0, -1)) {
1317
- if (!cut.transition_out) continue;
1318
- addFinding(findings, {
1319
- severity: "error",
1320
- check: "cuts.track-transition-unsupported",
1321
- message:
1322
- `cuts[].transition_out is declared on track ${ref}, which timeline.tracks composites through the `
1323
- + `gap-aware track engine. That engine treats adjacent same-track cuts as separate, non-overlapping `
1324
- + `windows, so it cannot represent an xfade's intentional overlap -- the composited window and the `
1325
- + `actually-shrunk clip diverge, and content disappears early (verified with a real render: the base `
1326
- + `track's background visibly leaked through where the dissolved clip should still have been `
1327
- + `playing). Remove transition_out from this track's cuts, or drop the custom timeline.tracks order `
1328
- + `for this track so it renders through the plain sequential path instead.`,
1329
- path: `edit.json#cuts[${index}]`,
1330
- });
1331
- }
1308
+ export function validateTrackTransitionOutCompatibility(edit, findings) {
1309
+ for (const { cutIndex, trackRef } of findUnsupportedDeclaredTrackTransitions(
1310
+ edit?.cuts,
1311
+ edit?.timeline?.tracks,
1312
+ )) {
1313
+ addFinding(findings, {
1314
+ severity: "error",
1315
+ check: "cuts.track-transition-unsupported",
1316
+ message:
1317
+ `cuts[].transition_out is declared on track ${trackRef}, which timeline.tracks composites through the `
1318
+ + `gap-aware track engine. That engine treats adjacent same-track cuts as separate, non-overlapping `
1319
+ + `windows, so it cannot represent an xfade's intentional overlap -- the composited window and the `
1320
+ + `actually-shrunk clip diverge, and content disappears early (verified with a real render: the base `
1321
+ + `track's background visibly leaked through where the dissolved clip should still have been `
1322
+ + `playing). Remove transition_out from this track's cuts, or drop the custom timeline.tracks order `
1323
+ + `for this track so it renders through the plain sequential path instead.`,
1324
+ path: `edit.json#cuts[${cutIndex}]`,
1325
+ });
1332
1326
  }
1333
1327
  }
1334
1328
 
1335
- function usesDefaultCompatibilityTrackOrder(tracks) {
1336
- const rank = new Map([["cuts", 0], ["layers", 1], ["overlays", 2], ["captions", 3], ["audio", 4]]);
1337
- const keys = tracks.map((track, index) => ({
1338
- kind: track?.kind,
1339
- ref: Number.isInteger(track?.ref) ? track.ref : -1,
1340
- index,
1341
- }));
1342
- if (keys.some(key => !rank.has(key.kind))) return false;
1343
- const expected = [...keys].sort((left, right) =>
1344
- rank.get(left.kind) - rank.get(right.kind) || left.ref - right.ref || left.index - right.index);
1345
- return keys.every((key, index) => key.index === expected[index].index);
1346
- }
1347
-
1348
1329
  function collectActualTrackNumbers(items) {
1349
1330
  const tracks = new Set();
1350
1331
  if (!Array.isArray(items)) return tracks;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * cuts 配列順で隣り合う同一トラックの 2 クリップが、トランジション UI を持てる境界か判定する。
3
+ * タイムライン UI と edit-lint は必ずこの関数を使い、フレーム量子化の条件式を複製しない。
4
+ */
5
+ export interface CutAdjacencyTransitionLike {
6
+ duration?: unknown;
7
+ }
8
+ export interface CutAdjacencyEarlierLike {
9
+ tlEnd: number;
10
+ transitionOut?: CutAdjacencyTransitionLike | null;
11
+ }
12
+ export interface CutAdjacencyLaterLike {
13
+ tlStart: number;
14
+ }
15
+ /**
16
+ * ギャップ 0、または earlier の宣言済み transitionOut.duration で説明できる重なりだけを
17
+ * 隣接とみなす。秒の誤差ではなく、出力 fps で量子化したフレーム数を比較する。
18
+ */
19
+ export declare function areCutsAdjacent(earlier: CutAdjacencyEarlierLike, later: CutAdjacencyLaterLike, fps?: number): boolean;