Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ scans.
| Malformed rule | Reports a scan error, skips that line, and continues the run. |
| Symlink modes | `none` is the default. `internal` follows targets under the workspace root; `all` follows every resolvable target. |
| Target identity | Discovery, dry-run, C# preflight, and content loading share one resolved identity. Stable allowed external targets are indexed; links retargeted after preflight are rejected as source drift. |
| Watch symlink reconciliation | Under `--watch --follow-symlinks internal`, an event for a lexical in-workspace symlink that now resolves outside the workspace remains a reconciliation input. The scoped update removes any stale indexed row without opening or indexing the disallowed target. |
| Explicit-file preflight | Explicit `--files` updates retain each directly supplied token and its normalization provenance, classify that complete list before any database write, and atomically return `UsageError` when a token is invalid, is a directory, or resolves to a duplicate canonical target. A bare `--files` is rejected before artifacts instead of becoming a full scan. `--project` expansion remains derived scope input in `UpdateFiles` and is not reclassified as a direct `--files` selection. Real and dry-run modes expose the same `rejected_paths` entries (`input_index`, support-safe `path`, stable `reason`) and bounded count/truncation metadata for outside-root, symlink-escaping or symlink-policy-disallowed, nonexistent, filtered, unsupported, and duplicate selections. Canonical duplicate checks use each resolved target's filesystem casing rather than the workspace-wide policy, and `none` mode detects actual symlink/reparse segments instead of treating native spelling normalization as a link. Windows 8.3 ancestor aliases canonicalize to the spelling already established in indexed-path membership (#5122). Indexed missing, filtered, or unsupported paths remain valid cleanup targets; indexed control files that become directories, FIFOs, or policy-disallowed symlinks remain tombstones and are classified without opening or following the object. Existing relevant ancestor ignore files remain valid reconciliation controls. In-workspace extractor/configuration inputs, including missing or deleted inputs used as reconciliation signals, also remain valid; generated code remains a valid explicit target. TypeScript path-alias configuration reads carry the active `none` / `internal` / `all` policy into both in-process and worker extraction, resolve only targets permitted by that policy, and secure-open only regular files. Failure to read indexed-path membership fails closed with `E008_DB_ERROR`. This validation does not change the successful zero-file contract for implicit full scans without `--files`. |
| Symlink warnings | Dangling links and directory-target permission failures are scan warnings. Dry-run exposes them through `warnings_total` and `warnings` while retaining successful exit behavior. |
| Windows attributes | Hidden or System paths are rejected before language detection. Clear those attributes on project-owned source because ignore rules cannot re-include the path. |
Expand Down Expand Up @@ -4465,6 +4466,7 @@ literal、`origin: decode_replacement` が不正 byte に対して decoder が
| malformed rule | scan error を報告してその行だけを skip し、run は継続します。 |
| symlink mode | 既定は `none` です。`internal` は workspace root 内の target、`all` は解決可能な全 target を追跡します。 |
| target identity | discovery、dry-run、C# preflight、content loading は同じ解決済み identity を使います。安定した許可済み外部 target は index し、preflight 後に retarget された link は source drift として拒否します。 |
| watch の symlink reconciliation | `--watch --follow-symlinks internal` では、workspace 内にある lexical symlink が workspace 外へ解決されるようになっても、その event を reconciliation input として保持します。scoped update は禁止された target を open / index せず、残っている stale indexed row を削除します。 |
| 明示 file の preflight | 明示的な `--files` 更新では、直接指定された各 token と正規化 provenance を保持し、DB へ書き込む前にそのリスト全体を分類します。無効な token、directory、または同じ canonical target に解決される重複が 1 件でもあれば、要求全体を atomic に `UsageError` とします。path token のない `--files` は artifact 作成前に拒否し、full scan へ移行しません。`--project` の展開結果は `UpdateFiles` 内の導出済み scope input のままとし、直接指定された `--files` 選択として再分類しません。通常実行と dry-run は、project root 外、symlink escape、選択した symlink policy で許可されない path、存在しない path、filter 対象、未対応、重複について、同じ `rejected_paths` entry(`input_index`、support-safe な `path`、安定した `reason`)と上限・件数・truncation metadata を返します。canonical duplicate 判定には workspace 全体の policy ではなく解決済み target ごとの filesystem casing を使い、`none` mode は native spelling の正規化差を link とみなさず、実在する symlink/reparse segment を検出します。Windows の 8.3 ancestor alias は indexed-path membership に確立済みの spelling へ canonicalize します (#5122)。既に index 済みで現在は missing / filtered / unsupported となった path は cleanup 対象として有効なままであり、indexed control file が directory、FIFO、または policy で禁止された symlink に変わっても tombstone として保持し、その object を open / follow せず分類します。既存の関連 ancestor ignore file は reconciliation control として許可します。workspace 内の extractor/configuration input も、reconciliation signal として使う missing / deleted input を含めて許可し、generated code も正常な明示対象として扱います。TypeScript path alias の configuration read は in-process / worker extraction の両方へ active な `none` / `internal` / `all` policy を渡し、その policy で許可された target だけを解決して regular file のみ secure-open します。indexed path membership を読めない場合は `E008_DB_ERROR` で fail closed します。この検証は、`--files` のない暗黙の full scan が 0 件でも成功する契約を変更しません。 |
| symlink warning | dangling link と directory target の permission failure は scan warning です。dry-run は `warnings_total` / `warnings` に出し、成功終了を維持します。 |
| Windows 属性 | Hidden / System path は言語検出前に拒否します。ignore rule では再包含できないため、project 所有 source では先に属性を外してください。 |
Expand Down
4 changes: 4 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,8 @@ Candidate-ordered parallel-index recovery tests must prove that the fatal result
- The top-level issue-4591 watch cancellation fixtures use the ready callback to cover both an idle loop and a deterministically enqueued active update. They run the synchronous command runner on a dedicated worker and await asynchronous readiness signals so full-suite thread-pool pressure cannot starve startup. A bounded wait must distinguish an early watch exit or fault from a still-running readiness observation timeout, and cleanup must cancel and drain the worker before deleting its temporary project. The FTS/planner fixtures cancel at the existing maintenance hooks; keep the bulk-FTS assertion that synchronization triggers are restored and the marker is recoverable without an active PID owner.
- `IndexWatchRunnerTests.ClassifyWatchPath_ReconcilesInputsAndUsesSharedCdidxMembership_Issue4592`, `IndexCommandRunnerTests.RunStatusCheck_CdidxSidecarIsExcludedFromScanAndWorkspaceMembership_Issue4592`, and the issue-4592 update-mode fixtures
keep scan, `status --check`, and watch on one `.cdidx` membership policy. Ignore/unignore changes and pattern-config add/edit/delete events must reach a debounced full scan, while pattern/plugin inputs and ordinary `.cdidx` sidecars remain outside indexed source rows. Pattern extension edits must purge the old extension and index the new one in the same process. The registry refresh test also removes a loaded workspace plugin and verifies that its collectible context is unloaded while a host-registered fallback survives. Test the classification boundary directly instead of waiting for platform-specific `FileSystemWatcher` delivery. `RunCore_SubprojectObservesAncestorIgnoreFileChanges_Issue4592` is the narrower integration exception: it must create the real ancestor `.gitignore` after readiness. On macOS, its .NET 8 target relies on bounded polling delivery for the exact ancestor ignore paths while project-tree delivery remains on FSEvents. The .NET 9 target retains FSEvents in the backend-selection matrix but explicitly forwards the real path through the readiness callback's production enqueue because host EventStream delivery is not deterministic under concurrent target execution. Linux and Windows retain the real watcher-delivery assertion (#4966). Keep the selection matrix and exact-path enumeration assertion so the workaround cannot expand into recursive project polling.
- `IndexWatchRunnerTests.RunCore_InternalSymlinkRetargetedOutsideProjectReconcilesIndexedRow_Issue5120`
retargets an indexed in-workspace file symlink to an external file under the `internal` policy, injects its lexical path through the deterministic startup backend, and verifies that the watch sub-update removes the stale file and symbol rows without indexing the external target. Keep the explicit enqueue and `updated` event assertion; platform watcher timing must not decide this reconciliation contract.
- Issue-5088 watch-internal-path coverage generates every atomic path with `AtomicFileWriter.BuildTempPathForTesting` and covers implicit-default, explicit in-project, and explicit out-of-project databases with the same exact target set. Preserve DB/WAL/SHM/journal/lock/lock-info assertions, normalized and case-sensitive/case-insensitive path comparisons, and nearby user `.tmp` / `.cdidx-*` negatives. `RunCore_AtomicLockInfoReplacementEvents_DoNotScheduleIndexCycle_Issue5088` injects both temp-source and lock-info-destination events through deterministic fake FSEvents and polling backends during the startup generation; it must emit no `updated` or `rescanned` cycle, and must not add fixed sleeps or handwritten fake temp names.
- `IndexWatchRunnerTests.RunCore_StartupHandoff_ReconcilesMutationAndDrainsBeforeReady_Issue4594`
mutates an already indexed symbol through the post-subscription startup hook, explicitly enqueues that event, and verifies the reconciled database content from the readiness callback. The transcript must place the startup `rescanned` event before `watching`; when the queued path still requires a startup `updated` event, that event must remain between them. The baseline rescan may already observe the mutation under load, in which case omitting the redundant `updated` event is equivalent. Keep the injected enqueue and readiness-state assertion: relying only on platform watcher timing or post-shutdown database state would make the handoff regression nondeterministic.
Expand Down Expand Up @@ -1590,6 +1592,8 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
- issue-4591 の top-level watch cancellation fixture は ready callback を使い、idle loop と決定的に enqueue した active update の両方を検証する。同期 command runner は専用 worker で実行し、非同期 readiness signal を await することで、full-suite の thread-pool 負荷が startup を枯渇させないようにする。bounded wait は watch の早期終了または fault と、watch が実行中のまま readiness を観測できない timeout を区別し、cleanup は一時 project を削除する前に worker を cancel して drain しなければならない。FTS / planner fixture は既存の maintenance hook で cancel する。bulk FTS では同期 trigger が復元され、active PID owner を持たない marker から recovery できることの assertion を維持する。
- `IndexWatchRunnerTests.ClassifyWatchPath_ReconcilesInputsAndUsesSharedCdidxMembership_Issue4592`、`IndexCommandRunnerTests.RunStatusCheck_CdidxSidecarIsExcludedFromScanAndWorkspaceMembership_Issue4592`、issue-4592 の update-mode fixture
scan、`status --check`、watch が単一の `.cdidx` membership policy を使うことを固定する。ignore / unignore 変更と pattern-config の追加 / 編集 / 削除は debounce 付き full scan に到達し、pattern / plugin 入力と通常の `.cdidx` sidecar は indexed source row から除外されたままでなければならない。pattern extension の編集では同一 process 内で旧 extension を purge して新 extension を index する。registry refresh test は読み込み済み workspace plugin を削除し、host 登録 fallback を維持しながら collectible context が unload されることも検証する。platform 固有の `FileSystemWatcher` 配信を待たず、classification 境界を直接検証する。`RunCore_SubprojectObservesAncestorIgnoreFileChanges_Issue4592` は限定的な integration 例外であり、ready 後に実際の祖先 `.gitignore` を作成する。macOS の .NET 8 target は project tree の FSEvents を維持しつつ、祖先 ignore path のみを対象にした bounded polling の実配信に依存させる。.NET 9 target は backend 選択 matrix で FSEvents を維持するが、host EventStream 配信は target 並行実行時に決定的ではないため、実 path を readiness callback の production enqueue へ明示転送する。Linux と Windows では実際の watcher 配信 assertion を維持すること (#4966)。workaround が project の再帰 polling へ拡大しないよう、選択 matrix と exact-path 列挙 assertion を維持する。
- `IndexWatchRunnerTests.RunCore_InternalSymlinkRetargetedOutsideProjectReconcilesIndexedRow_Issue5120`
`internal` policy で index 済みの workspace 内 file symlink を外部 file へ retarget し、その lexical path を決定的な startup backend から注入して、watch sub-update が外部 target を index せず stale な file / symbol row を削除することを検証する。明示的な enqueue と `updated` event assertion を維持し、この reconciliation 契約を platform watcher の timing に依存させないこと。
- issue-5088 の watch internal-path coverage は、すべての atomic path を `AtomicFileWriter.BuildTempPathForTesting` で生成し、implicit default database、project 内 explicit database、project 外 explicit database を同じ exact target set で検証する。DB/WAL/SHM/journal/lock/lock-info の assertion、正規化済み path と case-sensitive / case-insensitive 比較、および近接する user `.tmp` / `.cdidx-*` の negative assertion を維持する。`RunCore_AtomicLockInfoReplacementEvents_DoNotScheduleIndexCycle_Issue5088` は startup generation 中、決定的な fake FSEvents / polling backend から temp source と lock-info destination の両 event を注入し、`updated` / `rescanned` cycle が発生しないことを固定する。固定 sleep や手書きの fake temp name を追加しないこと。
- `IndexWatchRunnerTests.RunCore_StartupHandoff_ReconcilesMutationAndDrainsBeforeReady_Issue4594`
subscribe 後の startup hook から index 済み symbol を変更して event を明示的に enqueue し、readiness callback から reconciliation 済みの database content を検証する。transcript では startup `rescanned` event が `watching` より前でなければならず、queued path に startup `updated` event が引き続き必要な場合はその間に位置しなければならない。負荷下では baseline rescan が mutation を先に観測でき、その場合は冗長な `updated` event の省略を同等と扱う。platform watcher の timing や shutdown 後の database state だけに依存すると handoff 回帰が非決定的になるため、注入した enqueue と readiness 時点の assertion を維持すること。
Expand Down
19 changes: 19 additions & 0 deletions changelog.d/unreleased/5120.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
category: fixed
issues:
- 5120
affected:
- src/CodeIndex/Cli/IndexWatchRunner.cs
- src/CodeIndex/Cli/IndexWatchRunner.Watchers.cs
- tests/CodeIndex.Tests/IndexWatchRunnerTests.cs
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **Watch mode now removes symlinks retargeted outside the workspace (#5120)** — with `--follow-symlinks internal`, a change event for an indexed in-workspace symlink now reaches reconciliation after its target moves outside the workspace, removing the stale indexed row without following or indexing the disallowed target.

## 日本語

- **watch mode で workspace 外へ retarget された symlink を削除するようになりました (#5120)** — `--follow-symlinks internal` では、index 済みの workspace 内 symlink の target が workspace 外へ移動した場合も change event が reconciliation に到達し、禁止された target を追跡・index せず stale な indexed row を削除します。
25 changes: 21 additions & 4 deletions src/CodeIndex/Cli/IndexWatchRunner.Watchers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ private static WatchPathDisposition ClassifyWatchPath(
string fullPath,
bool ignoreCase,
bool dbPathExplicit,
FileIndexer.SymlinkPolicy symlinkPolicy,
FileIndexer fileIndexer)
{
var invalidation = FileIndexer.ClassifyIndexInputInvalidation(projectRoot, fullPath);
Expand All @@ -448,13 +449,29 @@ private static WatchPathDisposition ClassifyWatchPath(
: WatchPathDisposition.Reconcile;
}

if (ShouldIgnoreWatchInternalPath(projectRoot, resolvedDbPath, fullPath, ignoreCase, dbPathExplicit)
|| fileIndexer.ShouldSkipPath(fullPath))
{
if (ShouldIgnoreWatchInternalPath(projectRoot, resolvedDbPath, fullPath, ignoreCase, dbPathExplicit))
return WatchPathDisposition.Ignore;

var pathFilter = fileIndexer.EvaluatePathFilter(fullPath);
if (!pathFilter.ShouldSkip)
return WatchPathDisposition.Index;

// An in-workspace link can become OutsideProjectRoot only because its resolved
// target escaped the active internal policy. Keep the lexical link path as a
// reconciliation input so the scoped update can delete any stale row without
// opening the disallowed target.
// workspace 内の link が OutsideProjectRoot になるのは、解決済み target が active な
// internal policy の外へ出た場合である。禁止 target を open せず stale row を削除できるよう、
// lexical link path は reconciliation input として保持する。
if (symlinkPolicy == FileIndexer.SymlinkPolicy.Internal
&& pathFilter.FilterKind == FileIndexer.PathFilterKind.OutsideProjectRoot
&& IsSameOrUnderDirectory(projectRoot, normalizedPath, comparison)
&& FileIndexer.IsSymlinkOrReparsePointPath(normalizedPath))
{
return WatchPathDisposition.Reconcile;
}

return WatchPathDisposition.Index;
return WatchPathDisposition.Ignore;
}

private static bool IsSameOrUnderDirectory(string directory, string fullPath, StringComparison comparison)
Expand Down
Loading
Loading