generated from coulomb/repo-seed
Compare commits
2 Commits
c73815a115
...
5e0ffea585
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e0ffea585 | |||
| 56b73dbab0 |
@@ -16,6 +16,8 @@ ORIGIN_PRECEDENCE = {
|
||||
"manual": 5,
|
||||
}
|
||||
|
||||
PATH_SCOPED_NODE_KINDS = {"lockfile"}
|
||||
|
||||
|
||||
def reconcile_discovery_snapshots(
|
||||
previous: dict[str, Any] | None,
|
||||
@@ -159,14 +161,15 @@ def _merge_candidate(
|
||||
|
||||
def _node_conflicts(by_key: dict[str, dict[str, Any]]) -> list[dict[str, object]]:
|
||||
conflicts: list[dict[str, object]] = []
|
||||
seen: dict[tuple[str, str], str] = {}
|
||||
seen: dict[tuple[str, str], list[str]] = {}
|
||||
for key, node in sorted(by_key.items()):
|
||||
kind = str(node.get("kind") or "")
|
||||
labels = _node_identity_labels(node)
|
||||
for label in labels:
|
||||
match_key = (normalize_identity_part(kind), normalize_identity_part(label))
|
||||
other = seen.get(match_key)
|
||||
if other and other != key:
|
||||
for other in seen.get(match_key, []):
|
||||
if other == key or _path_scoped_nodes_are_distinct(by_key.get(other), node):
|
||||
continue
|
||||
conflicts.append(
|
||||
{
|
||||
"type": "possible_duplicate_node",
|
||||
@@ -175,7 +178,7 @@ def _node_conflicts(by_key: dict[str, dict[str, Any]]) -> list[dict[str, object]
|
||||
"match": {"kind": kind, "label": label},
|
||||
}
|
||||
)
|
||||
seen.setdefault(match_key, key)
|
||||
seen.setdefault(match_key, []).append(key)
|
||||
return _unique_conflicts(conflicts)
|
||||
|
||||
|
||||
@@ -282,6 +285,27 @@ def _node_identity_labels(node: dict[str, Any]) -> list[str]:
|
||||
])
|
||||
|
||||
|
||||
def _path_scoped_nodes_are_distinct(left: dict[str, Any] | None, right: dict[str, Any]) -> bool:
|
||||
if not left:
|
||||
return False
|
||||
left_kind = normalize_identity_part(str(left.get("kind") or ""))
|
||||
right_kind = normalize_identity_part(str(right.get("kind") or ""))
|
||||
if left_kind != right_kind or left_kind not in PATH_SCOPED_NODE_KINDS:
|
||||
return False
|
||||
left_paths = _source_anchor_paths(left)
|
||||
right_paths = _source_anchor_paths(right)
|
||||
return bool(left_paths and right_paths and left_paths.isdisjoint(right_paths))
|
||||
|
||||
|
||||
def _source_anchor_paths(candidate: dict[str, Any]) -> set[str]:
|
||||
anchors = candidate.get("source_anchors") if isinstance(candidate.get("source_anchors"), list) else []
|
||||
return {
|
||||
str(anchor.get("path") or "")
|
||||
for anchor in anchors
|
||||
if isinstance(anchor, dict) and anchor.get("path")
|
||||
}
|
||||
|
||||
|
||||
def _candidate_fingerprint(candidate: dict[str, Any]) -> str:
|
||||
ignored = {"provenance"}
|
||||
stable = {
|
||||
|
||||
@@ -80,6 +80,28 @@ def test_reconciliation_dedupes_diffs_and_tombstones_by_scope() -> None:
|
||||
assert any(tombstone["stable_key"].endswith("ancient-api") for tombstone in reconciled["tombstones"])
|
||||
|
||||
|
||||
def test_reconciliation_keeps_distinct_path_scoped_lockfiles_separate() -> None:
|
||||
scope = _scope("lockfiles", "file", "var/checkouts", "replacement")
|
||||
uv_a = "discovery:fixture-repo:lockfile:var-checkouts-a-uv.lock"
|
||||
uv_b = "discovery:fixture-repo:lockfile:var-checkouts-b-uv.lock"
|
||||
|
||||
current = _snapshot(
|
||||
replacement_scopes=[scope],
|
||||
nodes=[
|
||||
_node(uv_a, "Lockfile", "uv.lock", scope["id"], source_path="var/checkouts/a/uv.lock"),
|
||||
_node(uv_b, "Lockfile", "uv.lock", scope["id"], source_path="var/checkouts/b/uv.lock"),
|
||||
],
|
||||
)
|
||||
|
||||
reconciled = reconcile_discovery_snapshots(None, current, retired_at="2026-05-19T00:00:00Z")
|
||||
|
||||
assert reconciled["reconciliation"]["conflicts"] == []
|
||||
assert reconciled["reconciliation"]["diff"]["conflicted"] == []
|
||||
nodes = {node["stable_key"]: node for node in reconciled["candidates"]["nodes"]}
|
||||
assert nodes[uv_a]["review_state"] == "candidate"
|
||||
assert nodes[uv_b]["review_state"] == "candidate"
|
||||
|
||||
|
||||
def test_scan_cli_reconciles_against_previous_snapshot(tmp_path: Path, capsys) -> None:
|
||||
repo = tmp_path / "fixture-repo"
|
||||
repo.mkdir()
|
||||
@@ -231,8 +253,9 @@ def _node(
|
||||
review_state: str = "candidate",
|
||||
aliases: list[str] | None = None,
|
||||
attributes: dict[str, object] | None = None,
|
||||
source_path: str = "README.md",
|
||||
) -> dict[str, object]:
|
||||
anchor = _anchor("file", "README.md")
|
||||
anchor = _anchor("file", source_path)
|
||||
return {
|
||||
"stable_key": stable_key,
|
||||
"kind": kind,
|
||||
|
||||
@@ -4,7 +4,7 @@ type: workplan
|
||||
title: "Baseline Rollout And Conflict Review"
|
||||
domain: railiance
|
||||
repo: railiance-fabric
|
||||
status: ready
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: high
|
||||
@@ -64,7 +64,7 @@ local Fabric registry service was not listening on `127.0.0.1:8765`.
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0012-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "53880e37-6800-47db-8c35-4278eb241da4"
|
||||
```
|
||||
@@ -84,7 +84,7 @@ Acceptance notes:
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0012-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "1b945a9e-6193-4e34-8e3b-9b2d5a6aa828"
|
||||
```
|
||||
@@ -106,7 +106,7 @@ Acceptance notes:
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0012-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "52e18533-df3f-4478-84bd-f8fa688d83e3"
|
||||
```
|
||||
@@ -124,11 +124,34 @@ Acceptance notes:
|
||||
scanner identity, improve repo declarations, or create follow-up workplan.
|
||||
- Do not project accepted graph changes until this review is complete.
|
||||
|
||||
Review result:
|
||||
|
||||
- Latest `repo-scoping` discovery snapshot: `4`, commit
|
||||
`fd7f25866a94897acfdefaafc83a9d8336c1081b`, generated
|
||||
`2026-05-20T21:21:22Z`.
|
||||
- Conflicted candidates are all `Lockfile` nodes with labels `uv.lock` or
|
||||
`package-lock.json` under distinct `var/checkouts/...` paths:
|
||||
`llm-connect-ce2118b9dc59/uv.lock`,
|
||||
`markitect-main-1e0f80026926/package-lock.json`,
|
||||
`ops-bridge-9733411215b8/uv.lock`,
|
||||
`ops-warden-eac790a4872c/uv.lock`,
|
||||
`railiance-cluster-95d336518aae/uv.lock`,
|
||||
`vergabe-teilnahme-336b6f081ec8/package-lock.json`, and
|
||||
`vergabe-teilnahme-336b6f081ec8/uv.lock`.
|
||||
- Classification: duplicate-detection noise for path-scoped lockfile entities.
|
||||
The scanner is treating same kind plus same normalized label as enough to
|
||||
raise `possible_duplicate_node`, even when source paths clearly distinguish
|
||||
separate lockfiles.
|
||||
- Recommended action: refine duplicate detection for path-scoped nodes before
|
||||
broad rollout or acceptance projection. Lockfiles should require matching
|
||||
source anchor/path identity, not label alone, before being treated as possible
|
||||
duplicates.
|
||||
|
||||
### T04 - Acceptance Policy Trial
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0012-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "7ceaa12a-8044-4fa1-82c3-d202a815e494"
|
||||
```
|
||||
@@ -146,11 +169,24 @@ Acceptance notes:
|
||||
conflict/review work.
|
||||
- Record any blocked acceptance reasons.
|
||||
|
||||
Trial result:
|
||||
|
||||
- `llm-connect` discovery snapshot `2` is clean but contains only
|
||||
`candidate` review-state discoveries, so no candidates were projected.
|
||||
- `railiance-fabric` discovery snapshot `3` is clean, has accepted
|
||||
repo-declaration candidates, and was projected into accepted graph snapshot
|
||||
`24` with 49 nodes and 63 edges.
|
||||
- Projection appended a new graph snapshot with commit
|
||||
`discovery:9ad2750965f0100adcee2473b31ede6f7098205c`; it did not mutate the
|
||||
earlier repo-owned graph snapshots.
|
||||
- `registry rescan-status --review-only` still shows only `repo-scoping`
|
||||
snapshot `4` as review-required.
|
||||
|
||||
### T05 - Rollout Readiness Decision
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0012-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "4a73225c-18ce-4ba6-89b9-cdc8c76ca5d9"
|
||||
```
|
||||
@@ -167,6 +203,16 @@ Acceptance notes:
|
||||
- If ready, propose the command and guardrails for all-local-repo baseline
|
||||
ingestion.
|
||||
|
||||
Decision:
|
||||
|
||||
- Broad all-local-repo rollout is deferred until duplicate detection respects
|
||||
path-scoped node identity for lockfile-style evidence.
|
||||
- The operational loop itself is usable: registry service bootstrap, baseline
|
||||
ingest, unchanged skip, review-only status, and safe projection for clean
|
||||
accepted declarations all worked locally.
|
||||
- Follow-up: `RAIL-FAB-WP-0013` owns the path-scoped duplicate identity
|
||||
refinement and should be completed before the next broad rollout attempt.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Are `repo-scoping` conflicts caused by stale cache baselines from before the
|
||||
|
||||
104
workplans/RAIL-FAB-WP-0013-path-scoped-duplicate-identity.md
Normal file
104
workplans/RAIL-FAB-WP-0013-path-scoped-duplicate-identity.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
id: RAIL-FAB-WP-0013
|
||||
type: workplan
|
||||
title: "Path Scoped Duplicate Identity"
|
||||
domain: railiance
|
||||
repo: railiance-fabric
|
||||
status: active
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: high
|
||||
planning_order: 13
|
||||
created: "2026-05-20"
|
||||
updated: "2026-05-20"
|
||||
state_hub_workstream_id: "bf80900b-606c-442c-aa15-d6272d73a8d6"
|
||||
---
|
||||
|
||||
# RAIL-FAB-WP-0013 - Path Scoped Duplicate Identity
|
||||
|
||||
## Goal
|
||||
|
||||
Refine discovery reconciliation so path-scoped nodes, starting with `Lockfile`
|
||||
candidates, are not marked as duplicate entities solely because they share the
|
||||
same display label.
|
||||
|
||||
This unblocks broad local-repo rollout by making rescan review output point to
|
||||
real identity concerns instead of expected repeated files such as `uv.lock` and
|
||||
`package-lock.json` across different checkouts.
|
||||
|
||||
## Background
|
||||
|
||||
`RAIL-FAB-WP-0012` ingested a controlled three-repo baseline into the local
|
||||
Fabric registry. The only review-required repo was `repo-scoping`; all seven
|
||||
conflicted candidates were `Lockfile` nodes with labels `uv.lock` or
|
||||
`package-lock.json` under distinct `var/checkouts/...` paths.
|
||||
|
||||
The current duplicate detector compares node kind plus normalized label/alias.
|
||||
That is useful for semantic declaration nodes, but too broad for file-backed
|
||||
path-scoped evidence.
|
||||
|
||||
## Design Principles
|
||||
|
||||
- Keep semantic duplicate detection intact for declaration-like nodes.
|
||||
- For path-scoped node kinds, require matching source path identity before
|
||||
treating same-label nodes as possible duplicates.
|
||||
- Preserve conservative behavior when source path evidence is missing.
|
||||
- Add a regression test that mirrors the `repo-scoping` false-positive shape.
|
||||
- Rerun the controlled baseline before recommending broad rollout.
|
||||
|
||||
## Tasks
|
||||
|
||||
### T01 - Add Path-Scoped Reconciliation Coverage
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0013-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8d9cb1c2-77f7-45da-b942-a8d68454477a"
|
||||
```
|
||||
|
||||
Add tests proving that same-label `Lockfile` nodes at different source paths do
|
||||
not raise `possible_duplicate_node`, while same-label semantic nodes still do.
|
||||
|
||||
### T02 - Implement Path-Aware Duplicate Detection
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0013-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "7d4d439d-4f7f-4bdd-bc5f-b02524a10684"
|
||||
```
|
||||
|
||||
Update reconciliation duplicate detection so path-scoped node kinds compare
|
||||
source anchors before emitting duplicate conflicts.
|
||||
|
||||
### T03 - Verify Controlled Rescan
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0013-T03
|
||||
status: in_progress
|
||||
priority: high
|
||||
state_hub_task_id: "0d6cad1d-16f8-4ac7-b8d3-b62a2f8e4549"
|
||||
```
|
||||
|
||||
Rerun the three-repo controlled rescan against the local registry and confirm
|
||||
that `repo-scoping` no longer reports the seven lockfile duplicate conflicts.
|
||||
|
||||
### T04 - Rollout Recommendation
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0013-T04
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "4be31147-b060-4b47-9589-8d3d8024879d"
|
||||
```
|
||||
|
||||
Record whether broad all-local-repo baseline ingestion is ready after the
|
||||
identity refinement, or whether another targeted blocker remains.
|
||||
|
||||
## Close Criteria
|
||||
|
||||
- Unit tests cover path-scoped duplicate identity.
|
||||
- Reconciliation keeps semantic duplicate warnings intact.
|
||||
- Controlled rescan no longer flags distinct lockfiles as duplicates.
|
||||
- State Hub and this workplan capture the updated rollout recommendation.
|
||||
Reference in New Issue
Block a user