36 KiB
Slice Map — conventions-service-validator
Purpose
The per-project architectural-conventions standard for RoboCo: a service layer (ConventionsService) that builds, caches, renders, scaffolds, and restores the effective conventions map (auto-derived scan overlaid by the committed .roboco/conventions.yml), plus a standalone tree-sitter validator CLI (python -m roboco.conventions) that classifies each changed definition and flags forbidden placements, hygiene violations, custom-rule matches, and modularity smells. DocsService (in scope because it shares the roboco-docs/docs-MCP surface the standard's enforcement relies on) handles documentation file management with RAG dedup, team/path resolution, and repo commit. The standard is gated by ROBOCO_CONVENTIONS_ENABLED and enforced deterministically at i_am_done / pr_pass / claim_review.
Files
| Path | Role | approx LOC |
|---|---|---|
roboco/services/conventions.py |
ConventionsService — cache/render/scaffold/restore the effective conventions map; record + read findings |
448 |
roboco/services/docs.py |
DocsService — write/read/list/delete docs, RAG dedup, commit doc to repo, path-traversal containment |
737 |
roboco/conventions/__init__.py |
Package public API re-exports (run, Finding, check_*, get_parser, GrammarUnavailable, ValidatorCouldNotRun) |
30 |
roboco/conventions/__main__.py |
argparse CLI entrypoint (check --root <dir> --files ...), JSONL output, exit-0/exit-3 fail-loud |
69 |
roboco/conventions/runner.py |
run() orchestrator: per-file dispatch by suffix, waiver filtering, ValidatorCouldNotRun on grammar failure |
107 |
roboco/conventions/findings.py |
Finding frozen dataclass + as_json() (one JSONL line) |
27 |
roboco/conventions/grammars.py |
Lazy cached tree-sitter parser construction per language; GrammarUnavailable |
57 |
roboco/conventions/placement.py |
check_placement — map a file to its module, flag forbidden kinds (no_<kind>s_in_<leaf>) |
70 |
roboco/conventions/classify_python.py |
Python top-level definition classifier → model/route/helper/other |
101 |
roboco/conventions/classify_ts.py |
TypeScript/TSX top-level definition classifier → model/route/component/other |
159 |
roboco/conventions/hygiene.py |
check_hygiene — inline-comment + lint/type-suppression findings; sanctioned-code allowlist |
139 |
roboco/conventions/modularity.py |
check_modularity — cohesion (1 concern/file), thin routes, thin components, god class |
344 |
roboco/conventions/custom.py |
check_custom — project regex rules scoped by language with tsx→typescript dialect relation; unrecognized_rule_languages surfaces typo language tags |
85 |
roboco/conventions/scan.py |
derive_from_scan — infer modules/languages/rules from a repo; render_yaml; CLAUDE.md token lifting |
318 |
Key Symbols
| Name | Kind | File:Line | Responsibility |
|---|---|---|---|
_is_unique_violation |
fn | services/conventions.py:65 |
Narrow IntegrityError to UNIQUE constraint (SQLSTATE 23505); non-unique errors are re-raised, not swallowed |
ConventionsService |
class | services/conventions.py:82 |
Cache/render/scaffold/restore a project's conventions standard |
ConventionsService.get_map |
method | services/conventions.py:85 |
Return effective standard for a project at its HEAD (cache-first; degraded rows not trusted from cache) |
ConventionsService.baseline_constraints |
method | services/conventions.py:111 |
Render block rules + module boundaries as per-task constraints |
ConventionsService.render_ambient_block |
method | services/conventions.py:129 |
Bounded ## Architectural Standard prompt block (2000-char cap) |
ConventionsService.scaffold |
method | services/conventions.py:171 |
Open a PR adding the auto-scaffolded .roboco/conventions.yml |
ConventionsService.restore |
method | services/conventions.py:180 |
Open a PR re-committing the file from the last-good map (or a scan) |
ConventionsService.commit_standard |
method | services/conventions.py:194 |
Open a PR committing an externally-edited standard (panel save) |
ConventionsService.health |
method | services/conventions.py:206 |
Report live file status at HEAD + last-good commit SHA (reads file, not cache) |
ConventionsService.record_findings |
method | services/conventions.py:230 |
Replace a task's recorded findings (delete-then-insert); caller commits |
ConventionsService.recent_findings |
method | services/conventions.py:257 |
Recent findings across the project (panel feed) |
ConventionsService.resolve_workspace |
method | services/conventions.py:300 |
Ensure the project's read clone; None if unavailable (backfill path) |
ConventionsService._resolve |
method | services/conventions.py:316 |
Resolve repo root + HEAD sha; persist clone path + sha back onto project |
ConventionsService._head_sha_at |
method | services/conventions.py:344 |
git rev-parse HEAD in the clone (10s timeout), None if not a repo |
ConventionsService._read_committed_standard |
method | services/conventions.py:362 |
Read/parse .roboco/conventions.yml → (standard, status) |
ConventionsService._publish |
method | services/conventions.py:379 |
Open the conventions scaffold/restore PR via GitService |
ConventionsService._cache_get |
method | services/conventions.py:411 |
Read cached effective map row by (project_id, commit_sha) |
ConventionsService._cache_put |
method | services/conventions.py:422 |
Persist effective map in a savepoint (UNIQUE-violation-only swallow; non-unique IntegrityError re-raised) |
ConventionsService._latest_ok_row / _latest_ok_map |
method | services/conventions.py:472 / :486 |
Most recent status="ok" cached row (degraded fallback source) |
ScaffoldResult |
dataclass | services/conventions.py:47 |
Outcome of a scaffold/restore: pr_number, branch, created |
ConventionsHealth |
dataclass | services/conventions.py:56 |
status, head_sha, last_ok_sha |
get_conventions_service |
fn | services/conventions.py:493 |
Construct ConventionsService(session) |
main |
fn | conventions/__main__.py:57 |
argparse CLI entrypoint; returns exit code |
_run_check |
fn | conventions/__main__.py:42 |
Load file+scan, build effective map, run validator, print JSONL |
_fail |
fn | conventions/__main__.py:37 |
Emit {"error": ...} on stderr + return exit 3 |
run |
fn | conventions/runner.py:42 |
Check a file list under root against a standard; prepends language-scope findings then applies waivers |
_language_scope_findings |
fn | conventions/runner.py:55 |
Emit warn findings for custom rules scoped to unrecognized languages (typo guard, #129) |
_check_file |
fn | conventions/runner.py:78 |
Read + classify + run all check families for one file |
_apply_waivers |
fn | conventions/runner.py:103 |
Drop findings whose (path, rule) is waived |
ValidatorCouldNotRun |
class | conventions/runner.py:34 |
Fail-loud signal (raised on grammar failure) |
Finding |
dataclass | conventions/findings.py:13 |
One violation; as_json() → compact JSONL |
get_parser |
fn | conventions/grammars.py:48 |
Cached tree-sitter parser per language |
GrammarUnavailable |
class | conventions/grammars.py:16 |
Raised when a grammar import fails |
check_placement |
fn | conventions/placement.py:23 |
Placement findings for defs in a file's module |
_module_for |
fn | conventions/placement.py:37 |
Most-specific module whose path is a directory prefix |
classify_definitions (py) |
fn | conventions/classify_python.py:32 |
Top-level Python defs → (name, line, kind) |
classify_definitions (ts) |
fn | conventions/classify_ts.py:36 |
Top-level TS/TSX defs → (name, line, kind) |
check_hygiene |
fn | conventions/hygiene.py:58 |
Inline-comment + suppression findings |
_suppression_allowed |
fn | conventions/hygiene.py:89 |
True iff only sanctioned codes (TC001/2/3, prop-decorator) |
check_modularity |
fn | conventions/modularity.py:70 |
Cohesion + thin routes + thin components + god class |
_check_cohesion |
fn | conventions/modularity.py:96 |
>1 core kind in a file → modular_cohesion finding |
_body_hits_db |
fn | conventions/modularity.py:220 |
Route body calls execute/scalars/select/... (thin_routes) |
_component_fetches |
fn | conventions/modularity.py:300 |
Component body calls fetch/axios (thin_components) |
unrecognized_rule_languages |
fn | conventions/custom.py:37 |
Return language tags on a CustomRule that the validator never reports — flags typos in the conventions file |
check_custom |
fn | conventions/custom.py:56 |
Project regex rule matches scoped by language |
_rule_applies |
fn | conventions/custom.py:45 |
Language scoping with tsx→typescript dialect relation |
derive_from_scan |
fn | conventions/scan.py:100 |
Infer modules/languages/rules/CLAUDE.md-lifted custom rules |
_seed_rules |
fn | conventions/scan.py:142 |
Seed hygiene + placement + modularity rules (stack-scoped) |
_lift_claude_md |
fn | conventions/scan.py:223 |
Lift imperative CLAUDE.md code-spans into warn custom rules |
render_yaml |
fn | conventions/scan.py:304 |
Render a standard to commented YAML (round-trips via parse_yaml) |
DocsService |
class | services/docs.py:154 |
Documentation file management |
DocsService.write_doc |
method | services/docs.py:159 |
Write/update a doc with RAG dedup + repo commit |
DocsService.read_doc |
method | services/docs.py:468 |
Read a doc (role-gated, path-contained) |
DocsService.list_docs |
method | services/docs.py:504 |
List docs (by task or by team filesystem scan) |
DocsService.delete_doc |
method | services/docs.py:543 |
Delete a doc (role-gated, path-contained) |
DocsService._commit_doc_to_repo |
method | services/docs.py:245 |
Best-effort commit the doc onto the task branch via GitService |
DocsService._find_similar_doc |
method | services/docs.py:314 |
RAG content-similarity dedup (≥0.75, same team) |
_resolve_contained_path |
fn | services/docs.py:123 |
Containment-prove base / rel (anti path-traversal, absolute-escape) |
_coerce_doc_ref |
fn | services/docs.py:107 |
Defensive DocRef coercion from str/dict/DocRef |
WriteDocInput |
dataclass | services/docs.py:33 |
Route DTO for write_doc |
get_docs_service |
fn | services/docs.py:735 |
Factory for DocsService(session) |
Data Flow
Validator (gate path). GitService.conventions_check_for_task resolves the task's worktree + changed files and spawns python -m roboco.conventions check --root <worktree> --files .... __main__.main parses args, _load_file reads .roboco/conventions.yml (or None), derive_from_scan(root) auto-derives the default standard, effective_map(scan, file) merges them. runner.run iterates files, dispatching by suffix (.py/.ts/.tsx); _check_file reads bytes, classifies via classify_python/classify_ts (tree-sitter), then runs check_placement + check_hygiene + check_custom + check_modularity. _apply_waivers drops waived findings. Each Finding is printed as one JSONL line on stdout. Exit 0 = ran (findings maybe empty); exit 3 = could not run (fail-loud). The caller parses stdout into findings, treats exit≠0/timeout as could_not_run=True (block gate refuses submit). The choreographer then calls ConventionsService.record_findings to persist them, and block-level findings refuse i_am_done / pr_pass with the offending file:line + fix hint.
Service (render path). On spawn (_base.conventions_ambient_layer) and on task create (TaskService._baseline_constraints_for — the auto-attached ## Constraints section), ConventionsService.get_map is called. _resolve picks the read clone (via resolve_workspace → WorkspaceService.ensure_read_clone) or the legacy workspace_path, rev-parses the HEAD sha, and persists both back onto the project (the backfill). _cache_get returns a cached row if status != "degraded" (degraded rows are re-derived on every call); otherwise _read_committed_standard reads the committed file (status ok/missing/degraded). On degraded, the last-good status="ok" cached map is returned but NOT re-cached (so a repaired file is immediately visible on the next call). effective_map(derive(root), file_standard) builds the effective standard, cached via _cache_put (savepoint-guarded; UNIQUE-only IntegrityError swallowed). render_ambient_block / baseline_constraints render it into the prompt.
Scaffold/restore (panel path). /api/projects/{id}/conventions/* routes call scaffold / restore / commit_standard, which render YAML via render_yaml and open a PR through GitService.open_conventions_pr on the chore/roboco-conventions-scaffold branch. health reports status + last-good SHA; recent_findings feeds the panel.
DocsService. write_doc validates role (WRITE_ROLES = documenter/cell_pm) + doc_type + filename, RAG-searches for a similar existing doc (≥0.75 same team) → update or create under /app/docs/{team}/{subfolder}/{filename}, append a DocRef to task.documents, RAG-index, then best-effort commit the doc into the project workspace clone onto the task branch. read_doc/delete_doc are role-gated and path-contained via _resolve_contained_path. list_docs reads from task.documents (by task) or filesystem scan (by team).
Mermaid
flowchart TD
subgraph Gate["Enforcement gate (i_am_done / pr_pass / claim_review)"]
G["GitService.conventions_check_for_task"]
G -->|"spawn"| CLI["python -m roboco.conventions check"]
end
subgraph Validator["roboco.conventions (pure, tree-sitter)"]
CLI --> Main["__main__.main"]
Main --> Load["_load_file + derive_from_scan"]
Load --> EM["effective_map(scan, file)"]
EM --> Run["runner.run"]
Run --> CF["_check_file per file"]
CF --> ClsPy["classify_python"] & ClsTs["classify_ts"]
CF --> P["check_placement"] & H["check_hygiene"]
CF --> C["check_custom"] & M["check_modularity"]
P & H & C & M --> W["_apply_waivers"]
W -->|"stdout JSONL"| Out["Finding lines"]
W -.->|"grammar fail"| F["ValidatorCouldNotRun -> exit 3"]
end
Out --> Parse["GitService parses findings"]
F -->|"could_not_run=True"| Block["block gate refuses submit"]
Parse -->|"block-level finding"| Block
Parse --> Rec["ConventionsService.record_findings"]
subgraph Render["Render path (spawn + task create)"]
Spawn["agents/factories/_base"] & TC["TaskService.create"]
--> Svc["ConventionsService.get_map"]
Svc --> Resolve["_resolve -> read clone / workspace_path"]
Resolve --> Cache{"cache hit?\n(status != degraded)"}
Cache -->|"yes"| Ret["return cached effective_map"]
Cache -->|"no"| Read["_read_committed_standard"]
Read -->|"degraded"| LG["last-good ok row (returned, not cached)"]
Read -->|"ok/missing"| Build["effective_map(derive, file)"]
Build --> Put["_cache_put (savepoint, UNIQUE-only swallow)"]
Put & Ret & LG --> Amb["render_ambient_block / baseline_constraints"]
end
Logical Tree
conventions-service-validator
├── roboco/services/conventions.py # ConventionsService (cache/render/scaffold/restore/record)
│ ├── ScaffoldResult, ConventionsHealth (frozen dataclasses)
│ ├── _is_unique_violation (module-level helper — narrows IntegrityError to UNIQUE-only)
│ ├── get_map / baseline_constraints / render_ambient_block
│ ├── scaffold / restore / commit_standard / health
│ ├── record_findings / recent_findings
│ └── internals: _resolve, _head_sha_at, _read_committed_standard,
│ _publish, _cache_get/_cache_put (savepoint, UNIQUE-only swallow), _latest_ok_*
├── roboco/services/docs.py # DocsService (docs CRUD + RAG + repo commit)
│ ├── WriteDocInput, TEAM_PATHS, TYPE_SUBFOLDERS, WRITE_ROLES, READ_ROLES
│ ├── _coerce_doc_ref, _resolve_contained_path (anti path-traversal)
│ ├── write_doc -> _find_similar_doc -> _create/_update -> _index_doc_in_rag
│ ├── _commit_doc_to_repo (best-effort git commit; returns "committed"/"skipped"/"failed")
│ └── read_doc / list_docs / delete_doc
└── roboco/conventions/ # validator CLI (pure, tree-sitter)
├── __init__.py # public re-exports
├── __main__.py # argparse CLI, JSONL out, exit-3 fail-loud
├── runner.py # run() orchestrator + language-scope findings + waiver filter
├── findings.py # Finding dataclass + as_json
├── grammars.py # lazy cached tree-sitter parsers
├── placement.py # check_placement (module forbidden kinds)
├── classify_python.py # py defs -> model/route/helper/other
├── classify_ts.py # ts/tsx defs -> model/route/component/other
├── hygiene.py # inline comments + suppression markers + sanctioned codes
├── modularity.py # cohesion + thin_routes + thin_components + god_class
├── custom.py # project regex rules + tsx->typescript dialect + unrecognized_rule_languages
└── scan.py # derive_from_scan + render_yaml + CLAUDE.md lifting
Dependencies
Internal (roboco):
roboco.foundation.policy.conventions.effective_map.effective_map— overlay scan defaults with committed file (pure).roboco.foundation.policy.conventions.models—ConventionsStandard,ConventionsParseError,Module,Rule,RuleLevel,DefinitionKind,CustomRule,Waiver,BUILTIN_RULES.roboco.db.tables—ProjectConventionsCacheTable,ProjectConventionFindingTable,ProjectTable,TaskTable.roboco.services.base.BaseService(+NotFoundError,UnauthorizedError,ValidationError).roboco.services.git—get_git_service,conventions_check_for_task,open_conventions_pr,CONVENTIONS_SCAFFOLD_BRANCH.roboco.services.workspace.get_workspace_service—ensure_read_clone(backfill path).roboco.services.project.get_project_service(docs repo-commit + conventions scaffold-on-create).roboco.services.optimal.get_optimal_service(RAG dedup + indexing).roboco.models.task.DocRef,roboco.models.optimal.{IndexType,QueryContext}.roboco.agents_config.{get_agent_role,get_agent_team}(docs role/team resolution).roboco.seeds.initial_data.AGENT_UUIDS(docs agent slug→UUID).roboco.config.settings.conventions_enabled(gate).
External:
tree_sitter,tree_sitter_python,tree_sitter_typescript(grammars).sqlalchemy(async session,select/delete,IntegrityError,begin_nestedsavepoint).pydantic(model_validate/model_dump).yaml(scan render/parse).argparse,subprocess,asyncio,json,re,os,pathlib.
Entry Points
- CLI:
python -m roboco.conventions check --root <repo> --files a b ...(__main__.main, exit 0 ran / exit 3 could-not-run). Spawned as a subprocess byGitService._run_conventions_validatorper gate evaluation. - Gate verbs (choreographer):
i_am_done(dev pre-submit) andpr_pass(in-path PR gate) callGitService.conventions_check_for_task;claim_review(QA) assembles findings into evidence.block-level findings refuse the transition. Routed via_impl.py:2011/:2059andqa.py:187. - Spawn ambient block:
agents/factories/_base.conventions_ambient_layercallsrender_ambient_blockper project at spawn (when flag on). - Task create:
TaskService._baseline_constraints_for(task.py:1003) callsbaseline_constraintsto auto-attach the## Constraintssection. - Panel HTTP routes:
/api/projects/{id}/conventions(scaffold/save/ restore/health/findings) inapi/routes/project.py:480-541→scaffold/commit_standard/restore/health/recent_findings. - Project create:
ProjectService(project.py:134) +WorkspaceService(workspace.py:946) callscaffoldto seed the file on first project setup. - Docs MCP / HTTP:
/api/docsroutes (api/routes/docs.py) →write_doc/read_doc/list_docs/delete_doc;roboco-docsMCP server wraps these for theroboco-docstool mount (documenter / cell_pm write; board + delivery roles read).
Config Flags
ROBOCO_CONVENTIONS_ENABLED(settings.conventions_enabled) — master toggle. When off:_baseline_constraints_forreturns[],conventions_ambient_layerreturns None, and the whole subsystem is inert. Default-off (per CLAUDE.md, armed in the NAS compose as part of the feature).- (No other
ROBOCO_*flag is read inside this slice. The validator CLI reads no env — the gate's fail-closed/fail-open policy lives inGitService, not here.)
Gotchas
- Savepoint vs bare flush (F042).
_cache_putruns the insert inbegin_nested()and swallowsIntegrityError. A bareadd+flushhere would poison the shared session and crash the rest of task creation (the task-create transaction rides the same session). Two task creates for the same project/HEAD can race to populate the cache — the loser's insert is dropped silently. - Backfill mutates the project row.
_resolvewrites the clone path + rev-parsed sha back ontoproject.workspace_path/project.head_commit— a read path has a write side effect. A non-git legacy path keeps the persisted head_commit / "HEAD" (only a real rev-parse updates the cache key). get_mapdegraded: no caching, re-derived on every call (FIXED536bbb64). Adegradedcache row is not trusted on read (cached.status != "degraded"guard), and degraded results are never written to cache.health()reads the live file, not the cache, so an in-place repair is immediately visible.- Fail-closed vs fail-open in the gate (lives in
GitService, not here). Workspace/diff resolution errors and validator timeout/non-zero exit returncould_not_run=True→ block gate refuses submit. Branchless (nobranch_name) and no-changed-files are fail-open (genuinely nothing to validate). The validator CLI itself mirrors this: exit 3 = could-not-run. - Precision over recall. Classifiers abstain to
otheron anything ambiguous so ablockgate can't false-positive-strand a task.helperis the catch-all kind → helper placement seeds atwarn, notblock. - Sanctioned suppression codes.
no_lint_suppressionsallowsTC001/TC002/TC003/prop-decoratoronly; a barenoqa/type: ignore(no code) is still a finding. TypeScript has no code grammar → never allowed. Comment nodes come from the AST so markers inside strings are not matched. - tsx dialect relation is one-directional. A
typescript-scoped custom rule fires on.tsx; atsx-scoped rule does not fire on plain.ts. The scan reportstypescriptfor.tsx(nottsx), but the validator tags.tsxas languagetsx(JSX grammar). - Ambient block budget.
render_ambient_blocktruncates at a line boundary with a+N morepointer (never mid-line); reserve = header/footer + 80._AMBIENT_CHAR_CAP = 2000;_AMBIENT_TOTAL_CAP(in_base.py) bounds the multi-project concatenation. _resolve_contained_pathrejects absolute paths.DOCS_BASE_PATH / relwith an absoluterelevaluates torel(pathlib resets on absolute right operand) — the old".." in pathguard was bypassable by/etc/passwd. The new guard splits the raw string (notPath.parts, which collapses.on 3.13) and rejects""/./../empty segments, then resolve-and-contain.write_docpath is now containment-checked end-to-end (FIXED536bbb64)._create_new_doccalls_resolve_contained_path(DOCS_BASE_PATH, rel_path)on the built{team}/{subfolder}/{filename}path before writing;_update_existing_doccalls it on the RAG-sourcedexisting_path. The substring guard onfilename(rejects/,\\,..) is still the first-line check inwrite_doc, with containment as a second assertion._commit_doc_to_repois best-effort but now surfaces its outcome (FIXED536bbb64). Returns"committed"/"skipped"/"failed"(instead ofNone);write_docattaches this todoc_ref.commit_status. A git failure still does not abort the doc write to/app/docs, but the agent can see whether the doc landed in the repo or not.- RAG dedup is now filename-guarded (FIXED
536bbb64).write_doconly calls_update_existing_docwhenPath(existing_path).name == req.filename; a different filename forces a new file. A same-filename same-team similar-content match still overwrites (correct intent), but a topically-similar doc with a different name no longer collapses into it. _latest_ok_rowusesderived_at.desc(), notcommit_sharecency — the most recently derived ok row, which is the cache-population order, not necessarily the latest commit.- Validator runs against the worktree working tree (F123).
git conventions_check_for_taskresolves the per-task worktree, not the clone root (which sits on the default branch) — running against the clone root false-passes on newly-added files. - CLAUDE.md token lifting is best-effort + capped.
_lift_claude_mdlifts ≤25 imperative lines with a code-span, only tokens specific enough (separator, uppercase/digit, or ≥12 chars). A bare common word is not lifted.
Drift from CLAUDE.md
- CLAUDE.md says the validator is
python -m roboco.conventions check --root <repo> --files <a> <b> .... Actual (__main__.py:57-65): matches exactly. No drift. - CLAUDE.md says "Precision over recall … a validator that cannot run exits 3 so the gate blocks, never silently passes." Actual (
__main__.py:27,runner.py:29):ValidatorCouldNotRun→_fail→ exit 3. Matches. The gate-level fail-closed/fail-open split (resolution errors block, branchless / no-changed-files pass) lives inGitService.conventions_check_for_task(out of scope) and is consistent with the doc. - CLAUDE.md says
thin_routesdoesn't count an explicitdb.commit(). Actual (modularity.py:40-55):_DB_METHODSexcludescommit/flush/refreshand includesexecute/scalar/scalars/add/add_all/merge/query. Matches. - CLAUDE.md says the allowlist is "ruff
TC001–TC003, pydanticprop-decorator". Actual (hygiene.py:40):_ALLOWED_SUPPRESSION_CODES = {"TC001","TC002","TC003","prop-decorator"}. Matches. - CLAUDE.md says helper placement only warns. Actual (
scan.py:124-125):_SOFT_PLACEMENT_KINDS = {"helper"}→_SOFT_PLACEMENT_DEFAULT = "warn". Matches. - CLAUDE.md says the cache is "per
(project, HEAD sha)inproject_conventions_cache(migration043)". Actual (conventions.py:378-387): keyed by(project_id, commit_sha)onProjectConventionsCacheTable. Matches (migration number not verified in this slice). - CLAUDE.md says
resolve_workspace/ensure_read_cloneis "pinned to the default branch's HEAD". Actual (conventions.py:267-281): delegates toWorkspaceService.ensure_read_clone(project.slug); pinning happens insideWorkspaceService(out of scope) — not contradicted here. - CLAUDE.md says the panel offers "Save / Restore" on the per-project Conventions tab. Actual:
commit_standard(save) +restore(restore) exist and are wired inapi/routes/project.py. Matches. - Drift:
docs.pyread/delete path traversal. CLAUDE.md does not describe the docs path-traversal guard at all; the pre-15effce0code used a bypassable".." in pathcheck. Now fixed via_resolve_contained_path— no doc claim to drift from, but worth noting the historical gap was real. - None. No substantive contradiction between CLAUDE.md and the in-scope code was found.
Changes Since Baseline
Baseline: fd10cc862c2020b3f639cdb686d427b0198a2441. One commit touched this slice: 15effce0 "Chore: 141 Gaps fill-in (#283)".
roboco/conventions/custom.py— Added_DIALECT_OF = {"tsx": "typescript"}+_rule_applies(), replacing the barerule.languages and language not in rule.languagescheck. IMPACT: atypescript-scoped custom rule now fires on.tsxfiles (previously it did not — a React+TS repo with atypescript-scoped rule silently skipped every.tsxfile). One- directional: atsx-scoped rule still does not fire on.ts.roboco/services/conventions.py_cache_put— Wrapped the cache insert inbegin_nested()savepoint +IntegrityErrorswallow (F042). IMPACT: a concurrent duplicate cache insert (two task creates for the same project/HEAD) no longer poisons the shared session and crashes task creation. Previously a bareadd+flushleft the session in error state.roboco/services/docs.py— Replaced theif ".." in pathguard inread_docanddelete_docwith_resolve_contained_path(empty/NUL/./../empty-segment reject + resolve-and-contain). IMPACT: closes a path-traversal escape where an absolutepath(/etc/passwd) bypassed the..-only check becauseDOCS_BASE_PATH / "/etc/passwd"evaluates to/etc/passwd.read_docanddelete_docare now contained;write_doc'sfilenameguard is unchanged (still substring-based, no containment call).
No other logic-touching commits in the range.
Post-snapshot updates (since 2026-06-29):
536bbb64"Chore/all/logical gaps sweep (#286)" touched five files in this slice:
conventions/custom.py(+14 lines): added_KNOWN_LANGUAGESfrozenset andunrecognized_rule_languages()— surfaces typo language tags on custom rules.conventions/runner.py(+28 lines): importedFindingdirectly (not TYPE_CHECKING); added_LANGUAGE_SCOPE_RULE/_LANGUAGE_SCOPE_FILEconstants and_language_scope_findings()helper;run()now prepends language-scope warn findings before per-file dispatch.conventions/modularity.py(+2 lines): added"stream"and"stream_scalars"to_DB_METHODS(SA 2.0 streaming gap, #35).services/conventions.py(+43 lines): added_is_unique_violation()(narrowsIntegrityErrorcatch to SQLSTATE 23505);_cache_putre-raises non-UNIQUE errors;get_mapskips cache onstatus=="degraded"and no longer caches degraded results;health()reads the live file instead of the cache row.services/docs.py(+37 lines):write_docnow only updates the similar RAG-found doc when the filename matches (#35);_commit_doc_to_reporeturns"committed"/"skipped"/"failed"status surfaced ondoc_ref.commit_status(#34);_create_new_docand_update_existing_docboth call_resolve_contained_pathon their resolved path (#33).
Regression Risks
| Title | File:Line | Claim | Severity |
|---|---|---|---|
| Custom-rule tsx dialect broadens firing surface | conventions/custom.py:31-39 |
A typescript-scoped custom rule now fires on .tsx files that it did not before; a React repo with a broad typescript rule (e.g. a CLAUDE.md-lifted re.escape token that happens to appear in .tsx) may newly block tasks that previously passed. |
medium |
| Custom-rule dialect one-directional boundary | conventions/custom.py:38-39 |
_DIALECT_OF only maps tsx→typescript. If a future scan tags other dialects (e.g. jsx, mts), the family lookup silently returns None and a typescript-scoped rule stops firing on them — a quiet under-enforcement with no fail-loud signal. |
low |
services/conventions.py:451-465 |
except IntegrityError swallows any integrity error, not only a duplicate-key.536bbb64: _is_unique_violation now narrows the catch to SQLSTATE 23505 only; any other IntegrityError (FK / NOT NULL / check constraint) is logged at ERROR and re-raised. The savepoint still rolls back only the failed insert, leaving the outer session usable. |
low | |
| Backfill writes project row on a read path | services/conventions.py:303-307 |
_resolve mutates project.workspace_path/head_commit during get_map (a read). If the caller's session is not the one that commits, the backfill is lost; if it is, a read has a write side effect that can race with a concurrent project update. |
low |
degraded |
services/conventions.py:95-108 |
status="degraded".536bbb64: get_map now skips the cache when status=="degraded" (never pins a degraded row); health() re-reads the live file instead of consulting the cache, so an in-place repair is immediately visible. |
low |
services/docs.py:411-454 |
write_doc does NOT call _resolve_contained_path on the built path.536bbb64: _create_new_doc and _update_existing_doc both now call _resolve_contained_path on their resolved path before writing — the built {team}/{subfolder}/{filename} and the RAG-sourced existing_path are both containment-checked. |
medium | |
| _commit_doc_to_repo swallows all exceptions | services/docs.py:295-320 |
Broad except Exception catches all git failures. REDUCED 536bbb64: now returns "committed" / "skipped" / "failed" (instead of None); write_doc attaches the string to doc_ref.commit_status so the caller can surface it. The exception is still caught (the doc write must not fail), but the outcome is no longer invisible. |
low |
| RAG dedup overwrites similar docs | services/docs.py:221 |
_find_similar_doc matches ≥0.75 content similarity in the same team. REDUCED 536bbb64: write_doc now only calls _update_existing_doc when the similar doc's filename exactly matches req.filename — a different filename always creates a new file. Still possible for same-filename, same-team, similar content to collapse silently. |
low |
conventions/modularity.py:45-57 |
await session.stream(...) / stream_scalars(...) were missing from _DB_METHODS — thin-route false negative.536bbb64: "stream" and "stream_scalars" added to _DB_METHODS. Legacy query was already present. Other hypothetical future SA 2.0 constructs not in the set remain a low-severity under-enforcement non-issue. |
low | |
| Grammar import failure is fail-loud per-language | conventions/grammars.py:43-45 |
A missing tree_sitter_typescript import raises GrammarUnavailable → ValidatorCouldNotRun → exit 3 → block gate. Correct, but if the agent image is missing a grammar wheel, every TS task in that container is blocked until the image is fixed — a single missing dep halts all TS delivery. |
low |
Health
Integrity is solid and actively improving. The validator is pure, layered cleanly (scan → effective_map → runner → per-check-family), and obeys its fail-loud contract (exit 3 / ValidatorCouldNotRun) consistently; the service keeps the read path resilient (degraded → last-good uncached, missing → auto-derived, concurrent-cache → savepoint with UNIQUE-only swallow). The 15effce0 + 536bbb64 delta fixed five real defects (concurrent cache-poison F042; bypassable docs path-traversal on read/delete; IntegrityError over-swallow in _cache_put; degraded-row stickiness hiding in-place repairs; stream/stream_scalars thin-route false-negatives) and added a typo-language-scope warn signal and a repo-commit outcome surface on doc_ref.commit_status, with no guard dropped. Remaining risks are low-severity under-enforcement paths (dialect family lookup doesn't cover future jsx/mts, backfill write-side-effect on get_map, same-filename RAG dedup still collapses on content match, broad except Exception in _commit_doc_to_repo still catches) — all consistent with the "precision over recall" stance. The standard is default-off and gated. No blockers.