Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7dc50d0
feat(edit): evaluate frozen constraints read-only
aimasteracc Aug 12, 2026
7d340e9
fix(edit): bind frozen constraint evidence
aimasteracc Aug 12, 2026
3d3c7f8
test(edit): preserve portable snapshot seams
aimasteracc Aug 12, 2026
22afcfa
fix(edit): close frozen constraint review gaps
aimasteracc Aug 12, 2026
bb1df70
fix(index): bind reusable capability scope
aimasteracc Aug 12, 2026
688e4d5
fix(edit): require exact constraint authority
aimasteracc Aug 12, 2026
5c7aad7
test(edit): preserve config capture seams
aimasteracc Aug 12, 2026
4b2a448
test(edit): emulate portable config evidence
aimasteracc Aug 12, 2026
e46914a
test(edit): complete portable oracle seams
aimasteracc Aug 12, 2026
813e1dc
fix(edit): harden read-only constraint authority
aimasteracc Aug 12, 2026
4c2847b
fix(edit): enforce bounded constraint reads
aimasteracc Aug 12, 2026
55f7e8d
fix(edit): bound response materialization
aimasteracc Aug 12, 2026
29fd21b
test(edit): scope read-only errors by platform
aimasteracc Aug 12, 2026
bab41f8
test(edit): isolate facade snapshot contract
aimasteracc Aug 12, 2026
b53c5ea
fix(edit): certify bounded cross-platform constraints
aimasteracc Aug 12, 2026
12c6db7
fix(edit): close portable constraint gaps
aimasteracc Aug 12, 2026
0cfb5d1
fix(edit): normalize portable index errors
aimasteracc Aug 12, 2026
a70fc6b
test(edit): align portable snapshot contracts
aimasteracc Aug 12, 2026
34f6473
fix(edit): normalize corrupt portable indexes
aimasteracc Aug 12, 2026
28f063c
fix(edit): close portable constraint authority
aimasteracc Aug 12, 2026
fe4815f
test(edit): make portable oracles cross-platform
aimasteracc Aug 12, 2026
178b52d
fix(edit): close frozen constraint authority
aimasteracc Aug 13, 2026
4c1d981
test(index): align portable Windows contracts
aimasteracc Aug 13, 2026
bb38b2d
fix(cache): close final constraint gates
aimasteracc Aug 13, 2026
f86af1d
fix(snapshot): close final review gates
aimasteracc Aug 13, 2026
313ea8f
fix(snapshot): close cross-platform review gates
aimasteracc Aug 13, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ TSA ships curated workflows under `.claude/skills/tsa-*/`:

Each skill ships an `allowed-tools` subset + procedure recipe + decision-surface schema, so the agent doesn't have to triage 8 tools on every question.

### 323 CLI flags
### 324 CLI flags

Superset of CodeGraph's CLI surface. Highlights:

Expand Down
2 changes: 1 addition & 1 deletion README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ TSA は `.claude/skills/tsa-*/` 下にキュレーション済みワークフロ

各 skill は `allowed-tools` ツール サブセット + 手順レシピ + 決定面スキーマを同梱し、エージェントは 8 個のツールから毎回選別する必要がありません。

### 323 の CLI フラグ
### 324 の CLI フラグ

CodeGraph の CLI の厳密な上位互換。主なもの:

Expand Down
2 changes: 1 addition & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ TSA 在 `.claude/skills/tsa-*/` 下提供精选工作流:

每个 skill 都带 `allowed-tools` 工具子集 + 操作流程 + 决策面 schema,agent 不必在 8 个工具间反复挑选。

### 323 个 CLI flag
### 324 个 CLI flag

CodeGraph CLI 的严格超集。亮点:

Expand Down
4 changes: 2 additions & 2 deletions benchmarks/codegraph_compare/readme_claim_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
re.compile(r"13 言語は `?pipeline_registered|13 种语言为 `?pipeline_registered"),
re.compile(r"5 言語 gap|5 语言 gap"),
re.compile(r"1 ワークフロー|一个工作流"),
re.compile(r"\b323 CLI flags\b", re.IGNORECASE),
re.compile(r"323 の CLI フラグ|323 个 CLI flag", re.IGNORECASE),
re.compile(r"\b324 CLI flags\b", re.IGNORECASE),
re.compile(r"324 の CLI フラグ|324 个 CLI flag", re.IGNORECASE),
re.compile(r"\b(?:FTS5|BM25)\b"),
re.compile(r"\bE[0-4]\b"),
re.compile(r"\bE2E\b", re.IGNORECASE),
Expand Down
1 change: 1 addition & 0 deletions docs/CODEMAPS/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Categories of CLI surface:
- `--call-graph` — caller/callee graph

### Code Quality
- `--check-constraints [--constraints-read-only]` — evaluate architecture constraints; read-only mode forwards `persist=false` and never updates violation rows
- `--code-patterns` — smell detection
- `--refactor` — concrete refactor recipes
- `--outline` — hierarchical outline (package → class → method, no bodies)
Expand Down
2 changes: 1 addition & 1 deletion docs/api/facade-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Reading the tables:
| --- | --- | --- | --- |
| `ast_diff` | `diff_snapshot_id`, `file_path`, `include_node_bodies`, `language`, `mode`, `new_file`, `new_ref`, `new_source`, `old_file`, `old_ref`, `old_source`, `output_format` | `success`*, `verdict`*, `agent_summary`, `error` + action payload | `--ast-diff` |
| `classify` | `diff_snapshot_id`, `file_path`, `hunk_cap`, `include_ast_nodes`, `language`, `mode`, `new_ref`, `new_source`, `old_ref`, `old_source`, `output_format` | `success`*, `verdict`*, `agent_summary`, `error` + action payload | `--semantic-classify` |
| `constraints` | `output_format`, `path_filter`, `severity_min` | `success`*, `verdict`*, `agent_summary`, `error` + action payload | `--check-constraints` |
| `constraints` | `diff_snapshot_id`, `output_format`, `path_filter`, `persist`, `scope_paths`, `severity_min` | `success`*, `verdict`*, `agent_summary`, `error` + action payload | `--check-constraints` |
| `guard` | `modification_type`*, `symbol`*, `file_path` | `success`*, `verdict`*, `agent_summary`, `error` + action payload | `--modification-guard` |
| `impact` | `agent_summary_only`, `capture_diff_snapshot`, `compact_only`, `include_tests`, `mode`, `output_format`, `pr_url`, `resource_profile`, `scope_mode`, `scope_paths` — `capture_diff_snapshot` is an explicit boolean producer available only to same-process POSIX consumers | `success`*, `verdict`*, `agent_summary`, `error` + action payload | `--change-impact` |
| `pr` | `include_call_graph`, `mode`, `output_format`, `pr_url` | `success`*, `verdict`*, `agent_summary`, `error` + action payload | `--pr-review` |
Expand Down
6 changes: 6 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,3 +490,9 @@ uv run tree-sitter-analyzer large_file.java --query-key methods --filter "public
# 4. Extract only the lines you need
uv run tree-sitter-analyzer large_file.java --partial-read --start-line 100 --end-line 150
```

### Constraint evaluation

`--check-constraints` evaluates the project architecture rules. Add
`--constraints-read-only` to forward `persist=false`, opening an existing cache
read-only and leaving its schema and violation rows unchanged.
4 changes: 2 additions & 2 deletions tests/contracts/test_language_support_inventory_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ def test_translated_readmes_reject_unregistered_quantitative_marketing(
(
"Python 3.10 以上",
"8 MCP ツール",
"### 323 の CLI フラグ",
"### 324 の CLI フラグ",
"22 言語プラグイン",
"13 は `pipeline_registered`",
"3 は `index_admitted`",
Expand All @@ -471,7 +471,7 @@ def test_translated_readmes_reject_unregistered_quantitative_marketing(
(
"需要 Python 3.10+",
"8 个 MCP 工具",
"### 323 个 CLI flag",
"### 324 个 CLI flag",
"22 个语言插件",
"13 个为 `pipeline_registered`",
"3 个为 `index_admitted`",
Expand Down
52 changes: 45 additions & 7 deletions tests/integration/test_diff_snapshot_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,19 +245,16 @@ def test_frozen_scope_inventory_does_not_admit_post_capture_mutation(


@POSIX_SNAPSHOT_TEST
def test_staged_symlink_records_unsupported_source_kind(tmp_path: Path) -> None:
# PR #1252 review thread 3746878582.
def test_staged_symlink_cannot_claim_shared_authoritative_generation(
tmp_path: Path,
) -> None:
root = _repo(tmp_path)
(root / "module.py").symlink_to("old.py")
_git(root, "add", "module.py")

result = snapshots.DiffSnapshotRegistry().create(str(root), "staged", [])

assert result["success"] is True
record = next(
item for item in result["changed_records"] if item["path"] == "module.py"
)
assert (record["new_kind"], record["new_mode"]) == ("symlink", "120000")
assert result == {"success": False, "error_code": "SOURCE_SCOPE_UNSAFE"}


def test_entry_parts_rejects_malformed_git_header() -> None:
Expand Down Expand Up @@ -485,3 +482,44 @@ def test_directory_replacement_captures_deletion_and_untracked_ancestor(
assert sorted(records) == ["pkg", "pkg/nested.py"]
assert records["pkg/nested.py"]["new_available"] is False
assert records["pkg"]["old_available"] is False


@POSIX_SNAPSHOT_TEST
Comment thread
aimasteracc marked this conversation as resolved.
Outdated
def test_staged_snapshot_constraint_config_comes_from_index_plane(
tmp_path: Path,
) -> None:
# PR #1254 review 3765536002: staged constraints are index-plane evidence.
root = _repo(tmp_path)
config = root / "architectural-constraints.yml"
config.write_bytes(b"version: 1\nconstraints: []\n")
_git(root, "add", config.name)
config.write_bytes(b"version: 1\nconstraints: [invalid-worktree]\n")
registry = snapshots.DiffSnapshotRegistry()

created = registry.create(str(root), "staged", [])
consumer, error = registry.acquire(str(created["diff_snapshot_id"]), str(root))

assert error is None
assert consumer is not None
assert consumer.snapshot.constraint_config_path == config.name
assert consumer.snapshot.constraint_config_data == b"version: 1\nconstraints: []\n"
assert consumer.snapshot.staged_config_matches_worktree is False
consumer.release()


@POSIX_SNAPSHOT_TEST
def test_staged_snapshot_records_source_plane_divergence(tmp_path: Path) -> None:
# PR #1254 review 3765536016: live graphs cannot represent dirty staged sources.
root = _repo(tmp_path)
(root / "old.py").write_text("value = 2\n")
_git(root, "add", "old.py")
(root / "old.py").write_text("value = 3\n")
registry = snapshots.DiffSnapshotRegistry()

created = registry.create(str(root), "staged", [])
consumer, error = registry.acquire(str(created["diff_snapshot_id"]), str(root))

assert error is None
assert consumer is not None
assert consumer.snapshot.staged_source_matches_worktree is False
consumer.release()
6 changes: 5 additions & 1 deletion tests/integration/test_diff_snapshot_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,11 @@ def oracle(*args, deadline=None):
monkeypatch.setattr(snapshots.time, "monotonic", lambda: 100.0)
monkeypatch.setattr(snapshots, "oracle_generation", oracle)
consumer, error = registry.acquire(str(created["diff_snapshot_id"]), str(tmp_path))
assert (consumer, error, deadlines) == (None, "DIFF_SNAPSHOT_EXPIRED", [135.0])
assert (consumer, error, deadlines) == (
None,
"DIFF_SNAPSHOT_EXPIRED",
[135.0, 135.0],
)


def test_strict_scope_binds_only_scoped_change_and_valid_scope(monkeypatch):
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/_diff_snapshot_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ def fake_oracle(project_root, mode="diff", *, deadline=None, manifest=None):
return "sg_test", identity

monkeypatch.setattr(snapshots, "oracle_generation", fake_oracle)
monkeypatch.setattr(
snapshots, "shared_source_generation", lambda *_a, **_k: "sg_test"
)
from types import SimpleNamespace

import tree_sitter_analyzer.index_source_snapshot as source_snapshot
from tree_sitter_analyzer.source_oracle import SafePath

monkeypatch.setattr(
source_snapshot,
"capture_current_source_snapshot",
lambda *_a, **_k: SimpleNamespace(
state="exact", generation="sg_test", reason=None
),
)
monkeypatch.setattr(
snapshots,
"safe_workspace_path",
lambda *_a, **_k: SafePath(data=None, metadata=(b"missing",), kind="missing"),
)
monkeypatch.setattr(
snapshots,
"frozen_index_constraint_config",
lambda *_a, **_k: (None, None, ()),
)
monkeypatch.setattr(
snapshots, "frozen_index_sources_match_worktree", lambda *_a, **_k: True
)
monkeypatch.setattr(
snapshots,
"capture_inventory",
Expand Down
122 changes: 122 additions & 0 deletions tests/unit/cli/test_constraint_check_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,24 @@ def test_returns_edge_count_from_db(self, tmp_path):
_, edge_count = _run_and_persist(db, [])
assert edge_count == 1

def test_read_only_evaluation_returns_rows_without_creating_cache_table(
self, tmp_path
):
db = self._db_with_edges(tmp_path)
violation = _v()

with patch(_EVALUATE, return_value=[violation]):
result = _run_and_persist(db, ["c"], persist=False)

conn = sqlite3.connect(str(db))
tables = conn.execute(
"SELECT name FROM sqlite_master "
"WHERE type = 'table' AND name = 'ast_constraint_violations'"
).fetchall()
conn.close()
assert result == ([violation], 1)
assert tables == []

def test_violations_table_cleared_before_insert(self, tmp_path):
db = self._db_with_edges(tmp_path)
# Pre-populate violations table with a stale row
Expand Down Expand Up @@ -586,6 +604,24 @@ def test_passes_path_filter_and_severity(self, tmp_path):
assert called_payload["path_filter"] == "src/*"
assert called_payload["output_format"] == "toon"

def test_read_only_omits_persistence_from_tool_execution(self, tmp_path):
import asyncio

mock_tool = MagicMock()
mock_tool.execute = AsyncMock(return_value={"success": True})

with patch(_CCT_CLS, return_value=mock_tool):
asyncio.run(_run_tool(str(tmp_path), "warn", "", "json", persist=False))

mock_tool.execute.assert_awaited_once_with(
{
"path_filter": "",
"severity_min": "warn",
"output_format": "json",
"persist": False,
}
)


# ---------------------------------------------------------------------------
# _evaluate_with_explicit_file
Expand All @@ -601,13 +637,15 @@ def _call(
severity_min: str = "warn",
path_filter: str = "",
output_format: str = "json",
persist: bool = True,
) -> dict:
return _evaluate_with_explicit_file(
project_root=str(tmp_path),
constraint_file=constraint_file,
severity_min=severity_min,
path_filter=path_filter,
output_format=output_format,
persist=persist,
)

def test_file_not_found_returns_failure(self, tmp_path):
Expand Down Expand Up @@ -652,6 +690,71 @@ def test_with_db_no_violations_returns_safe(self, tmp_path):
assert result["success"] is True
assert result["evaluated_edge_count"] == 5

def test_read_only_missing_edges_fails_closed_with_nonzero_exit(self, tmp_path):
# PR #1254 review 3766246590: an absent edge capability is not SAFE.
yaml_file = tmp_path / "constraints.yml"
yaml_file.write_text("")
db_dir = tmp_path / ".ast-cache"
db_dir.mkdir()
sqlite3.connect(str(db_dir / "index.db")).close()

with patch(_LOAD_EXPLICIT, return_value=[]):
with patch(_APPLY_TOON, side_effect=lambda p, fmt: p):
result = self._call(tmp_path, str(yaml_file), persist=False)

assert (
result["success"],
result["verdict"],
result["error_code"],
result["error"],
result["violations"],
result["rule_count"],
_exit_code_for(result),
) == (
False,
"ERROR",
"CONSTRAINT_INDEX_UNKNOWN",
"no such table: edges",
[],
0,
1,
)

def test_read_only_corrupt_edges_fails_closed_with_nonzero_exit(self, tmp_path):
# PR #1254 review 3766246590: evaluator database failures are not SAFE.
yaml_file = tmp_path / "constraints.yml"
yaml_file.write_text("")
db_dir = tmp_path / ".ast-cache"
db_dir.mkdir()
conn = sqlite3.connect(str(db_dir / "index.db"))
conn.execute("CREATE TABLE edges(kind TEXT)")
conn.execute("INSERT INTO edges VALUES ('calls')")
conn.commit()
conn.close()

with patch(_LOAD_EXPLICIT, return_value=[]):
with patch(_EVALUATE, side_effect=sqlite3.DatabaseError("bad edge row")):
with patch(_APPLY_TOON, side_effect=lambda p, fmt: p):
result = self._call(tmp_path, str(yaml_file), persist=False)

assert (
result["success"],
result["verdict"],
result["error_code"],
result["error"],
result["violations"],
result["rule_count"],
_exit_code_for(result),
) == (
False,
"ERROR",
"CONSTRAINT_INDEX_UNKNOWN",
"bad edge row",
[],
0,
1,
)

def test_constraint_file_path_included_in_result(self, tmp_path):
yaml_file = tmp_path / "constraints.yml"
yaml_file.write_text("")
Expand Down Expand Up @@ -690,6 +793,7 @@ def _ns(**kwargs: object) -> SimpleNamespace:
severity_min=kwargs.get("severity_min", "warn"),
constraint_path_filter=kwargs.get("constraint_path_filter", ""),
constraint_file=kwargs.get("constraint_file", None),
constraints_read_only=kwargs.get("constraints_read_only", False),
)


Expand Down Expand Up @@ -764,3 +868,21 @@ def test_path_filter_passed_to_evaluate_explicit(self, tmp_path):
run_check_constraints(args, str(tmp_path))
called_kwargs = mock_eval.call_args.kwargs
assert called_kwargs["path_filter"] == "src/**"


def test_read_only_option_forwards_persist_false(tmp_path):
args = _ns(constraints_read_only=True)
safe_result = {"success": True, "verdict": "SAFE"}
with patch(_RESOLVE_OFMT, return_value="json"):
with patch(_ASYNCIO_RUN, return_value=safe_result):
with patch(_PRINT_RESULT):
with patch(
"tree_sitter_analyzer.cli.commands.constraint_check_command._run_tool"
) as run_tool:

async def result():
return safe_result

run_tool.return_value = result()
run_check_constraints(args, str(tmp_path))
assert run_tool.call_args.kwargs["persist"] is False
Loading
Loading