markharness

0013: 可変IDと不変Identityを分離するKnowledge同一性モデル

ステータス

Accepted(2026-08-22。実装完了・checklist-immutable-identity-model.md参照)。下記「移行」節のschema_version = 2という値は0018によりschema_version = 1へ訂正されている。retire・restore・release・reissueに関する決定は0021により置き換えられた。UID発行・UIDとidの分離・rename時のUID維持を含む本ADRのその他の内容はそのまま有効。

背景

0004は、Feature id:変更後も履歴を追跡したいという具体的な要望が出た場合に移行方法を再検討するとしていた。Issue #17により、ドメイン用語変更、命名規則統一、他システムからの移行、repository統合、組織・製品再編という具体的な必要性が示され、この条件が満たされた。

当初はfeature.ymlへ旧IDのaliasesを追加する案、次にFeatureだけへ不変uidを追加する案を検討した。しかし、いずれも次の問題を局所的にしか解決しない。

根本原因は、人間向けの可変な名前と機械的な同一性を同じ文字列IDに担わせていることである。実装コストを判断材料にせず、この二つをKnowledgeモデル全体で分離する。Git上の任意の2 ref snapshotから結果を決定的に再計算できるという既存の原則(論文§3.2〜3.4)は維持するが、入力の役割を分離する。内容変更のChangeEventは従来どおり2つのKnowledge snapshotから導出し、稀なidentity lifecycle宣言だけがそのsnapshot間で論理的同一性を解決する。Identity eventは通常編集の操作ログではない。入力範囲はFeature tree SHAだけからcommit済み.markharness snapshotへ一般化する。

この設計は、外部データベースプロセスや専用サーバーを一切必要としないという意味では、論文§1.4「専用DB不要」の字義を満たす。しかし実態としては、Git管理されたappend-only identity eventログとそのreplayによる導出・crash-recovery protocolという、軽量なevent-sourcing型ストレージエンジンをGit管理ファイルの上に自作することになる。これは「専用DBが持つ複雑さそのものを避ける」という同フレーズの含意からは踏み出す選択である。この判断はIssue #17の要求(削除後も残る同一性追跡)から見て有用性の観点で正当化される。論文§1.4はそのため、Gitを唯一の永続化境界とし、軽量identity event storeをリポジトリ内に持ち、Git外の正準永続化サービスを持たないという正確な境界を記述する。

決定内容

1. 全ての永続ドメイン要素へ不変UIDを導入する

Requirement、Feature、Behavior、Condition、ExpectedResultに不変uidを持たせる。UIDはCLIが要素の発行時に生成する26文字のULIDとする。

uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV
id: task-management
label: Task Management

役割を次のように分離する。

用途 変更
uid 内部同一性、関連付け、外部連携 不可
id CLI入力、人間可読な外部識別子 明示操作で可能
label 自由な表示名 可能
path Git上の配置 可能

ChangeEvent、lineage、verification、execution、canonical import、derived index、server履歴表示はUIDを同一性キーに使う。IDは対象ref内でUIDへ解決する。同一snapshot内のUIDとIDはそれぞれ一意でなければならない。

5要素種別に共通する実装構造

5要素種別へ同じidentity lifecycleを個別実装しない。UID発行、rename、retire、restore、release、reissue、event replay、Registry導出、migration、共通validationは、一つの深いIdentity ModuleのImplementationへ集約する。その小さなInterfaceは少なくとも次の共通ドメイン型を受け渡す。

要素種別ごとの差は、親kind、marker file、schema名、ID policyなどの宣言的なEntityDescriptorへ置く。種類固有の読み書きが本当に異なる箇所だけに薄いAdapterを置き、lifecycle規則をAdapterへ複製しない。1種類しか実装がない振る舞いのために抽象的なSeamを増やさない。

Knowledgeの親子参照も可変IDではなくUIDを正準にする。requirement_uidfeature_uidbehavior_uidcondition_uidを保存し、IDは表示・CLI解決用のprojectionとする。これにより親のrenameが子孫ファイルの関係書換えを発生させない。

Rustのdomain type、配布JSON Schema、markharness initが生成するschemaを別々の正準として手作業で同期しない。共通IdentityHeaderを含むRustのdomain typeをschemaの単一の正準情報源とし、配布schemaとinit schemaを決定的に生成する。生成物をcommitする場合、CIは再生成差分を拒否する。

EntityKindへ同じcontract test suiteを適用する。少なくともUID必須、重複拒否、rename event、event replayとKnowledgeの一致、cache有無の等価性、migration冪等性、crash recovery、descriptor/schema/fixtureの網羅性を種類ごとに検証する。種類追加時はEntityKind、descriptor、schema、fixtureの不足を一つの網羅性testで検出する。

2. identity宣言を正準情報源とし、Registryは派生物に保つ

現在のKnowledge treeから消えた同一性も追跡するため、.markharness/identity-events/のappend-only eventをidentity lifecycleの唯一の正準情報源とする。eventはknowledge/と同じく通常のGit管理ファイルであり、外部データベースやツール内部の状態ではない。記録するのは、最終content snapshotだけから意図を復元できない発行、rename、retire、restore、release、reissueに限る。通常のKnowledge編集はidentity eventにせず、内容ChangeEventは2 snapshot差分から事後導出する。

.markharness-cache/identities/のIdentity Registryは、対象refに含まれるidentity eventを決定的にreplayして得られる非commitのmaterialized viewとし、既存id解決cacheと同じ設計原則に従う。削除後も同じrefのeventだけから再構築できなければならない。Knowledge YAMLは現在のKnowledge内容の投影であり、validatorはevent replayとKnowledge YAMLを直接比較する。Registry cacheがある場合はcontent-addressed cache keyとreplay結果の一致後だけ使う。CLIはrename・作成・retire・restore・release・reissueのevent追加とKnowledge YAML変更を一つのcrash-recoverableなidentity operationとして行い、cacheは後から再生成または無効化する。

# .markharness-cache/identities/features/01ARZ3NDEKTSV4RRFFQ69G5FAV.yml
uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV
kind: feature
status: active
current_id: task-management
id_history:
  - id: todo-management
    from_identity_event_uid: 01ARZ3NDEKTSV4RRFFQ69G5FE0
  - id: task-management
    from_identity_event_uid: 01ARZ3NDEKTSV4RRFFQ69G5FE1

RegistryはUIDの発行、lifecycle、ID履歴を表す派生snapshotである。Registryの欠落は正常であり再構築を起動する。存在するcacheが古い、または不整合な場合は破棄する。要素を削除してもreplay結果にはretiredとして残る。

各要素のlifecycle eventは因果graphを構成する。発行eventは先行eventを持たず、通常の後続eventはprevious_identity_event_uidでそのsnapshot内の現在headを参照する。競合解決eventはprevious_identity_event_uidsで解決対象の全divergent headをjoinする。replay順序はこれらの先行参照で定め、filename、recorded_at、filesystemの列挙順、ULIDの時刻順に依存しない。独立した要素graphは任意順でreplayでき、byte-for-byteで同じ結果を生まなければならない。同じheadを伸ばす2つのeventはbranch divergenceである。各branch snapshotは個別に正常だが、両方を含むsnapshotは明示的な解決eventを必要とし、なければ曖昧性エラーにする。

この設計により、changes computeが読む入力範囲は、従来の「knowledge/配下のFeatureツリーSHA」から「ある時点でコミットされた.markharnessスナップショット全体(knowledge/identity-events/・migration manifest・必要な生成物を含む)」へ一般化される。任意の2つのrefの比較は、各snapshotにeventをreplayするだけで完結し、Git commit historyを辿らない。同じentity UIDが両snapshotにある場合、root発行event UIDとcanonical payloadが一致し、両snapshotに共通する全event UIDのcanonical contentがbyte単位で同一でなければならない。rootが異なる、または共通eventが書き換えられている場合は継続性ではなくidentity conflictとする。branch固有のsuffixは個別にreplayし、branch統合時にその和集合をvalidationする。

ChangeAnalyzerverify等の中核Moduleの結果は、比較対象2refのコミット済み.markharness snapshot、明示されたoptions、identity canonicalization version、tool versionだけで決定される。working tree、現在のHEAD、外部DB・外部service、wall-clock time、乱数、非コミットcache、第三のrefには依存しない。cacheを利用しても、cacheを削除した場合とbyte-for-byteで同じ結果を返さなければならない。

Git commit historyを走査するIdentityAuditorは、両方の選択snapshotから消えたevent、共通event集合外の過去改変、repository履歴上のUID再利用を検出する。2-ref比較とは別Moduleである。ChangeAnalyzerは比較対象2 snapshotの整合性に加え、root発行eventと共通eventの同一性を保証するが、repository全体のappend-only完全性やcross-branch履歴の網羅性までは主張しない。changes computeverify等の中核パスはIdentityAuditorに依存しないが、出力でこの狭い監査境界を明示する。

snapshot内のevent replayにより、UID重複・変更・再利用、旧ID再利用、削除済み要素の再出現、repository統合時の衝突を検証できる。一度あるUIDへ発行されたIDは、明示的なrelease event(下記)で解除されない限り、同じUIDのrestoreを除き別UIDへ再割り当てできない。過去commitに対するeventの削除・改変はIdentityAuditorが検出する。

3. renameとlifecycle変更を第一級イベントとして保存する

renameは通常のYAML編集ではなく、CLIによる明示的なドメイン操作として行い、.markharness/identity-events/へappend-only eventを追加する。

identity_event_uid: 01ARZ3NDEKTSV4RRFFQ69G5FE1
previous_identity_event_uid: 01ARZ3NDEKTSV4RRFFQ69G5FE0
type: feature_renamed
entity_uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV
from_id: todo-management
to_id: task-management
recorded_at: 2026-08-20T12:34:56Z

markharness feature rename-id <old> <new>は、UID維持、ID重複検証、Knowledge YAML更新、event追加、Registry cache無効化、全体validationを一つのidentity operationとして実行する。手編集によるID変更は、対応するevent遷移がないためvalidation errorになる。

作成、retire、restore、release、import時のreissueも同じeventモデルで記録する。releaseは、retired状態のUIDに紐づく旧idの再利用予約を明示的に解除するeventであり、以後そのidを別のUIDへ新規発行できるようにする。対象snapshot内のevent順序矛盾はreplay時に検出し、過去commitに対するevent fileの変更・削除はIdentityAuditorが検出する。

crash recovery方針

複数ファイルをOSレベルで同時に書き換える「真のmulti-file atomic write」や、通常エラー時に全ファイルを必ず旧状態へ戻すrollbackを必須方式とはしない。必要な保証は、処理途中の状態を正常状態として公開せず、通常エラー・プロセスkill・system crash後の次回起動時に、旧状態またはcommit済みの新状態のいずれかへ収束することである。

identity operationは、少なくともtransaction intent、staging、単一の論理commit point、recovery情報を持つ。commit pointより前の未完了operationは破棄または旧状態へ復旧し、commit pointより後は正準identity eventからKnowledge projection・生成物を冪等にroll-forwardし、派生cacheを無効化または再生成する。対応するcommitted operationが存在する不一致はrecovery対象とし、operation記録のない不一致は不正な手編集としてvalidation errorにする。

通常コマンドは開始時に未完了operationを検出し、recoveryを完了するまで通常処理を行わない。同時実行はlockで制御する。recovery自身が途中停止しても再実行可能でなければならない。既存のknowledge_apply::apply_batchのbest-effortな個別ファイル削除は、この保証を満たすtransactionプリミティブとはみなさない。

4. TestCaseとExecutionも不変Identityで追跡する

生成TestCaseには不変case_uidを割り当てる。case_idは現在の人間可読な投影として維持するが、照合には使わない。

case_uid: 01ARZ3NDEKTSV4RRFFQ69G5FT1
case_id: tc-task-management-create-task-empty-title
generated_from:
  requirement_uid: 01ARZ3NDEKTSV4RRFFQ69G5FAA
  feature_uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV
  behavior_uid: 01ARZ3NDEKTSV4RRFFQ69G5FB1
  condition_uid: 01ARZ3NDEKTSV4RRFFQ69G5FC1
  expected_result_uids:
    - 01ARZ3NDEKTSV4RRFFQ69G5FD1

case_uidは、requirement_uidfeature_uidbehavior_uidcondition_uidexpected_result_uidの集合(canonical順に整列)から決定的に導出する(例:これらを連結した値への決定的ハッシュ)。これは新たな永続ストア(TestCase Identity Registryのようなもの)を必要としない純粋関数であり、generateが既に持つ決定性・純粋性(同じKnowledgeスナップショットからは常に同じ出力が得られる性質)をそのまま維持する。同じprovenance集合であれば再生成のたびに同じcase_uidが得られる。ID・label・path変更やprovenance構成要素自体の内容変更ではTestCase identityを維持し、Conditionの分割などprovenance構成要素のUID集合そのものが変わる場合は新しいTestCaseとする。

execution recordはexecution_uidcase_uidfeature_uid、実行時点のcase_id、検証したFeature tree SHAを保存する。verificationはcase_uidで再実行を照合する。

5. ChangeEvent自体にも不変UIDを導入する

change_event_uidを内部参照キーとし、現在のevent_idは人間可読な表示値へ位置づけを変える。change_event_uidはULIDを新規発行せず、2-ref再計算時に決定的に導出する。

change_event_uid: 8f8a3c5d-2df5-5ca7-95ef-11e405455a07
event_id: task-management--v2--v3
feature_uid: 01ARZ3NDEKTSV4RRFFQ69G5FAV
feature_id_at_from: todo-management
feature_id_at_to: task-management
from_milestone: v2
to_milestone: v3

導出にはdomain separator、identity canonicalization/algorithm version、from/to snapshot identity、対象feature_uid、canonical change payload、結果に影響する明示optionsを、型tagと長さを含むcanonical encodingで入力したUUIDv5または同等のversioned hashを使う。一般的なtool versionはUIDへ直接含めず、UIDの意味が変わる場合だけalgorithm versionを上げる。同じ入力からは常に同じUIDを得て、入力境界の衝突を許さない。annotation、related event、verificationとの関連はchange_event_uidで保存する。過去・現在の表示IDは監査情報として保持する。

6. copy、import、repository統合の規則

移行

実装は、(1)共通Identity Moduleとcrash-recovery機構、(2)Featureを使ったend-to-endのvertical slice、(3)残る4種類のdescriptor/Adapter、(4)全要素migration、(5)schema version 2の公開cutover、の順で進める。Feature vertical sliceは共通設計とInterfaceを早期検証する内部段階であり、FeatureだけがUID modeになる中間形式を公開・永続サポートしない。公開cutoverまでは既存schema versionを維持し、cutoverは5種類を一括して切り替える。

project-level markerを.markharness/config.tomlへ追加する。

[identity]
schema_version = 2
mode = "uid"

markharness identity migrateは次を一つのcrash-recoverableなidentity operationとして行う。

  1. 全Knowledge要素へUIDを発行する。
  2. 初期発行eventを作成し、非commitのIdentity Registry cacheを導出する。
  3. 既存TestCaseへcase_uidを割り当てる。
  4. 既存ChangeEvent・executionとのlegacy mappingを作成する。
  5. schema、cache/index canonicalization version、project markerを更新する。
  6. 生成物を再生成して全体validationを実行する。

dry-runでは予定するUID、競合、変更ファイルを表示する。論理commit pointより前の失敗ではUID modeを有効にせず、commit後の失敗では次回起動時に冪等なroll-forwardで移行を完了する。partial migrationは正規状態として通常コマンドへ公開しない。

過去refと既存成果物を読み取るため、migration manifestへlegacy snapshot identity(tree SHA)、entity kind、旧ID、旧path/content locator、旧case IDと新UIDの対応を保存する。比較方向にかかわらず、2つのsnapshotに含まれるmanifestを対称に収集し、snapshot-qualified keyから一意に解決する。mappingがない、または複数候補が残る場合は決定的なエラーにする。UID modeへの移行後にUIDなし要素が追加された場合は通常コマンドを拒否し、明示的なrepair/import操作を要求する。移行済みかどうかはFeature数やUIDの有無ではなくproject markerで判定する。

検証規則

0004およびIssue #17の要件への対応

Acceptedへ変更する条件

将来の再検討条件