自分のこれまでの活動や経験を構造化・可視化する個人プロジェクト「HASM (Human Activity Structuring Model)」を進めています。
今回は、そのHASMで効率的に人生の記録を編集することを目的として開発している hasm_markdown(Tauri v2 + React)について、以下の2つの柱に絞って開発ログを記載します。
- HASM Markdown が目指す「ポータブル化」の仕組み
- ほぼ初心者の状態から AI(Gemini / Github Copilot)で開発を進めたプロセス
HASM Markdown の「ポータブル化」とは?
普通のMarkdownやWordと何が違うのか?
Markdownファイルでドキュメントやログを管理する際、必ずぶつかるのが「画像アセットのパス崩れ」です。HASM Markdownは、「プレーンテキストの検索性・差分管理のしやすさ」と「1ファイル/1フォルダで完結する持ち運びやすさ」を両立させるために設計しました。
従来のツールとの違いを表にまとめると以下のようになります。
| ツール | アセットのポータブル性(リンク切れ耐性) | 検索性・差分管理(Git Diff等) | ファイルの透明性(専用アプリ不要で中身が見えるか) |
|---|---|---|---|
| 標準のMarkdown | ×(環境移行・フォルダ移動で容易にリンク切れする) | 〇(プレーンテキストのため容易) | 〇(任意のテキストエディタで開ける) |
| Word / PDF | 〇(ファイル内に画像が埋め込まれる) | ×(バイナリのためDiff管理は困難) | ×(専用アプリですべてを開く必要がある) |
| HASM Markdown(目標) | 〇(1ファイル・1フォルダ内に実体を内包) | 〇(プレーンテキストを維持) | 〇(ZIPやフォルダ構造のため中身が透明) |
共有するファイルの正体
ポータブル化するにあたり、共有するファイルは以下になります。
<AppLocalDataDir>/<UUID>/
├── .lock # 同時編集を防ぐためのプロセスロック管理ファイル
├── main.md # ユーザーが編集するMarkdown本文
├── assets.json # 画像エイリアスとUUID等を紐付ける台帳(メタデータ)
└── assets/ # UUID名で保存された画像ファイルの実体ディレクトリ
├── 3f8b9a20-1c2d-4e5f-8a9b-0c1d2e3f4a5b.png
└── 9e8d7c6b-5a4f-3e2d-1c0b-a9b8c7d6e5f4.jpg
それぞれのファイルはアプリ専用ではないので、中身を見れば内容はわかります。しかし、ファイル構造自体がアプリ専用になってしまっている点は今後の改善点です。どんなエディタでも扱えるようにしたいと思っています。
二層ストレージ保存構造の仕組み
このポータブル化と、大容量アセットを扱った際のパフォーマンスを両立するために、HASM Markdown では二層ストレージ構造を採用しています。
- Temporal Layer(App Local 領域:一時作業場所)
普段エディタで作業しているときはこの層を使います。画像を追加しても重いコピー処理は行わず、メタデータ(main.md や assets.json)だけを高速に扱います。
- Master Layer(保存先・共有用パッケージ)
ユーザーが明示的に保存した際や、他人に共有する際に作られる .hasmmd(ZIP)やフォルダです。ここに初めて画像の実体がまとめられます。
アセット管理(Asset Management)の3つのライフサイクル
エディタ上で直感的に  のようなエイリアス(登録名)を指定して画像を配置します。この裏側では、Dual Layer構造を活かして以下のような3段階のパス解決を行っています。
-
Asset Registration(登録時:コピーせず参照のみ)
画像をドラッグ&ドロップで追加した瞬間は、プロジェクト領域への実体コピーを行いません。OS上の元の絶対パスをそのまま参照し、assets.jsonにエイリアス(登録名)と紐付けて記憶させるだけに留めます。これにより、数GBの画像を追加してもUIがフリーズしません。 -
Save(保存時:実体コピーとポータブル化)
ユーザーが「Save」を実行し、.hasmmdアーカイブや共有フォルダを作成する段階で、初めてアセットの実体が Master Layer へコピーされます。このとき、参照していた絶対パスはすべてassets/UUID.pngのような環境に依存しない相対パス(ポータブル化)に書き換えられます。 -
Load(展開時:ローカル絶対パスへの再解決)
一度保存されたパッケージを別のPCや別の環境で開く際は、assets.jsonに記録された相対パスを読み込み、その環境におけるローカルの絶対パス(またはストリーミングURI)へ動的に再解決します。これにより、どんな環境へ持っていってもリンク切れを起こさず、エディタ上で正しく画像をプレビューできるようになります。
AIを使ってどう作ったか?(開発プロセス)
現状の自分のスキル感
- Rust: ほぼ初心者。所有権やライフタイム、TauriのIPC周りは手探り状態
- React: 学生時代にインターンで少し触った程度。JSXでの状態管理は曖昧
普段Web系の仕事をしているわけでもないので、今回は未知の領域からのスタートでした。そのため、「AIにいきなりコードを書かせる」やり方はすぐに破綻しました。そこで、「AIを相棒にして、設計から一緒に組み上げる」プロセスを採用しました。
AIと自分の役割分担
実際にどのような順序でAIを活用したかを図解すると、以下のようになります。
最初からコードを書かせない(アイデア共有〜設計書づくり)
いきなり「Tauriでエディタを作って」と頼むと、AIは適当なコードを出力して破綻します。
まずは「どんな機能が欲しいか」「なぜポータブル化したいのか」という概念を対話で共有し、ハイレベル設計書(System Overview) と 詳細設計書(Sequence Diagram) をMermaidの図付きでAIと一緒に作成しました。
どうやって機能を担保するかを先に考える(テストケースづくり)
コードを書く前に、「何が起きたら成功/失敗か」を定義する 評価仕様書(EVL) をAIに作らせました。
例えば、「存在しないパスが渡された時にエラーで落ちずに即座に Exit Code 1 で終了するか」といったエッジケースをあらかじめテストケースとしてリスト化し、検証の土台を固めました。
指示書(Copilot Instructions)を与えて実装してもらう
AIが勝手にコードの書き方やログの形式を変えないよう、.github/copilot-instructions.md を配置しました。
「シーケンス番号付きのログ([SEQ-MD-01])を出力すること」や「共有の hasm_logger を必ず使うこと」といったルールを指示書で縛ることで、一貫性のあるコードを実装してもらいました。
結局、バグがいっぱいあったので今頑張っています!
「AIと設計書から完璧に作った」と言えれば格好いいのですが、現実は甘くありませんでした。
いざ実装して動かしてみると、
- Mermaidの記法で
<br>や角括弧のエスケープが漏れて図がパースエラーを起こす - PlaywrightでのCIテスト時、GitHub Actions上にブラウザ本体が入っておらずテストが全落ちする
- 1GBを超えるアセットを入れた時の画面のちらつきやメモリリーク
- Rust側のロジックはテストされていたのに、ReactからRustのinvokeをテストできていなかったので、GUIで確認するとすべて動かない
など、想定外のバグが大量に噴出しました。
現在は、AIにログを見せながらデバッグを繰り返し、ひとつずつ泥臭くバグを潰している最中です。
おわりに
hasm_markdown はまだまだ完成には程遠く、デザインポリシーや操作感を含め、試行錯誤の連続です。
ですが、プログラミング言語に不安があっても、「設計 ➔ テストケース ➔ 実装」のステップをAIと一緒に踏むことで、個人開発でもここまで形にできるという手応えを感じています。引き続き開発ログとして進捗を残していきたいと思います!