/** * edit.json / captions.json の atomic 書き込みと保存後 lint(Node 専用)。 * * 由来: apps/shell の akari-annotations-service.ts / akari-preview-service.ts に * 複製されていた assertLintPasses / runEditLint / findEditLintBinPath / writeAtomic を * ここへ一本化した(プレビュー・パリティ契約 §2.7「すべての書き込み経路は edit-lint を通す」)。 * preview-server の PUT ハンドラも同じ共有層を使う。保存前ゲートだった lint は * task 2026-08-18-shell-write-path-latency で保存後 debounce lint へ移した。 * * 比較なしの保存は lock + tmp + rename、比較つきの保存は journal と退避も使う。 * edit-lint は保存後 400ms の末尾 * debounce で同じプロジェクトにつき最新 1 本だけをプロセス内実行する。lint が * 利用できない場合は従来どおり fail-open とし、編集不能にはしない。 * * fail-open(オーナー裁定 2026-08-02、初出 2026-07-26 editlint-packaged-resolve): * lint 実行系が見つからない場合は書き込みを全面ブロックせず検証スキップで続行する。 * 編集不能より lint なし保存の方が被害が小さいという判断(型不正は各書き込みの * ローカル検証が別途残るため安全側は保たれる)。 */ import { promises as fs } from 'fs'; export declare const SAVED_BY_PATH = ".akari/saved-by.json"; export interface EditLintFinding { severity?: string; message?: string; check?: string; path?: string; range?: { start?: number; end?: number; }; /** 今回の保存より前の lint にも同じ指摘があった(この保存が起こした問題ではない)。 */ preexisting?: boolean; } export interface EditLintGateResult { pass: boolean; errors: string[]; findings: EditLintFinding[]; } /** 候補ファイル名(プロジェクト直下からの相対パス)→ 書き込み予定の全文。null は不在扱い。 */ export type LintCandidates = Record; export interface DeferredLintOptions { /** edit.json を保存した AKARI Video の版。不明なら既存スタンプを消す。 */ appVersion?: string; debounceMs?: number; onLintResult?: (result: EditLintGateResult) => void | Promise; /** Deterministic test seam; production callers use runEditLint. */ lintRunner?: (projectRoot: string) => Promise; /** * atomic rename が完了した「直後」に、書けた全文を同期で渡す。 * onWillWrite(rename 直前・自己書き込み由来 watcher の抑止用)の対になる通知で、 * 用途は「書き込みの発生を watcher より先に知らせる」こと。 * 購読側(プレビュー拡張)は file watcher の通知を待たずに差分判定へ入れる。 * ここで例外を投げても保存は完了済みなので、呼び出し側は握りつぶして保存を維持する。 */ onDidWrite?: (filePath: string, content: string) => void; onRecovered?: (notice: string) => void; } /** 影プロジェクトの 1 エントリをどう実体化したか。 */ export type ShadowEntryStrategy = 'symlink' | 'copy' | 'skip'; /** * 影プロジェクト構築の決定論テスト用シーム。本番呼び出しは既定(fs.symlink)のままで、 * 何も渡さなければ挙動は従来と同一。 */ export interface ShadowLintHooks { /** symlink の差し替え口。権限のある環境/無い環境をテストから作り分けるためだけに使う。 */ symlink?: (target: string, path: string, type: 'junction' | 'file') => Promise; /** エントリ 1 件ごとに実体化の手段を通知する観測口。 */ onShadowEntry?: (name: string, strategy: ShadowEntryStrategy) => void; /** 影プロジェクトを作れず候補のメモリ検証へ退避したことを通知する観測口。 */ onShadowUnavailable?: (reason: string) => void; } /** * 実ファイルは変更せず、候補全文だけを options.inputOverrides で差し替えて検証する。 * 既存 export のシグネチャは維持し、preview-server の保存前検査にも使える。 */ export declare function lintProjectCandidates(projectRoot: string, candidates: LintCandidates): Promise; /** * 実ディスクを直接読む lint check(motion 袋参照等)を含め、候補一式を保存前に検証する。 * 元プロジェクトの直下エントリは影プロジェクトへ symlink し、候補の祖先だけを実体化する。 * 既存 lintProjectCandidates の inputOverrides 契約は変更せず、Project API だけがこの入口を使う。 * * リンクが使えない環境(Windows の非特権ユーザー等)では**ファイルだけコピーへ倒す**。 * 影プロジェクトは lint の読み取り専用ステージングなので、ファイルはコピーで等価であり、 * かつ影側への書き込みが元プロジェクトへ伝播しない(symlink 経路と同じ安全性)。 * ディレクトリはコピーしない: プロジェクト直下には assets/(4K 原本が何十 GB)が来るため、 * junction も作れない環境では影プロジェクトの構築自体を諦め、候補のメモリ差し替えだけで * 検証する(実ディスクを読む check は落ちるが、保存は止めない — 冒頭の fail-open 裁定)。 */ export declare function lintProjectCandidatesOnDisk(projectRoot: string, candidates: LintCandidates, hooks?: ShadowLintHooks): Promise; /** 互換 API。保存後 lint への移行後も、明示的に検証したい呼び出し側向けに残す。 */ export declare function assertLintPasses(projectRoot: string, candidates: LintCandidates): Promise; /** CLI / preview-server 等のプロセス内で書き手の版を 1 回だけ設定する。 */ export declare function setDefaultSavedByAppVersion(version: string | undefined): void; /** CLI の既存 edit.json 保存直後に、スタンプだけを atomic に更新する。 */ export declare function writeSavedByStamp(projectRoot: string, appVersion: string | undefined): Promise; export declare function projectFileHash(bytes: Buffer | Uint8Array | string): string; export declare class ConflictError extends Error { readonly code = "AKARI_WRITE_CONFLICT"; readonly files: string[]; constructor(files: string[]); } type TransactionFile = { expectedHash?: string | null; next: string | null; }; type TransactionHooks = { now?: () => number; processAlive?: (pid: number) => boolean; rename?: (source: string, target: string, index: number) => Promise; crashAfterJournal?: boolean; crashAfterFirstRename?: boolean; fs?: Partial>; }; export type TransactionOptions = { waitTimeoutMs?: number; writeUnchanged?: boolean; onRecovered?: (notice: string) => void; onWillWrite?: (destination: string, name: string) => void; onDidWrite?: (path: string, content: string, name: string) => void | Promise; hooks?: TransactionHooks; }; export type RecoveryDetail = { restored: string[]; preserved: string[]; missing: string[]; broken?: true; }; /** 保存の onDidWrite 通知の中から await しない。 */ export declare function recoverInterruptedProjectWrite(projectRoot: string): Promise; export declare function recoverInterruptedProjectWriteDetailed(projectRoot: string): Promise; export declare function writeProjectFilesTransactional(projectRoot: string, input: { files: Record; label: string; }, options?: TransactionOptions): Promise<{ written: string[]; recovered?: string; }>; /** atomic 保存を即時完了し、lint は末尾 debounce で非同期に実行する。 */ export declare function writeProjectFilesGuarded(projectRoot: string, candidates: LintCandidates, options?: DeferredLintOptions): Promise; export declare function captureProjectLintBaseline(projectRoot: string): Promise; /** Web UI 旧版が生成した camelCase は schema が閉じていない legacy edit でも保存させない。 */ export declare function assertNoCamelCaseTransitionOut(content: string): void; /** * 同じプロジェクト宛ての連続保存をまとめ、最後の状態だけを lint する。 * 保存後 lint は実 projectRoot に対して writeReports=true で走るため、結果は * `.akari/lint.json` と `.akari/reports/edit-lint-report.html` へ書かれる。 * これは render-cut が読む PASS ゲートを常に最新に保つ、意図した保存後 lint の副作用。 */ export declare function scheduleProjectLint(projectRoot: string, options?: DeferredLintOptions): void; /** * 保存前の lint にも有った指摘へ preexisting を付ける。字幕の挿入で配列の番号がずれても * 同じ指摘と分かるよう、時刻範囲を持つ指摘は path の番号ではなく範囲で照合する。 * 基準が無い(初回・lint.json 不在)ときは従来どおり全部を今回の保存の指摘として返す。 */ export declare function markPreexistingLintFindings(result: EditLintGateResult, baseline: readonly EditLintFinding[] | undefined): EditLintGateResult; export declare function writeAtomic(destination: string, content: string, rename?: (source: string, target: string) => Promise): Promise; /** * edit-lint をプロセス内実行する。inputOverrides は候補全文のメモリ差し替え。 * writeReports=false は保存前候補検査用で実プロジェクトへレポートを書かない。 * 既定の true は保存後 lint 用で、projectRoot の `.akari/lint.json` と HTML レポートを更新する。 */ export declare function runEditLint(projectRoot: string, inputOverrides?: LintCandidates, writeReports?: boolean): Promise; export declare function findEditLintModulePath(): string; /** 既存の複数候補探索を維持する。 */ export declare function findEditLintBinPath(): string; export {};