Skip to content

feat(workflow): add repository identity model and SQLite state foundation - #45

Merged
yohnark merged 10 commits into
mainfrom
feat/31-workflow-state-foundation
Aug 7, 2026
Merged

yohnark merged 10 commits into
mainfrom
feat/31-workflow-state-foundation

Conversation

@yohnark

@yohnark yohnark commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Add repository/source/instance identity resolution and the SQLite workflow-state foundation: new repository_sources, repository_instances, repository_paths tables plus a workflow-scoped WorkflowStateStore interface and SQLite implementation, under src/workflow/domain/ and src/workflow/state/.

Linked issue

Closes #31

Scope

Included

  • src/workflow/domain/identity.ts: resolveRepositoryIdentity(cwd) resolves, for the Git repository at cwd, a RootCommitDigest (hash of root commit SHA(s) — a Git-derived hint, not a globally unique id) and a RepositoryInstanceId (a UUID persisted in a marker file inside the Git common directory, created on first resolution). Fails closed (structured { ok: false, reason }) on non-Git directories, unborn HEAD, and corrupted marker files.
  • src/state/migrations.ts: append-only migration (schema version 2) adding repository_sources, repository_instances, repository_paths. Existing migration 1 is untouched.
  • src/workflow/state/store.ts: WorkflowStateStore interface, scoped to repository identity, separate from the existing session/read-evidence StateStore.
  • src/workflow/state/sqlite-store.ts: WorkflowSqliteStateStore implementation, reusing resolveStateDir/resolveStateDbPath and the transactional migration-apply mechanism. observeRepositoryInstance() is the single write path: mints a RepositorySourceId (via crypto.randomUUID()) the first time a RootCommitDigest is seen, reuses it on subsequent observations, tracks canonicalized worktree paths per instance, and reports detected moves.
  • docs/workflow-policy.md, AGENTS.md: document the identity model and state foundation.

Excluded

  • No worktrees, tasks, locks/leases, or pr_records tables — owned by later child Issues, added when their domain contracts are concrete.
  • No protected-branch logic (separate child Issue).
  • No new SQLite library or ORM; reuses node:sqlite via the existing SqliteStateStore pattern.

Implementation

Two design points diverged from the most direct reading of the Issue, discovered while writing tests (both confirmed with the repo owner before implementing):

  1. RepositorySourceId is not derived purely from root commit SHAs. A hash of root commit SHA(s) alone is not globally unique — two independently initialized repositories with identical content (same tree, same author/timestamp/message) can produce the same root commit SHA, which a test reproduced directly. So identity.ts only computes a RootCommitDigest (a hint/lookup key), and WorkflowSqliteStateStore.observeRepositoryInstance() owns actual RepositorySourceId issuance: it mints a fresh UUID the first time a digest is observed and reuses it afterward via a root_commit_digest unique lookup, so a digest collision cannot mint a duplicate source.
  2. RepositoryInstanceId is not derived from the common-dir path. A path-derived id would change when a repository is moved/renamed, which defeats the Issue's own move-detection requirement (an "instance" must stay the same instance after a move). Instead, instanceId is a UUID written to a marker file inside the Git common directory (<git-common-dir>/mottainai-instance-id) on first resolution; since the common directory moves as a unit with the repository, the id survives renames. A corrupted marker file fails closed rather than silently re-minting an id (which would silently fork identity for a repository that state already tracks).

Also fixed during implementation: git rev-parse --git-common-dir returns a relative path (e.g. .git) for non-worktree repositories, which was being resolved against the calling process's cwd instead of the target repository's cwd before being canonicalized — this collapsed all test repositories to this project's own .git directory until fixed.

Behavioral changes

None for existing code paths — this PR only adds new, currently-unused modules under src/workflow/domain/ and src/workflow/state/, plus an additive (never-mutated) migration. src/workflow/state/sqlite-store.ts opens the same on-disk SQLite database as the existing session-scoped SqliteStateStore (same resolveStateDbPath()), so its migration runs automatically the next time any workflow or session state store is initialized.

Validation

  • Typecheck
  • Tests
  • Build
  • Package check

pnpm test (526/526 pass, including 16 new tests across identity.test.ts and sqlite-store.test.ts), pnpm run typecheck, pnpm run build, and pnpm run governance:test (18/18) all pass. Package check is not applicable: no path in packageCheckPaths (package.json, pnpm-lock.yaml, tsconfig.build.json, src/index.ts, src/server.ts, src/cli.ts, publish workflow) is touched.

Risks

Filesystem path identity varies across OS/symlinks/case-sensitivity; identity.test.ts covers symlinked paths and repository moves explicitly (both directly and through the SQLite move-detection path) rather than assuming POSIX semantics. The migration is append-only and must never rewrite existing src/state/migrations.ts entries — verified this PR only appends a new version: 2 entry. repository_sources.root_commit_digest collisions (same digest, different actual repositories) are handled by design (see Implementation) rather than assumed away.

Breaking changes

No.

Review focus

resolveOrCreateInstanceId() in src/workflow/domain/identity.ts (marker-file read/create/race-handling logic) and observeRepositoryInstance() in src/workflow/state/sqlite-store.ts (source-id issuance and move-detection transaction) — these are the two places where a subtle bug would silently fork or duplicate repository identity.

yohnark added 5 commits August 6, 2026 21:46
Append repository_sources, repository_instances, and repository_paths
tables (schema version 2) for Issue #28 Child 2. Existing migration 1
is untouched.
resolveRepositoryIdentity() derives a RootCommitDigest (a Git-derived
hint, not globally unique) and a stable RepositoryInstanceId from a
UUID marker file persisted inside the Git common directory, so
identity survives repository moves/renames without depending on
remote URL, filesystem path, or branch name alone. A corrupted marker
file fails closed instead of silently re-minting an id.
New repository-identity-scoped persistence interface, separate from
src/state/store.ts's session/read-evidence StateStore, so a future
per-repository DB split doesn't change workflow-domain APIs.
WorkflowSqliteStateStore shares the same DB file and migration
mechanism as the session-scoped SqliteStateStore. observeRepositoryInstance()
is the single write path: it mints a globally unique RepositorySourceId
the first time a given RootCommitDigest is seen (reusing it afterward,
so root-commit-digest collisions never produce duplicate sources),
tracks canonicalized worktree paths per instance, and reports detected
moves.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a4622b9-c151-4eba-92a3-768f9fb9b62d

📥 Commits

Reviewing files that changed from the base of the PR and between d8510bc and 14adc74.

📒 Files selected for processing (2)
  • src/workflow/domain/identity.test.ts
  • src/workflow/domain/identity.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/workflow/domain/identity.test.ts

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added reliable Git repository identity tracking across worktrees, symlinks, relocations, and repository moves.
    • Added persistent workflow state storage for repository sources, instances, and historical paths.
    • Repository observations now reuse existing records while preserving history and tracking path changes.
    • Added safeguards that reject corrupted identity data and unsupported repository states.
    • Added a command-line utility for resolving a repository instance identity.
  • Documentation

    • Added documentation describing repository identity resolution and workflow state behavior.

Walkthrough

The pull request adds Git repository identity resolution, migration version 2 for workflow state tables, and a SQLite-backed WorkflowStateStore. It tracks repository instances, canonical worktree paths, path moves, and persistent instance markers.

Changes

Repository workflow state

Layer / File(s) Summary
Repository identity resolution
src/workflow/domain/identity.ts, src/workflow/domain/identity-resolve-worker.mjs, src/workflow/domain/identity.test.ts
Adds stable Git-based repository identities, persistent instance markers, canonical path handling, concurrent creation handling, and fail-closed errors. Tests cover repository separation, symlinks, relocation, worktrees, orphan branches, unborn repositories, and corrupted markers.
Workflow state contract and schema
src/workflow/state/store.ts, src/state/migrations.ts
Defines workflow state records and operations. Migration version 2 adds repository source, instance, and path tables with constraints, defaults, foreign keys, and indexes.
SQLite observation and supporting validation
src/workflow/state/sqlite-store.ts, src/workflow/state/sqlite-store.test.ts, docs/workflow-policy.md, AGENTS.md
Adds SQLite initialization, transactional observation, source and instance lookups, path history, move detection, lifecycle handling, persistence checks, and workflow documentation. AGENTS.md lists the workflow layer.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitRepository
  participant resolveRepositoryIdentity
  participant WorkflowSqliteStateStore
  participant SQLite
  GitRepository->>resolveRepositoryIdentity: resolve canonical paths and Git root commits
  resolveRepositoryIdentity-->>GitRepository: return repository identity
  GitRepository->>WorkflowSqliteStateStore: observe repository identity and worktree path
  WorkflowSqliteStateStore->>SQLite: create or reuse source and instance
  WorkflowSqliteStateStore->>SQLite: record current and historical paths
  SQLite-->>WorkflowSqliteStateStore: commit observation
  WorkflowSqliteStateStore-->>GitRepository: return observation result
Loading

Possibly related issues

Possibly related PRs

  • yohn-jp/mottainai#1: Extends the shared SQLite migration infrastructure used by this workflow-state schema.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR meets the main identity and SQLite objectives, but it places RepositorySourceId in store.ts instead of identity.ts as specified by issue #31. Define or re-export RepositorySourceId from identity.ts, or update the issue contract if store.ts is the intended ownership.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the repository identity model and SQLite workflow-state foundation added by the pull request.
Description check ✅ Passed The description directly covers repository identity resolution, migrations, WorkflowStateStore, implementation scope, and validation.
Out of Scope Changes check ✅ Passed All changes support issue #31, including tests and documentation; no later workflow tables or unrelated behavior were added.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/31-workflow-state-foundation

Comment @coderabbitai help to get the list of available commands.

@yohnark

yohnark commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
src/state/migrations.ts (1)

79-86: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Enforce one current path per instance in the schema.

repository_paths allows several rows with is_current = 1 for the same instance_id. WorkflowSqliteStateStore.observeRepositoryInstance reads the current path with a single-row get() (src/workflow/state/sqlite-store.ts lines 141-143), so a broken invariant produces a non-deterministic current path and wrong move detection. Add a partial unique index while the migration is still unreleased.

Also, idx_repository_paths_instance duplicates the leftmost column of the primary key (instance_id, canonical_path). SQLite can serve instance_id lookups from the primary-key index, so the extra index only adds write cost.

♻️ Proposed change
         CREATE TABLE repository_paths (
           instance_id TEXT NOT NULL REFERENCES repository_instances (instance_id),
           canonical_path TEXT NOT NULL,
           is_current INTEGER NOT NULL DEFAULT 1,
           observed_at INTEGER NOT NULL,
           PRIMARY KEY (instance_id, canonical_path)
         );
-        CREATE INDEX idx_repository_paths_instance ON repository_paths (instance_id);
+        CREATE UNIQUE INDEX idx_repository_paths_current
+          ON repository_paths (instance_id) WHERE is_current = 1;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/state/migrations.ts` around lines 79 - 86, Update the repository_paths
schema in the migration to enforce at most one row with is_current = 1 per
instance_id using a SQLite partial unique index, and remove the redundant
idx_repository_paths_instance index because the primary key already supports
instance_id lookups.
src/workflow/domain/identity.ts (2)

60-60: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Tighten the marker validation pattern.

INSTANCE_ID_PATTERN accepts any 36-character string built from hex digits and hyphens, for example "------------------------------------". A stricter UUID shape makes the fail-closed corruption check reliable.

Also, if the race winner's content is invalid, the function returns cannot create instance marker file: EEXIST. That reason mislabels a corrupt marker. Return the corrupt-marker reason in that branch.

♻️ Proposed change
-const INSTANCE_ID_PATTERN = /^[0-9a-f-]{36}$/;
+const INSTANCE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
     if ((err as NodeJS.ErrnoException).code === "EEXIST") {
       try {
         const raceWinner = fs.readFileSync(markerPath, "utf8").trim();
         if (INSTANCE_ID_PATTERN.test(raceWinner)) return { ok: true, instanceId: raceWinner as RepositoryInstanceId };
+        return { ok: false, reason: `instance marker file is corrupt: ${markerPath}` };
       } catch {
         // 下の共通エラーへフォールスルー
       }
     }

Also applies to: 90-98

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/identity.ts` at line 60, Strengthen INSTANCE_ID_PATTERN
to validate the canonical UUID structure, including required hyphen positions
and hexadecimal group lengths, rather than accepting any 36-character hex/hyphen
string. In the race-winner validation branch of the instance-marker creation
flow, return the corrupt-marker reason when the existing marker content is
invalid instead of the EEXIST creation error.

42-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document platform-dependent case handling. fs.realpathSync.native resolves symlinks, but case normalization depends on the operating system and filesystem. Do not describe case normalization as a universal guarantee.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/identity.ts` around lines 42 - 45, Update the
documentation for canonicalizePath to state that it resolves symlinks and
normalizes trailing-slash differences, while clarifying that case normalization
is platform- and filesystem-dependent rather than guaranteed universally.
src/workflow/domain/identity.test.ts (2)

62-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a git worktree case.

resolveRepositoryIdentity documents that two worktrees of one repository share gitCommonDir and therefore return the same instanceId (src/workflow/domain/identity.ts lines 102-106). No test covers that path, and the linked issue lists worktree/path behavior as an acceptance item. Add a test that runs git worktree add, resolves identity from the linked worktree, and asserts an equal instanceId with a different worktreePath.

Do you want me to draft that test?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/identity.test.ts` around lines 62 - 79, Add a test
alongside the symlink identity test that creates a second checkout using git
worktree add, then resolves both repository paths with
resolveRepositoryIdentity. Assert both resolutions succeed, their instanceId
values match, and their worktreePath values differ, cleaning up the added
worktree in a finally block.

13-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add temp-directory cleanup.

initRepo creates a directory under os.tmpdir() and most tests never remove it. Each run leaves Git repositories behind. Register cleanup with the test context, for example t.after(() => fs.rmSync(root, { recursive: true, force: true })), or delete the directory at the end of each test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/identity.test.ts` around lines 13 - 22, Update initRepo
to register cleanup for the temporary repository directory, using the test
context’s after hook to remove root recursively with force enabled; adjust the
helper’s interface or call sites as needed so the cleanup runs for every test.
src/workflow/state/sqlite-store.test.ts (1)

6-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a file-backed store test.

Every test uses :memory:, so the file-backed branch of init() never runs (src/workflow/state/sqlite-store.ts lines 76-91): directory creation, 0o600/0o700 permissions, and WAL sidecar handling. Add one test that points dbPath at a temp file, then asserts that observation and lookups work and that the file mode is owner-only on non-Windows platforms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/state/sqlite-store.test.ts` around lines 6 - 10, Add a
file-backed test alongside openStore that creates a temporary database path,
initializes WorkflowSqliteStateStore, records an observation, and verifies
observation and lookup behavior. On non-Windows platforms, assert the database
file uses owner-only permissions, and ensure temporary resources are cleaned up;
cover the file-backed init path including directory and WAL handling without
changing existing in-memory tests.
src/workflow/state/sqlite-store.ts (1)

111-124: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Return the inserted row without a second SELECT.

The code selects, inserts when missing, then selects again. RETURNING removes the extra query and keeps the record construction in one place. The same applies to the instance block on lines 126-139.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/state/sqlite-store.ts` around lines 111 - 124, Update the source
and instance persistence blocks around toSourceRecord to replace the
pre-check/insert/second-SELECT flow with an INSERT ... ON CONFLICT ... RETURNING
query, while preserving existing-row reuse and constructing each record from the
returned row in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/workflow/domain/identity.ts`:
- Around line 129-140: The root commit digest derived in the current
identity-resolution flow is not stable across unrelated histories or orphan
branches. Confirm the intended repository-wide identity behavior, then update
the root commit lookup around runGit and deriveRootCommitDigest to use a stable
reference or deterministic fixed root, ensuring the same repository preserves
one RepositorySourceId as documented.

In `@src/workflow/state/sqlite-store.ts`:
- Around line 130-136: Confirm the intended replacement policy for a reused
git_common_dir, then update the existing-instance handling around the
repository_instances INSERT/UPDATE flow to resolve a stale row whose instance_id
differs but whose git_common_dir matches. Detach or remove the stale mapping
before inserting the new instance so the UNIQUE constraint is not violated,
while preserving normal last_seen_at updates for matching instance_id values.
- Around line 164-167: Update the catch block in applyMigrations to wrap
db.exec("ROLLBACK") in a nested try/catch, ignoring any rollback failure so the
original COMMIT error is rethrown. Match the existing guarded rollback pattern
used in the migrations implementation.

---

Nitpick comments:
In `@src/state/migrations.ts`:
- Around line 79-86: Update the repository_paths schema in the migration to
enforce at most one row with is_current = 1 per instance_id using a SQLite
partial unique index, and remove the redundant idx_repository_paths_instance
index because the primary key already supports instance_id lookups.

In `@src/workflow/domain/identity.test.ts`:
- Around line 62-79: Add a test alongside the symlink identity test that creates
a second checkout using git worktree add, then resolves both repository paths
with resolveRepositoryIdentity. Assert both resolutions succeed, their
instanceId values match, and their worktreePath values differ, cleaning up the
added worktree in a finally block.
- Around line 13-22: Update initRepo to register cleanup for the temporary
repository directory, using the test context’s after hook to remove root
recursively with force enabled; adjust the helper’s interface or call sites as
needed so the cleanup runs for every test.

In `@src/workflow/domain/identity.ts`:
- Line 60: Strengthen INSTANCE_ID_PATTERN to validate the canonical UUID
structure, including required hyphen positions and hexadecimal group lengths,
rather than accepting any 36-character hex/hyphen string. In the race-winner
validation branch of the instance-marker creation flow, return the
corrupt-marker reason when the existing marker content is invalid instead of the
EEXIST creation error.
- Around line 42-45: Update the documentation for canonicalizePath to state that
it resolves symlinks and normalizes trailing-slash differences, while clarifying
that case normalization is platform- and filesystem-dependent rather than
guaranteed universally.

In `@src/workflow/state/sqlite-store.test.ts`:
- Around line 6-10: Add a file-backed test alongside openStore that creates a
temporary database path, initializes WorkflowSqliteStateStore, records an
observation, and verifies observation and lookup behavior. On non-Windows
platforms, assert the database file uses owner-only permissions, and ensure
temporary resources are cleaned up; cover the file-backed init path including
directory and WAL handling without changing existing in-memory tests.

In `@src/workflow/state/sqlite-store.ts`:
- Around line 111-124: Update the source and instance persistence blocks around
toSourceRecord to replace the pre-check/insert/second-SELECT flow with an INSERT
... ON CONFLICT ... RETURNING query, while preserving existing-row reuse and
constructing each record from the returned row in one place.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f6f2c23-8ac7-406d-9766-a9a00b9ef0e6

📥 Commits

Reviewing files that changed from the base of the PR and between 5c83b01 and afcea0c.

📒 Files selected for processing (8)
  • AGENTS.md
  • docs/workflow-policy.md
  • src/state/migrations.ts
  • src/workflow/domain/identity.test.ts
  • src/workflow/domain/identity.ts
  • src/workflow/state/sqlite-store.test.ts
  • src/workflow/state/sqlite-store.ts
  • src/workflow/state/store.ts

Comment thread src/workflow/domain/identity.ts
Comment thread src/workflow/state/sqlite-store.ts
Comment thread src/workflow/state/sqlite-store.ts
yohnark added 3 commits August 6, 2026 22:55
Address CodeRabbit review on PR #45:
- rootCommitDigest now hashes root commits reachable from any ref
  (`rev-list --max-parents=0 --all`) instead of HEAD only, so switching
  branches or adding an orphan branch no longer changes the digest of
  an already-known repository.
- INSTANCE_ID_PATTERN now validates the canonical UUID shape instead
  of accepting any 36-char hex/hyphen string.
- A race-winner marker that fails validation now reports the
  corrupt-marker reason instead of the EEXIST creation error.
- canonicalizePath's doc comment no longer claims case normalization
  is guaranteed (it's platform/filesystem-dependent).

Added tests for stability across worktrees and orphan-branch checkout.
…oryInstance

Address CodeRabbit review on PR #45:
- A reused git_common_dir (marker file deleted, or re-clone into the
  same path) minting a new instanceId no longer fails on the UNIQUE
  constraint. The stale instance row is superseded: its
  git_common_dir is rewritten to a unique tombstone value so the new
  instance can claim it, while the stale row and its repository_paths
  history are preserved.
- ROLLBACK is now wrapped in try/catch, matching the existing pattern
  in src/state/migrations.ts, so a rollback failure can't mask the
  original transaction error.
- repository_paths gets a partial unique index enforcing at most one
  is_current=1 row per instance (this migration is unreleased, so it's
  edited in place rather than appended as a new version). The
  redundant non-unique index on instance_id is removed since the
  primary key's leading column already serves that lookup.

Added tests for the supersede path and a file-backed store round trip
(directory/file permissions, WAL handling).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/workflow/domain/identity.ts (1)

78-99: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Publish the instance marker atomically.

fs.writeFileSync(..., { flag: "wx" }) provides exclusive creation, but not atomic content publication. A concurrent resolver can read an empty or partial marker, and a crash can leave a corrupt marker that causes later resolutions to fail closed. Write to a temporary file and publish it with exclusive no-overwrite semantics. Add a multi-process first-creation test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/identity.ts` around lines 78 - 99, Update the
instance-marker creation flow around the resolver’s fs.writeFileSync call to
write the generated ID to a unique temporary file, then atomically publish it
with exclusive no-overwrite semantics so readers only observe complete content
and existing markers are never replaced. Preserve the EEXIST race-winner
handling and add a multi-process first-creation test covering concurrent
resolution.
🧹 Nitpick comments (3)
src/workflow/domain/identity.test.ts (2)

14-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace behavior comments with rationale comments.

Lines 14 and 27 describe what each helper does. Keep only Japanese comments that state why the helper exists.

Proposed change
-/** 一時リポジトリを作り、テスト終了時に自動削除する。 */
+// テスト間でリポジトリ状態が残らないようにする。
 function initRepo(t: TestContext): string {
 
-/** 一時ディレクトリを作り、テスト終了時に自動削除する(git init しない)。 */
+// 非Gitディレクトリが必要な失敗系を他のテストから隔離する。
 function tmpDir(t: TestContext, prefix: string): string {

As per coding guidelines, “コメントは日本語で why のみ記述し、what の説明コメントは書かない”.

Also applies to: 27-31

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/identity.test.ts` around lines 14 - 15, Update the
Japanese comments above initRepo and the other helper around lines 27–31 to
explain why each helper is needed rather than describing what it does. Keep the
comments Japanese and rationale-focused, removing behavior-only wording while
preserving the helper implementations.

Source: Coding guidelines


72-77: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use a junction for Windows directory links.

When this test runs on Windows, fs.symlinkSync(..., "dir") can fail with EPERM without symlink privileges. Select "junction" on win32; the current Windows CI job excludes this test, but the repository supports Windows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/identity.test.ts` around lines 72 - 77, Update the
symlink setup in the test resolving through a symlinked path to use `"junction"`
as the link type when process.platform is `"win32"`, while retaining `"dir"` on
other platforms. Keep the existing cleanup and identity assertions unchanged.
src/workflow/domain/identity.ts (1)

42-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rewrite this JSDoc as a why-only comment.

The first sentence describes what canonicalizePath returns. Replace it with the reason canonicalization is required, or remove it.

Proposed wording
- * シンボリックリンク解決・末尾スラッシュ差異を吸収した絶対パスを返す。
+ * リポジトリ移動とシンボリックリンク経由の入力を同一パスとして比較するため、
+ * 実体パスを基準に正規化する。

As per coding guidelines, comments must be written in Japanese and explain why only, not what.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/identity.ts` around lines 42 - 46, Rewrite the JSDoc
above canonicalizePath as a Japanese why-only comment: remove the description of
what the function returns and retain only the reason path canonicalization is
needed, including the relevant filesystem case-sensitivity caveat if still
useful.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/workflow/domain/identity.ts`:
- Around line 78-99: Update the instance-marker creation flow around the
resolver’s fs.writeFileSync call to write the generated ID to a unique temporary
file, then atomically publish it with exclusive no-overwrite semantics so
readers only observe complete content and existing markers are never replaced.
Preserve the EEXIST race-winner handling and add a multi-process first-creation
test covering concurrent resolution.

---

Nitpick comments:
In `@src/workflow/domain/identity.test.ts`:
- Around line 14-15: Update the Japanese comments above initRepo and the other
helper around lines 27–31 to explain why each helper is needed rather than
describing what it does. Keep the comments Japanese and rationale-focused,
removing behavior-only wording while preserving the helper implementations.
- Around line 72-77: Update the symlink setup in the test resolving through a
symlinked path to use `"junction"` as the link type when process.platform is
`"win32"`, while retaining `"dir"` on other platforms. Keep the existing cleanup
and identity assertions unchanged.

In `@src/workflow/domain/identity.ts`:
- Around line 42-46: Rewrite the JSDoc above canonicalizePath as a Japanese
why-only comment: remove the description of what the function returns and retain
only the reason path canonicalization is needed, including the relevant
filesystem case-sensitivity caveat if still useful.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6fe99305-a8a3-48b0-bcc7-c8fbb89e1104

📥 Commits

Reviewing files that changed from the base of the PR and between afcea0c and 0e3ac23.

📒 Files selected for processing (6)
  • docs/workflow-policy.md
  • src/state/migrations.ts
  • src/workflow/domain/identity.test.ts
  • src/workflow/domain/identity.ts
  • src/workflow/state/sqlite-store.test.ts
  • src/workflow/state/sqlite-store.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/state/migrations.ts
  • docs/workflow-policy.md
  • src/workflow/state/sqlite-store.ts

Address CodeRabbit review on PR #45 (Critical):
- resolveOrCreateInstanceId() previously wrote the marker file
  directly with `wx` (exclusive create). Exclusive creation guards
  against overwriting an existing marker, but the write itself is not
  atomic: a concurrent resolver could observe an empty or partial
  file, and a crash mid-write could leave a corrupt marker that later
  resolutions then fail closed on.
- Now writes the generated id to a unique temp file first, then
  publishes it via fs.linkSync (a hard link, which fails with EEXIST
  rather than silently overwriting if the target already exists).
  Other processes only ever observe "marker absent" or "marker
  complete" — never partial content.
- Added identity-resolve-worker.mjs plus a test that spawns 5 real
  concurrent processes racing to create the marker for the same
  repository, asserting they converge on one instance id.

Also addressed nitpicks: rewrote two doc comments as why-only
(canonicalizePath, test helpers) and made the symlink test use a
junction on win32 (plain symlinks can require elevated privileges
there).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/workflow/domain/identity.ts (1)

108-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep failure-path comments rationale-only and correct.

Line 109 describes control flow instead of rationale. Line 117 incorrectly implies that tmpPath is absent after fs.linkSync. The temporary hard-link remains until fs.unlinkSync(tmpPath) succeeds.

Proposed fix
         } catch {
-          // 下の共通エラーへフォールスルー
+          // 競合 winner を検証できない場合に新しい ID を発行しないため、失敗として扱う。
         }
@@
       } catch {
-        // ベストエフォート(既に自分で publish 済みなら該当ファイルは残らない想定)
+        // 公開済み marker の整合性を優先するため、削除失敗で解決を失敗させない。
       }

Also applies to: 114-118

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/identity.ts` around lines 108 - 110, Update the
failure-path comments in the surrounding identity-linking logic to state only
the rationale for falling through to the shared error handling, not the control
flow itself. Correct the comment near fs.linkSync and tmpPath to reflect that
the temporary hard link remains until fs.unlinkSync(tmpPath) succeeds.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/workflow/domain/identity.test.ts`:
- Around line 176-179: Update the child-process completion handling in runWorker
to resolve only after the stdout stream closes, using the close event rather
than exit so all output has been received before trimming and returning the
instance ID; preserve rejection for non-zero exit codes.

---

Nitpick comments:
In `@src/workflow/domain/identity.ts`:
- Around line 108-110: Update the failure-path comments in the surrounding
identity-linking logic to state only the rationale for falling through to the
shared error handling, not the control flow itself. Correct the comment near
fs.linkSync and tmpPath to reflect that the temporary hard link remains until
fs.unlinkSync(tmpPath) succeeds.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d2be75b-a2c1-4075-86f2-9b52de59b444

📥 Commits

Reviewing files that changed from the base of the PR and between 0e3ac23 and d8510bc.

📒 Files selected for processing (3)
  • src/workflow/domain/identity-resolve-worker.mjs
  • src/workflow/domain/identity.test.ts
  • src/workflow/domain/identity.ts

Comment thread src/workflow/domain/identity.test.ts Outdated
Address CodeRabbit review on PR #45:
- runWorker() resolved on the child's "exit" event, which can fire
  before the final stdout "data" event, risking a truncated instance
  id read. Now waits for "close" instead.
- Two failure-path comments in resolveOrCreateInstanceId() described
  control flow ("フォールスルー") or an incorrect claim (that tmpPath
  is already gone after linkSync) rather than rationale; reworded to
  state why, matching the coding guideline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add repository identity model and SQLite workflow-state foundation

1 participant