akari-video 0.1.12 → 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 (61) hide show
  1. package/bin/akari.mjs +8 -3
  2. package/package.json +1 -1
  3. package/src/cli.mjs +51 -13
  4. package/src/messages.mjs +79 -8
  5. package/src/self-update.mjs +4 -0
  6. package/src/status-command.mjs +10 -2
  7. package/src/update-check.mjs +103 -23
  8. package/vendor/.akari-capability-sources.json +1 -0
  9. package/vendor/docs/contract-2026-07-17-data-contract-versioning.md +6 -0
  10. package/vendor/docs/contract-2026-07-25-r6-audio-tracks-and-trim.md +1 -1
  11. package/vendor/docs/contract-2026-08-23-stroke-persistence.md +75 -0
  12. package/vendor/packages/akari-launcher/package.json +1 -1
  13. package/vendor/packages/decision-cards/README.md +12 -0
  14. package/vendor/packages/decision-cards/package.json +3 -2
  15. package/vendor/packages/edit-lint/src/edit-lint.mjs +191 -389
  16. package/vendor/packages/edit-store/lib/cut-adjacency.d.ts +19 -0
  17. package/vendor/packages/edit-store/lib/cut-adjacency.js +28 -0
  18. package/vendor/packages/edit-store/lib/edit-store.d.ts +13 -4
  19. package/vendor/packages/edit-store/lib/edit-store.js +62 -45
  20. package/vendor/packages/edit-store/lib/edit-v2-item-write.d.ts +53 -0
  21. package/vendor/packages/edit-store/lib/edit-v2-item-write.js +162 -0
  22. package/vendor/packages/edit-store/lib/edit-v2.d.ts +45 -4
  23. package/vendor/packages/edit-store/lib/edit-v2.js +72 -1
  24. package/vendor/packages/edit-store/lib/index.d.ts +3 -0
  25. package/vendor/packages/edit-store/lib/index.js +3 -0
  26. package/vendor/packages/edit-store/lib/internal-model.d.ts +9 -0
  27. package/vendor/packages/edit-store/lib/internal-model.js +381 -41
  28. package/vendor/packages/edit-store/lib/migrate/index.js +226 -19
  29. package/vendor/packages/edit-store/lib/migrate/legacy-parse.js +5 -1
  30. package/vendor/packages/edit-store/lib/retime.js +14 -0
  31. package/vendor/packages/edit-store/lib/track-transition-compatibility.d.ts +23 -0
  32. package/vendor/packages/edit-store/lib/track-transition-compatibility.js +76 -0
  33. package/vendor/packages/edit-store/lib/write-gate.d.ts +2 -0
  34. package/vendor/packages/edit-store/lib/write-gate.js +11 -1
  35. package/vendor/packages/overlay-runtime/package.json +2 -1
  36. package/vendor/packages/pen-visuals/package.json +1 -0
  37. package/vendor/packages/schemas/bin/validate-research-plan.mjs +55 -7
  38. package/vendor/packages/schemas/edit.schema.json +53 -3
  39. package/vendor/packages/schemas/examples/edit-v2-audio-track-valid/edit.json +56 -0
  40. package/vendor/packages/schemas/examples/edit-v2-valid/edit.json +38 -2
  41. package/vendor/packages/schemas/fixtures/review/valid-strokes/review.json +1 -1
  42. package/vendor/packages/schemas/research-plan.schema.json +24 -2
  43. package/vendor/packages/schemas/review.schema.json +18 -0
  44. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +106 -5
  45. package/vendor/packages/schemas/test/fixtures/research-plan/invalid-nested-cutaway/research-plan.json +25 -0
  46. package/vendor/packages/schemas/test/fixtures/research-plan/valid-legacy-without-shot-ids/research-plan.json +29 -0
  47. package/vendor/packages/schemas/test/fixtures/research-plan/valid-visual-storyboard/research-plan.json +72 -0
  48. package/vendor/packages/schemas/test/validate-research-plan.test.mjs +18 -0
  49. package/vendor/skills/address-review/bin/list.mjs +12 -0
  50. package/vendor/skills/address-review/dev-fixtures/fixture-project/review.json +3 -0
  51. package/vendor/skills/address-review/test/review-store.test.mjs +6 -0
  52. package/vendor/skills/compile-review-session/bin/core/compiler.mjs +5 -0
  53. package/vendor/skills/compile-review-session/test/compiler.test.mjs +9 -0
  54. package/vendor/skills/compile-review-session/test/ui-events.test.mjs +1 -0
  55. package/vendor/skills/edit-plan/SKILL.md +1 -1
  56. package/vendor/skills/edit-plan/beat-sync.md +18 -4
  57. package/vendor/skills/edit-plan/beats.md +1 -1
  58. package/vendor/skills/edit-plan/emphasis-detection.md +1 -1
  59. package/vendor/skills/edit-plan/execution.md +1 -1
  60. package/vendor/skills/research-plan/SKILL.md +1 -1
  61. 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.12",
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 { 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 {
@@ -16,7 +16,9 @@ import {
16
16
  readCacheSync,
17
17
  readOwnVersion,
18
18
  recordDismissalSync,
19
+ refreshUpdateFeed,
19
20
  resolveCachePath,
21
+ resolveInstalledVersionInfo,
20
22
  triggerBackgroundRefresh
21
23
  } from './update-check.mjs';
22
24
  import { applySelfUpdate, isRunningFromAppDir, rollbackSelfUpdate } from './self-update.mjs';
@@ -41,7 +43,8 @@ export async function run(args, options = {}) {
41
43
  const spawnOpencode = options.spawnOpencode ?? defaultSpawnOpencode;
42
44
  const env = options.env ?? process.env;
43
45
  const platform = options.platform ?? process.platform;
44
- const currentVersion = options.currentVersion ?? readOwnVersion();
46
+ const versionInfo = resolveCommandVersionInfo(options, env);
47
+ const currentVersion = versionInfo.currentVersion;
45
48
  const now = options.now ?? new Date();
46
49
 
47
50
  // --opencode / --claude / --claudecode / --yes / --here フラグを解析
@@ -92,13 +95,13 @@ export async function run(args, options = {}) {
92
95
  } catch (error) {
93
96
  log(`接続確認でエラーが発生しました(続行します): ${error instanceof Error ? error.message : String(error)}`);
94
97
  }
95
- log(describeVersionStatus(currentVersion, readCacheSync(resolveCachePath(env))));
98
+ log(describeVersionStatus(versionInfo, readCacheSync(resolveCachePath(env))));
96
99
  }
97
100
 
98
101
  // 新版通知(契約 §4-1): キャッシュの読み比較のみ・ネットワークには一切触れない
99
102
  // (起動をブロックしない)。fetch は detached な子プロセスへ切り離し、
100
103
  // 結果は次回セッションで効く。
101
- const updateNotice = formatUpdateNotice((options.checkUpdate ?? checkForUpdateSync)({ currentVersion, env }));
104
+ const updateNotice = formatUpdateNotice((options.checkUpdate ?? checkForUpdateSync)({ currentVersion, versionInfo, env }));
102
105
  if (updateNotice) {
103
106
  log(updateNotice);
104
107
  }
@@ -178,22 +181,26 @@ function defaultSpawnOpencode(opencodePath, args, projectRoot) {
178
181
  /**
179
182
  * `akari update`: 新版があり、かつ自己更新の対象(app 経由実行 + フィードに
180
183
  * `components.app` がある)なら DL → sha256 検証 → 適用まで実行する(契約 §11。
184
+ * `app/.akari-install-ref` がある管理インストールは、npm 側 CLI から実行した場合も対象。
181
185
  * 「update は明示操作」なので U2 の沈黙原則は適用せず、失敗は必ず表示する)。
182
186
  * 対象外(npm グローバル / git checkout・旧フィード)のときは従来どおり
183
187
  * **案内するだけ**(自動実行はしない — 契約 §4-1)に縮退する。`--dismiss` は
184
188
  * 自己更新を試みず、キャッシュに載っている最新版の通知を今後出さないよう記録するだけ
185
- * (既存挙動を維持)。`--rollback` は直前 1 世代(`~/.akari/app-previous/`)へ戻す。
186
- * ネットワークに触れるのは自己更新の DL 区間のみ — フィード自体は既存キャッシュ由来
187
- * (最新情報は `akari` 起動時のバックグラウンド fetch で更新される)。
189
+ * (既存挙動を維持)。`--force` は同じ版の本体も再導入し、`--rollback` は直前 1 世代
190
+ * (`~/.akari/app-previous/`)へ戻す。
191
+ * 通常時にネットワークへ触れるのは自己更新の DL 区間のみ。`--force` だけはキャッシュが
192
+ * 未取得なら、復旧経路を塞がないためフィードの同期取得を 1 回試す。
188
193
  */
189
194
  export async function runUpdateCommand(args, options = {}) {
190
195
  const log = options.log ?? ((line) => console.log(line));
191
196
  const env = options.env ?? process.env;
192
- const currentVersion = options.currentVersion ?? readOwnVersion();
197
+ const versionInfo = resolveCommandVersionInfo(options, env);
198
+ const currentVersion = versionInfo.currentVersion;
193
199
  const cachePath = resolveCachePath(env);
194
- const cache = readCacheSync(cachePath);
200
+ let cache = readCacheSync(cachePath);
195
201
  const dismissRequested = args.includes('--dismiss');
196
202
  const rollbackRequested = args.includes('--rollback');
203
+ const forceRequested = args.includes('--force');
197
204
 
198
205
  if (rollbackRequested) {
199
206
  return (options.rollbackSelfUpdate ?? rollbackSelfUpdate)({ env, log });
@@ -206,26 +213,41 @@ export async function runUpdateCommand(args, options = {}) {
206
213
  dismissed = true;
207
214
  }
208
215
  const finalCache = dismissed ? readCacheSync(cachePath) : cache;
209
- for (const line of describeUpdateCommand({ currentVersion, cache: finalCache, dismissed })) {
216
+ for (const line of describeUpdateCommand({ currentVersion, versionInfo, cache: finalCache, dismissed })) {
210
217
  log(line);
211
218
  }
212
219
  return { exitCode: 0 };
213
220
  }
214
221
 
222
+ if (forceRequested && !cache?.feed) {
223
+ await (options.refreshUpdateFeed ?? refreshUpdateFeed)({ env, fetchImpl: options.fetchImpl });
224
+ cache = readCacheSync(cachePath);
225
+ }
226
+
215
227
  const feed = cache?.feed;
216
228
  const updateAvailable = isValidFeedShape(feed) && compareVersions(feed.product, currentVersion) > 0;
217
- const selfUpdateEligible = updateAvailable
229
+ const reinstallRequested = forceRequested && isValidFeedShape(feed) && compareVersions(feed.product, currentVersion) >= 0;
230
+ const hasManagedApp = versionInfo.managedApp === true;
231
+ const selfUpdateEligible = (updateAvailable || reinstallRequested)
218
232
  && !!feed.components?.app?.url
219
233
  && !!feed.components?.app?.sha256
220
- && (options.isRunningFromAppDir ?? isRunningFromAppDir)({ env, launcherRoot: options.launcherRoot });
234
+ && (hasManagedApp || (options.isRunningFromAppDir ?? isRunningFromAppDir)({ env, launcherRoot: options.launcherRoot }));
221
235
 
222
236
  if (!selfUpdateEligible) {
223
- for (const line of describeUpdateCommand({ currentVersion, cache, dismissed: false })) {
237
+ for (const line of describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed: false })) {
224
238
  log(line);
225
239
  }
226
240
  return { exitCode: 0 };
227
241
  }
228
242
 
243
+ for (const line of describeInstalledVersions(versionInfo)) {
244
+ log(line);
245
+ }
246
+ log(`最新バージョン: v${feed.product}`);
247
+ if (forceRequested) {
248
+ log(describeForceReinstall(versionInfo, feed.product));
249
+ }
250
+
229
251
  return (options.applySelfUpdate ?? applySelfUpdate)({
230
252
  env,
231
253
  feed,
@@ -234,3 +256,19 @@ export async function runUpdateCommand(args, options = {}) {
234
256
  runNpmInstall: options.runNpmInstall
235
257
  });
236
258
  }
259
+
260
+ function resolveCommandVersionInfo(options, env) {
261
+ // 既存テスト/埋め込み利用の currentVersion 注入は CLI 版注入としても扱う。
262
+ const cliVersion = options.cliVersion ?? options.currentVersion ?? readOwnVersion();
263
+ if (options.currentVersion !== undefined) {
264
+ const appVersion = options.appVersion ?? null;
265
+ return {
266
+ cliVersion,
267
+ appVersion,
268
+ currentVersion: options.currentVersion,
269
+ source: appVersion ? 'install-ref' : 'cli-fallback',
270
+ mismatch: appVersion !== null && compareVersions(cliVersion, appVersion) !== 0
271
+ };
272
+ }
273
+ return resolveInstalledVersionInfo({ env, cliVersion });
274
+ }
package/src/messages.mjs CHANGED
@@ -59,18 +59,83 @@ export function opencodeMissingGuidance() {
59
59
  */
60
60
  export function formatUpdateNotice(status) {
61
61
  if (!status?.available) {
62
+ if (status?.mismatch) {
63
+ return formatVersionMismatch(status);
64
+ }
62
65
  return null;
63
66
  }
64
- return `⬆ AKARI Video v${status.latestVersion}${channelSuffix(status.channel)}があります(現在 v${status.currentVersion})→ 詳細: akari update`;
67
+ const current = status.mismatch
68
+ ? `CLI v${status.cliVersion} / 本体 v${status.appVersion} → ${versionRelationLabel(status)}`
69
+ : `現在 v${status.currentVersion}`;
70
+ return `⬆ AKARI Video v${status.latestVersion}${channelSuffix(status.channel)}があります(${current})→ 詳細: akari update`;
65
71
  }
66
72
 
67
73
  /** `akari doctor` 系出力に足す 1 行(現在版 + フィード取得状態)。 */
68
- export function describeVersionStatus(currentVersion, cache) {
74
+ export function describeVersionStatus(versionOrInfo, cache) {
75
+ if (typeof versionOrInfo === 'string') {
76
+ if (!cache?.feed) {
77
+ return `バージョン: v${versionOrInfo}(更新フィード: 未取得)`;
78
+ }
79
+ const fetchedAt = typeof cache.fetched_at === 'string' ? cache.fetched_at : '不明';
80
+ return `バージョン: v${versionOrInfo}(更新フィード: 取得済み・${fetchedAt} 時点)`;
81
+ }
82
+ const info = normalizeVersionInfo(versionOrInfo);
83
+ const installed = describeInstalledVersions(info).join(' / ');
69
84
  if (!cache?.feed) {
70
- return `バージョン: v${currentVersion}(更新フィード: 未取得)`;
85
+ return `${installed}(更新フィード: 未取得)`;
71
86
  }
72
87
  const fetchedAt = typeof cache.fetched_at === 'string' ? cache.fetched_at : '不明';
73
- return `バージョン: v${currentVersion}(更新フィード: 取得済み・${fetchedAt} 時点)`;
88
+ return `${installed}(更新フィード: 取得済み・${fetchedAt} 時点)`;
89
+ }
90
+
91
+ export function describeInstalledVersions(versionOrInfo) {
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
+ }
101
+ if (!info.appVersion) {
102
+ return [
103
+ `現在のバージョン: v${info.currentVersion}`,
104
+ `CLI バージョン: v${info.cliVersion}`,
105
+ `本体バージョン: 未記録(更新判定は CLI v${info.currentVersion} へフォールバック)`
106
+ ];
107
+ }
108
+ const lines = [`CLI バージョン: v${info.cliVersion}`, `本体バージョン: v${info.appVersion}(更新判定の基準)`];
109
+ if (info.mismatch) {
110
+ lines.push(`版のずれ: CLI v${info.cliVersion} / 本体 v${info.appVersion} → ${versionRelationLabel(info)}`);
111
+ }
112
+ return lines;
113
+ }
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
+
122
+ function normalizeVersionInfo(value) {
123
+ if (typeof value === 'string') {
124
+ return { cliVersion: value, appVersion: null, currentVersion: value, mismatch: false };
125
+ }
126
+ return value;
127
+ }
128
+
129
+ function versionRelationLabel(info) {
130
+ return compareVersions(info.appVersion, info.cliVersion) < 0 ? '本体が古い' : 'CLI が古い';
131
+ }
132
+
133
+ function formatVersionMismatch(info) {
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}`;
74
139
  }
75
140
 
76
141
  /**
@@ -107,8 +172,9 @@ export function creatorRootPromptText(defaultPath) {
107
172
  * `akari update` の出力本文(複数行)。フィード未取得・最新・新版ありで案内が変わる。
108
173
  * `dismissed` は今回の実行で dismiss 記録を書いたかどうか(表示文言の切り替えのみに使う)。
109
174
  */
110
- export function describeUpdateCommand({ currentVersion, cache, dismissed }) {
111
- const lines = [`現在のバージョン: v${currentVersion}`];
175
+ export function describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed }) {
176
+ const info = versionInfo ?? normalizeVersionInfo(currentVersion);
177
+ const lines = versionInfo ? describeInstalledVersions(info) : [`現在のバージョン: v${currentVersion}`];
112
178
  const feed = cache?.feed;
113
179
  if (!feed) {
114
180
  lines.push('最新情報をまだ取得できていません(オフライン、または初回起動直後の可能性があります)。');
@@ -121,7 +187,12 @@ export function describeUpdateCommand({ currentVersion, cache, dismissed }) {
121
187
  lines.push(`リリースノート: ${feed.notes_url}`);
122
188
  }
123
189
 
124
- if (compareVersions(feed.product, currentVersion) <= 0) {
190
+ if (info.installRefNeedsRepair) {
191
+ lines.push('本体版を判定できないため、更新判定を行いません。');
192
+ return lines;
193
+ }
194
+
195
+ if (compareVersions(feed.product, info.currentVersion) <= 0) {
125
196
  lines.push('お使いのバージョンは最新です。');
126
197
  return lines;
127
198
  }
@@ -222,7 +293,7 @@ export function describeCliHelp() {
222
293
  ' (引数なし) プロジェクトを開いて AI エージェントを起動(未作成なら自動作成)',
223
294
  ' store connect アカウント連携(無料の素材パックと購入済み素材が使えるようになる)',
224
295
  ' sounds 公式音源ライブラリを一括ダウンロード(無料)',
225
- ' update 更新を確認する',
296
+ ' update [--force] 更新を確認する(--force で本体を入れ直す)',
226
297
  ' status 接続状態を確認する',
227
298
  ' migrate [dir] 古い edit.json を退避バックアップ付きで v2 へ変換',
228
299
  '',
@@ -378,6 +378,10 @@ export function rollbackSelfUpdate({ env = process.env, log = () => {} } = {}) {
378
378
  }
379
379
 
380
380
  const version = readVersionAt(appDir);
381
+ if (version) {
382
+ // install-ref が無い古い app-previous へ戻した場合も、以後の更新判定を本体版基準に保つ。
383
+ writeFileSync(join(appDir, '.akari-install-ref'), `v${version}\n`, 'utf8');
384
+ }
381
385
  log(version ? `v${version} へロールバックしました` : 'ロールバックしました');
382
386
  return { exitCode: 0, rolledBack: true, version };
383
387
  }
@@ -8,6 +8,8 @@ import {
8
8
  serializeWorkspaceStatus,
9
9
  formatWorkspaceStatusSummary,
10
10
  } from "./status-core/status.mjs";
11
+ import { describeVersionStatus } from "./messages.mjs";
12
+ import { readCacheSync, readOwnVersion, resolveCachePath, resolveInstalledVersionInfo } from "./update-check.mjs";
11
13
 
12
14
  export async function runStatusCommand(argv, options = {}) {
13
15
  const log = options.log ?? ((line) => process.stdout.write(line));
@@ -19,18 +21,24 @@ export async function runStatusCommand(argv, options = {}) {
19
21
  error(cause instanceof Error ? cause.message : String(cause));
20
22
  return { exitCode: 2 };
21
23
  }
24
+ const env = options.env ?? process.env;
25
+ const versionInfo = options.versionInfo ?? resolveInstalledVersionInfo({
26
+ env,
27
+ cliVersion: options.cliVersion ?? readOwnVersion(),
28
+ });
29
+ const versionLine = describeVersionStatus(versionInfo, readCacheSync(resolveCachePath(env)));
22
30
  if (detectStatusScope(parsed.projectRoot) === "workspace") {
23
31
  const workspace = resolveWorkspaceStatus(parsed.projectRoot);
24
32
  // fail-safe: a broken root.json falls through to the unchanged project-scope path below.
25
33
  if (workspace) {
26
- log(parsed.json ? serializeWorkspaceStatus(workspace) : `${formatWorkspaceStatusSummary(workspace)}\n`);
34
+ log(parsed.json ? serializeWorkspaceStatus(workspace) : `${versionLine}\n${formatWorkspaceStatusSummary(workspace)}\n`);
27
35
  return { exitCode: 0, workspace };
28
36
  }
29
37
  }
30
38
  const status = parsed.full
31
39
  ? await resolveFullProjectStatus(parsed.projectRoot)
32
40
  : resolveProjectStatus(parsed.projectRoot, { mode: "fast" });
33
- log(parsed.json ? serializeStatus(status) : `${formatStatusOutput(status)}\n`);
41
+ log(parsed.json ? serializeStatus(status) : `${versionLine}\n${formatStatusOutput(status)}\n`);
34
42
  return { exitCode: status.state_health === "inconclusive" ? 1 : 0, status };
35
43
  }
36
44
 
@@ -55,12 +55,63 @@ export function resolveCachePath(env = process.env) {
55
55
  return join(resolveAkariHome(env), 'update-check.json');
56
56
  }
57
57
 
58
+ export function resolveInstallRefPath(env = process.env) {
59
+ return join(resolveAkariHome(env), 'app', '.akari-install-ref');
60
+ }
61
+
58
62
  /** launcher 自身の package.json version(D3 裁定により現状はプロダクト版と一致)。 */
59
63
  export function readOwnVersion() {
60
64
  const raw = readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8');
61
65
  return JSON.parse(raw).version;
62
66
  }
63
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
+
86
+ /**
87
+ * 実際に render-cut / edit-lint を実行する本体の導入版。
88
+ * install.sh / self-update.mjs が書く `vX.Y.Z` を読む。互換 API として版または null を返し、
89
+ * 未記録と破損の区別は `resolveInstalledVersionInfo()` が保持する。
90
+ */
91
+ export function readInstalledAppVersion(env = process.env) {
92
+ return readInstalledAppVersionInfo(env).version;
93
+ }
94
+
95
+ /** 更新判定に使う版と、CLI / 本体のずれを一度に解決する。 */
96
+ export function resolveInstalledVersionInfo({ env = process.env, cliVersion = readOwnVersion() } = {}) {
97
+ const installRef = readInstalledAppVersionInfo(env);
98
+ const installedAppVersion = installRef.version;
99
+ const currentVersion = installedAppVersion ?? cliVersion;
100
+ return {
101
+ cliVersion,
102
+ appVersion: installedAppVersion,
103
+ currentVersion,
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',
111
+ mismatch: installedAppVersion !== null && compareVersions(cliVersion, installedAppVersion) !== 0
112
+ };
113
+ }
114
+
64
115
  /** "major.minor.patch" の先頭 3 要素だけを数値比較する(prerelease 考慮不要 — 契約 D4: stable のみ)。 */
65
116
  export function compareVersions(a, b) {
66
117
  const pa = parseVersionTriplet(a);
@@ -104,23 +155,35 @@ function writeCacheSync(cachePath, cache) {
104
155
  /**
105
156
  * キャッシュと現在版から、新版通知を出すべきかを判定する(同期・純粋関数・I/O なし)。
106
157
  */
107
- export function evaluateUpdateStatus({ currentVersion, cache }) {
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
+ };
108
170
  const feed = cache?.feed;
109
171
  if (!isValidFeedShape(feed)) {
110
- return { available: false };
172
+ return { available: false, ...versionDetails };
111
173
  }
112
174
  const latest = feed.product;
113
175
  if (compareVersions(latest, currentVersion) <= 0) {
114
- return { available: false };
176
+ return { available: false, latestVersion: latest, ...versionDetails };
115
177
  }
116
178
  const dismissedAt = cache?.dismissed?.[latest];
117
- if (dismissedAt) {
118
- return { available: false, dismissed: true, latestVersion: latest };
179
+ // CLI と本体の取り残しは release 通知の dismiss より強い。黙って古い本体を使わせない。
180
+ if (dismissedAt && !mismatch) {
181
+ return { available: false, dismissed: true, latestVersion: latest, ...versionDetails };
119
182
  }
120
183
  return {
121
184
  available: true,
122
185
  latestVersion: latest,
123
- currentVersion,
186
+ ...versionDetails,
124
187
  channel: typeof feed.channel === 'string' ? feed.channel : undefined,
125
188
  notesUrl: typeof feed.notes_url === 'string' ? feed.notes_url : undefined,
126
189
  cli: feed.components?.cli
@@ -131,9 +194,13 @@ export function evaluateUpdateStatus({ currentVersion, cache }) {
131
194
  * `akari` 起動時の同期チェック。ファイルの読み比較のみ(ネットワーク I/O 無し)。
132
195
  * fetch 関数への参照すら持たない — 「起動をブロックしない」を構造で保証する。
133
196
  */
134
- export function checkForUpdateSync({ currentVersion, env = process.env } = {}) {
197
+ export function checkForUpdateSync({ currentVersion, versionInfo, env = process.env } = {}) {
135
198
  const cache = readCacheSync(resolveCachePath(env));
136
- return evaluateUpdateStatus({ currentVersion, cache });
199
+ const versions = versionInfo
200
+ ?? (currentVersion === undefined
201
+ ? resolveInstalledVersionInfo({ env })
202
+ : { cliVersion: currentVersion, appVersion: null, currentVersion, source: 'cli-fallback', mismatch: false });
203
+ return evaluateUpdateStatus({ ...versions, currentVersion: currentVersion ?? versions.currentVersion, cache });
137
204
  }
138
205
 
139
206
  /** 「この版の通知を今後出さない」を記録する(同期・ローカル I/O のみ)。 */
@@ -157,6 +224,7 @@ export function recordDismissalSync({ version, env = process.env, now = new Date
157
224
  *
158
225
  * `launcherRoot` はテスト用の注入口(`isRunningFromAppDir` へそのまま渡す。
159
226
  * `runUpdateCommand` の `options.launcherRoot` と同じ流儀)。
227
+ * app 外から動く npm CLI でも install-ref があれば管理本体を staging 対象にする。
160
228
  */
161
229
  export async function maybeStageInBackground({ env = process.env, feed, fetchImpl = globalThis.fetch, timeoutMs, extract, launcherRoot } = {}) {
162
230
  if (env.AKARI_NO_AUTO_UPDATE === '1') {
@@ -166,26 +234,19 @@ export async function maybeStageInBackground({ env = process.env, feed, fetchImp
166
234
  if (!appComponent?.url || !appComponent?.sha256) {
167
235
  return null;
168
236
  }
169
- if (compareVersions(feed.product, readOwnVersion()) <= 0) {
237
+ if (compareVersions(feed.product, resolveInstalledVersionInfo({ env }).currentVersion) <= 0) {
170
238
  return null;
171
239
  }
172
- if (!isRunningFromAppDir({ env, launcherRoot })) {
240
+ // npm 側 CLI と本体が分離していても、install-ref があれば管理対象の本体を更新できる。
241
+ if (!isRunningFromAppDir({ env, launcherRoot }) && !readInstalledAppVersion(env)) {
173
242
  return null;
174
243
  }
175
244
  // 沈黙原則(U2 を継承): staging の失敗メッセージはどこにも表示しない(log は no-op)。
176
245
  return stageSelfUpdate({ env, feed, log: () => {}, fetchImpl, timeoutMs, extract });
177
246
  }
178
247
 
179
- /**
180
- * バックグラウンド fetch 本体。fetch 失敗・非 200・JSON パース失敗・スキーマ不明は
181
- * すべて沈黙して return する(何も throw しない)。`dismissed` は既存キャッシュから
182
- * 引き継ぐ(fetch のたびに既読状態が消えないように)。
183
- *
184
- * フィード取得・キャッシュ反映が成功した後、契約 §11 のバックグラウンド staging も
185
- * 同じ沈黙原則のもとで試みる(`maybeStageInBackground`)。成功したときだけ
186
- * キャッシュへ `staged`(版・sha256・staged_at)を追記する。
187
- */
188
- export async function runBackgroundFetch({ env = process.env, fetchImpl = globalThis.fetch, launcherRoot } = {}) {
248
+ /** フィードを 1 回取得し、正常なら既読状態を保ったままキャッシュへ反映する。 */
249
+ export async function refreshUpdateFeed({ env = process.env, fetchImpl = globalThis.fetch } = {}) {
189
250
  const feedUrl = resolveFeedUrl(env);
190
251
  const cachePath = resolveCachePath(env);
191
252
  try {
@@ -202,15 +263,34 @@ export async function runBackgroundFetch({ env = process.env, fetchImpl = global
202
263
  }
203
264
  const feed = await response.json();
204
265
  if (!isValidFeedShape(feed)) {
205
- return;
266
+ return null;
206
267
  }
207
268
  const existing = readCacheSync(cachePath);
208
- writeCacheSync(cachePath, {
269
+ const next = {
209
270
  schema: CACHE_SCHEMA,
210
271
  fetched_at: new Date().toISOString(),
211
272
  feed,
212
273
  dismissed: existing?.dismissed ?? {}
213
- });
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
+ }
214
294
 
215
295
  const staged = await maybeStageInBackground({ env, feed, fetchImpl, launcherRoot });
216
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",
@@ -91,3 +91,9 @@ edit.json の `version` は変わらないし、edit.json が `version: 1` に
91
91
  - 本体から外した後のエラー文言は次で固定する。
92
92
 
93
93
  > このプロジェクトは古い形式です。`npx akari-migrate@<版> <dir>` で変換してから開いてください。
94
+
95
+ ### 6.1 音声トラック化(2026-08-21・凍結前の最後の追加)
96
+
97
+ task `2026-08-20-v2-audio-tracks` に限り、「機能追加禁止・バグ修正のみ」の明示承認済み・1 回限りの例外として、v0/v1 の `audio.sfx[]`・`audio.narration[]`・`audio.bgm` を v2 の audio lane `tracks[].items[]` へ移す変換を追加した。出力側の `at` / `duration` は整数フレームへ確定し、素材側の `in` / `out` と fade / `bgm.in` は秒のまま保持する。実尺が旧形式だけでは決まらない音声は `duration: 0` センチネルとし、この変換器では ffprobe しない。トップレベル `audio` は `master` が宣言されている場合だけ残す。
98
+
99
+ これは凍結前に許可された最後の機能追加である。今後この変換器へさらに変換能力を足さず、§6 の期限どおり本体から分離・削除する。
@@ -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 の表示挙動は変更しない。