Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0fb0901
fix(devtools): bind affordance reports to selected index
Sinity Aug 6, 2026
1f73cec
fix(devtools): sample only effectively unresolved children
Sinity Aug 6, 2026
d7b6357
fix(maintenance): protect durable tier initialization
Sinity Aug 6, 2026
2b83ce1
fix(maintenance): narrow publication descriptor type
Sinity Aug 6, 2026
eec5ee4
fix(maintenance): allow receipt-free empty rebuilds
Sinity Aug 6, 2026
d1aef97
fix(devtools): bind reports to coherent index snapshots
Sinity Aug 8, 2026
7d114cf
test(reindex): align fixtures with source authority gates
Sinity Aug 8, 2026
9eb08c0
fix(devtools): grade exact selected evidence
Sinity Aug 8, 2026
bdaa53d
fix(reindex): pin evidence and adoption markers
Sinity Aug 8, 2026
851ca3c
test(devtools): type promotion race wrapper
Sinity Aug 8, 2026
e4427b5
fix(storage): require anonymous durable publication
Sinity Aug 8, 2026
a01d6d2
fix: harden archive adoption and pinned read evidence
Sinity Aug 8, 2026
16d27a4
fix: close CodeRabbit maintenance review residuals
Sinity Aug 8, 2026
37c1b05
fix(devtools): preserve index snapshot path alias
Sinity Aug 8, 2026
09f9de0
fix: close exact-head evidence and provenance gaps
Sinity Aug 8, 2026
3bae017
fix(storage): close ArchiveStore read-only mutation gaps
Sinity Aug 8, 2026
e82bde5
fix(devtools): pin affordance evidence to selected index
Sinity Aug 8, 2026
67b005a
fix(rebuild): retire empty-source resume transactions
Sinity Aug 8, 2026
ff548ab
fix(storage): clean up failed durable publication
Sinity Aug 8, 2026
32419da
fix(storage): keep pinned evidence reads non-mutating
Sinity Aug 9, 2026
23d3b8e
fix(maintenance): reject retained archive evidence
Sinity Aug 9, 2026
755fc05
chore: synchronize PR publication state
Sinity Aug 9, 2026
303df98
fix(devtools): bind evidence snapshots to opened indexes
Sinity Aug 9, 2026
c8a054b
fix(maintenance): anchor durable tier publication to its directory
Sinity Aug 9, 2026
f34959e
fix(devtools): bind reports to opened index files
Sinity Aug 9, 2026
0672d9c
fix(maintenance): pin durable publication authority
Sinity Aug 9, 2026
f0df424
fix: close filesystem authority gaps
Sinity Aug 9, 2026
6d42925
test: exercise filesystem authority races
Sinity Aug 9, 2026
1bbc5e4
fix: close snapshot and durable publication gaps
Sinity Aug 9, 2026
cef7894
fix(reindex): preserve uncertain publication targets
Sinity Aug 9, 2026
81d02e4
fix(bootstrap): create archive root before ownership
Sinity Aug 9, 2026
a3576fc
fix: bind daemon ownership locks to canonical path
Sinity Aug 10, 2026
7364e6a
fix: refuse durable tier adoption over maintenance state
Sinity Aug 10, 2026
e3aa74e
fix: require bound archive snapshots for validation
Sinity Aug 10, 2026
db3449c
fix: recheck durable adoption evidence before publish
Sinity Aug 10, 2026
fdff879
fix: harden raw-authority review boundaries
Sinity Aug 10, 2026
43d418d
test: type-check durable publication fault injection
Sinity Aug 10, 2026
069b369
test: align raw-authority receipt coverage
Sinity Aug 10, 2026
a5fea38
fix: revalidate bootstrap archive ownership
Sinity Aug 10, 2026
f1da67d
test: type-check bootstrap ownership fixture
Sinity Aug 10, 2026
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
102 changes: 96 additions & 6 deletions devtools/affordance_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from sqlite3 import Connection
from typing import Any, cast

from devtools.index_snapshot import data_version, open_index_file_set, snapshot_identity, snapshot_index_file_set
from polylogue.config import Config, get_config
from polylogue.insights.affordance_usage import (
DEFAULT_FAMILY_PATTERNS,
Expand All @@ -37,6 +38,14 @@
)
from polylogue.storage.sqlite.connection_profile import open_readonly_connection

_data_version = data_version
_snapshot_identity = snapshot_identity
_snapshot_observation = snapshot_index_file_set

Comment thread
coderabbitai[bot] marked this conversation as resolved.

class _DivergentSelectedIndexError(RuntimeError):
"""The product route opened a physical index other than the selected evidence."""


@dataclass(frozen=True, slots=True)
class AffordanceUsageArgs:
Expand All @@ -48,6 +57,7 @@ class AffordanceUsageArgs:
sample_limit: int
json: bool
all_time: bool
index_db: Path | None = None


def _parser() -> argparse.ArgumentParser:
Expand All @@ -56,6 +66,12 @@ def _parser() -> argparse.ArgumentParser:
description="Analyze agent affordance/tool usage from archive tool-use rows.",
)
parser.add_argument("--archive-root", type=Path, default=None, help="Override the active archive root.")
parser.add_argument(
"--index-db",
type=Path,
default=None,
help="Read a specific candidate/live index database instead of <archive-root>/index.db.",
)
parser.add_argument("--out-dir", type=Path, default=None, help="Write CSV artifacts and report JSON.")
parser.add_argument("--days", type=int, default=7, help="Recent window in days for adoption-sensitive counts.")
parser.add_argument(
Expand Down Expand Up @@ -190,6 +206,9 @@ def _demo_summary(report: dict[str, Any]) -> dict[str, Any]:
"artifact": "agent-affordance-usage",
"updated_at": report["captured_at"],
"archive_root": report["archive_root"],
"evidence_root": report["evidence_root"],
"index_db": report["index_db"],
"snapshot_identity": report["snapshot_identity"],
"index_schema_version": report["index_schema_version"],
"claim": (
"Polylogue can compare agent affordance usage across normalized action evidence "
Expand Down Expand Up @@ -860,12 +879,20 @@ def _try_product_detail_report(
args: AffordanceUsageArgs,
config: Config,
conn: Connection,
opened_main_fd: int,
recent_cutoff_ms: int,
effective_detail_patterns: tuple[str, ...],
) -> dict[str, Any] | None:
if not effective_detail_patterns or args.family:
return None
try:
selected_index_db = config.db_path.resolve(strict=True)
if selected_index_db != (config.archive_root / "index.db").resolve():
# ArchiveStore opens exactly <archive-root>/index.db. Any other
# selected candidate, including a sibling file in the same root, must
# stay on the direct read-only SQLite fallback so its counts and
# snapshot identity cannot describe different databases.
return None
from polylogue.insights.tool_usage import ToolUsageInsightQuery
from polylogue.storage.sqlite.archive_tiers.archive import ArchiveStore
except Exception:
Expand All @@ -883,7 +910,16 @@ def _try_product_detail_report(
since_ms = None if args.all_time else recent_cutoff_ms
action_scope = "product-action-evidence-all-time" if args.all_time else "product-action-evidence-recent-window"
try:
with ArchiveStore.open_existing(config.archive_root) as archive:
with ArchiveStore.open_existing(
config.archive_root,
index_path=selected_index_db,
opened_main_fd=opened_main_fd,
) as archive:
opened_index_db = Path(archive.index_db_path).resolve(strict=True)
if opened_index_db != selected_index_db:
raise _DivergentSelectedIndexError(
"ArchiveStore opened a different physical index than the selected affordance evidence database"
)
merged_rows: dict[tuple[str, str, str, str, str, str], dict[str, object]] = {}
for family, patterns in pattern_groups.items():
rows = archive.list_tool_action_evidence_count_rows(
Expand All @@ -910,6 +946,8 @@ def _try_product_detail_report(
bucket["normalized_tool_name"] = str(
bucket.get("normalized_tool_name") or f"{family}/command-detail"
)
except _DivergentSelectedIndexError:
raise
except Exception:
return None
rows = sorted(
Expand Down Expand Up @@ -1177,13 +1215,38 @@ def _all_time_action_rows(

def build_report(args: AffordanceUsageArgs) -> dict[str, Any]:
config = _config_with_archive_root(get_config(), args.archive_root)
index_db = config.db_path
index_db = (args.index_db or config.db_path).expanduser().resolve()
config = Config(
archive_root=config.archive_root,
render_root=config.render_root,
sources=config.sources,
db_path=index_db,
drive_config=config.drive_config,
index_config=config.index_config,
)
where_sql, where_params = _where_for_filters(args.family, args.detail_pattern, alias="a")
effective_tool_patterns = _clean_patterns(args.family or (() if args.detail_pattern else DEFAULT_FAMILY_PATTERNS))
effective_detail_patterns = _clean_patterns(args.detail_pattern)
recent_cutoff_ms = _recent_cutoff_ms(args.days)
conn = open_readonly_connection(index_db)
opened_index_files = open_index_file_set(index_db)
opened_file_set = opened_index_files.__enter__()
opened_main_fd = opened_file_set.main_fd
conn: Connection | None = None
observer: Connection | None = None
try:
conn = open_readonly_connection(index_db, opened_main_fd=opened_main_fd)
opened_file_set.capture_sidecars(index_db)
observer = open_readonly_connection(index_db, opened_main_fd=opened_main_fd)
assert conn is not None
observer_data_version_before = _data_version(observer)
opened_file_set.capture_sidecars(index_db)
conn.execute("BEGIN")
Comment on lines +1231 to +1243

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use contextlib.ExitStack instead of manual __enter__/__exit__.

Lines 1226 and 1353 drive the context manager by hand. ExitStack expresses the same lifetime, keeps the acquisition inside the guarded region, and removes the need for the conn/observer None sentinels.

Line 1234 assert conn is not None follows the assignment at line 1231 directly, so it can never fail. Remove it.

The same manual pattern exists in devtools/lineage_validation.py lines 796-798 and 966.

🤖 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 `@devtools/affordance_usage.py` around lines 1225 - 1237, Replace the manual
context-manager lifecycle in the affected flow of affordance_usage.py and the
corresponding lineage_validation.py locations with contextlib.ExitStack,
acquiring the index-file context and connections inside the guarded stack so
cleanup is automatic. Remove the conn/observer None sentinels and the redundant
assert conn is not None, while preserving acquisition order and existing cleanup
behavior.

index_schema_version = _user_version(conn)
snapshot_before = _snapshot_observation(
index_db,
opened_main_fd=opened_main_fd,
opened_sidecar_fds=dict(opened_file_set.sidecar_fds),
)
origin_counts = _rows(
conn,
"SELECT origin, COUNT(*) AS sessions FROM sessions GROUP BY origin ORDER BY sessions DESC",
Expand All @@ -1192,6 +1255,7 @@ def build_report(args: AffordanceUsageArgs) -> dict[str, Any]:
args=args,
config=config,
conn=conn,
opened_main_fd=opened_main_fd,
recent_cutoff_ms=recent_cutoff_ms,
effective_detail_patterns=effective_detail_patterns,
)
Expand Down Expand Up @@ -1262,7 +1326,7 @@ def build_report(args: AffordanceUsageArgs) -> dict[str, Any]:
"command": "devtools workspace affordance-usage",
"archive_root": str(config.archive_root),
"index_db": str(index_db),
"index_schema_version": _user_version(conn),
"index_schema_version": index_schema_version,
"patterns": list(args.family or (() if args.detail_pattern else DEFAULT_FAMILY_PATTERNS)),
"detail_patterns": list(args.detail_pattern),
"action_scope": action_scope,
Expand All @@ -1280,8 +1344,29 @@ def build_report(args: AffordanceUsageArgs) -> dict[str, Any]:
surface_summary = _surface_inventory_summary(surface_inventory)
report["surface_inventory"] = surface_inventory
report["surface_inventory_summary"] = surface_summary
opened_file_set.capture_sidecars(index_db)
snapshot_after = _snapshot_observation(
index_db,
opened_main_fd=opened_main_fd,
opened_sidecar_fds=dict(opened_file_set.sidecar_fds),
)
observer_data_version_after = _data_version(observer)
finally:
conn.close()
if observer is not None:
observer.close()
if conn is not None:
conn.close()
opened_index_files.__exit__(None, None, None)
report["archive_root"] = str(config.archive_root)
report["evidence_root"] = str(index_db.parent)
report["index_db"] = str(index_db)
report["snapshot_identity"] = _snapshot_identity(
index_db,
snapshot_before,
snapshot_after,
observer_data_version_before=observer_data_version_before,
observer_data_version_after=observer_data_version_after,
)
if args.out_dir is not None:
out_dir = args.out_dir.expanduser()
out_dir.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -1316,7 +1401,11 @@ def _write_readme(path: Path, report: dict[str, Any]) -> None:
"# Agent Affordance Usage",
"",
f"Generated: {report['captured_at']}",
f"Archive root: `{report['archive_root']}`",
f"Configured archive root: `{report['archive_root']}`",
f"Evidence root: `{report['evidence_root']}`",
f"Evidence index: `{report['index_db']}`",
f"Evidence snapshot SHA-256: `{report['snapshot_identity']['sha256']}`",
f"Evidence snapshot stable: `{str(report['snapshot_identity']['stable']).lower()}`",
f"Index schema: v{report['index_schema_version']}",
f"Action scope: `{report['action_scope']}`",
"",
Expand Down Expand Up @@ -1385,6 +1474,7 @@ def main(argv: list[str] | None = None) -> int:
report = build_report(
AffordanceUsageArgs(
archive_root=parsed.archive_root,
index_db=parsed.index_db,
out_dir=parsed.out_dir,
days=parsed.days,
family=tuple(parsed.family or ()),
Expand Down
Loading