Repository navigation
feat(checks): drift ratchet tool, baseline, CI gate + docs (XWING-2210) - #6
Conversation
smith-xyz
left a comment
There was a problem hiding this comment.
Great tool — the alias resolution and the shrink-only baseline are exactly right, and the test-per-kind suite is solid. I test-merged this with #7 locally: clean merge (no conflicts, even on AGENTS.md/ci.yml), ratchet green on the combined tree, and the regenerated baseline is byte-identical to the committed one.
Plan: I'll merge #7 first (it lands the platform symbols this tool's "use instead" column points at), then this rebases trivially.
One substantive ask — the http rule, which encodes a direction we're not actually going (my fault; story in the inline comment) — plus a couple of nits. Everything else in the ruleset lines up with the platform API and the layer rules.
| Rule( | ||
| "http", | ||
| "HttpClient / a service client", | ||
| import_modules=frozenset({"requests", "httpx", "urllib.request"}), |
There was a problem hiding this comment.
This one's on me: XWING-2210's parenthetical "(there is no HTTP client in core)" was me saying I hadn't worked out the REST/HTTP story yet — and it hardened here into "core does no HTTP." That's not the direction.
The ticket's own table says the platform API for requests/httpx is a core provider (SourceProvider, FeedProvider, tool providers), and #7's Layers section is explicit: outbound HTTP lives inside providers (forge, feeds, advisory APIs); inbound is the REST adapter. Concrete case: say we add a VexService that has to fetch data from the VEX feed — that fetch is exactly outbound HTTP in a provider. The pattern working as intended. As written (allowed=()), the ratchet blocks the very thing we want, and the message points at HttpClient, which doesn't exist here either.
Suggested fix — same treatment connect gets:
Rule(
"http",
"a core provider (SourceProvider, FeedProvider, tool provider)",
import_modules=frozenset({"requests", "httpx", "urllib.request"}),
allowed=("*/providers/*",), # outbound HTTP lives in providers; add the REST adapter path when it lands
)That still catches HTTP creeping into services/models/repositories — the actual drift.
| | `os.getenv` / `os.environ` | `Config` / `Context` | | ||
| | `json.dump` / `write_text(json…)` | `ObjectStore` / `ArtifactPublisher` | | ||
| | `datetime.now()` / `datetime.utcnow()` | `Clock` | | ||
| | `requests` / `httpx` / `urllib.request` | none — core does no HTTP (see Layers); it belongs in the SDK/consumer | |
There was a problem hiding this comment.
Same correction here — per the XWING-2210 table this row should read a core provider (SourceProvider, FeedProvider, tool provider) rather than "none".
Heads-up also: once this rebases onto #7, "see Layers" will point at the new Layers section, which says outbound HTTP is in scope inside providers — the row and the section it cites would disagree with each other.
| calls: frozenset[str] = field(default_factory=frozenset) | ||
| import_modules: frozenset[str] = field(default_factory=frozenset) | ||
| env_subscript: bool = False | ||
| json_write: bool = False |
There was a problem hiding this comment.
Tiny nit: env_subscript and json_write are set on the rules but scan_source never reads them — the os.environ[...] subscript and write_text(json.dumps(...)) checks are hardcoded in the loop. Works fine, but the dataclass implies configurability that isn't wired. Either wire them or drop them.
| @@ -0,0 +1,11 @@ | |||
| from traust_core.v1.checks.ratchet import Hit, Result, Rule, compare, counts, scan, scan_source | |||
There was a problem hiding this comment.
Nit: because __init__.py imports from ratchet and the CI job runs python -m traust_core.v1.checks.ratchet, every CI run logs the "found in sys.modules after import of package… prior to execution" RuntimeWarning. A __main__.py or a lazy import in __init__ silences it.
ed4c02b to
e069c79
Compare
|
Sorry for the misunderstanding on how HTTP was meant to work, I believe it should now be updated to more accurately represent the rules.
Related: the misreading also shaped traust-security/traust#61, so that retry/validate code now lives in core as |
Accuracy review: enforcement gaps remain despite passing testsReviewed head: P2 — "Baselines only shrink" is not enforced across PR changes
An isolated CLI smoke demonstrated:
The current implementation catches growth relative to a fixed baseline and reports shrinkable counts; it does not ensure that a PR cannot increase its baseline. Please enforce monotonicity against the base branch, or explicitly document an approved baseline-growth escape hatch instead of claiming automatic shrink-only enforcement. Initial baseline bootstrapping can remain a separate operation. P2 — Ordinary JSON-write and environment-access forms escape detection
These source snippets each returned zero hits: import json
path.write_text(json.dumps(data) + "\n")import json
path.write_text(data=json.dumps(data))import os
settings = os.environ.copy()Controls using positional Description correctionsThe PR body still says core permits no HTTP and that rollout needs a released core. The revised HTTP rule correctly permits providers, and the documented core consumption model uses commit pins rather than requiring a tagged release. Please bring the description into line with the implemented fix and current project guidance. Exercised verification
The HTTP allowance, lazy-import warning fix, and ordinary fixed-baseline operation are working. The enforcement gaps above are not covered by the passing suite. PostgreSQL was not exercised. No source changes were made. Recommendation: changes requested before approval. |
e069c79 to
a9753b8
Compare
|
@chubirka all three findings addressed in
Gates on this head: lint clean, 791 unit, 200 Postgres integration, coverage 93.8%; ratchet green against baseline + reference on the rebased tree (3 hits / 2 files, byte-identical — the table-repository refactor added zero hand-wiring). |
Core-first slice of the drift ratchet: - traust_core.v1.checks.ratchet: AST-based check over six hand-wiring kinds (connect, subprocess, env, json-write, clock, http) with a CLI; a test per kind incl. the exempt-repository and docstring cases (AC#1, AC#3) - .ratchet.json baseline for core's own src — only the sanctioned ProcessRunner/SystemClock wrapper sites (AC#4 for core) - ratchet job in ci.yml enforcing the baseline, failing on new hand-wiring (AC#2 for core) - "Where we're headed" section + hand-wiring -> platform-API table in AGENTS.md (AC#5 for core) Cross-repo rollout (baselines + CI in traust/engine/ledger) is the follow-up, gated on a traust-core release. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kyl-Bempah <24903083+Kyl-Bempah@users.noreply.github.com>
…ose the AST gaps - --reference: the PR baseline may not grow past the base branch's, so a PR cannot raise its own baseline to cover new hand-wiring; CI passes main's baseline as the reference (bootstrap-safe when absent) - json-write now catches keyword arguments and concatenation around json.dumps; env now catches os.environ.copy() - the ratchet's own baseline writer is exempted (the sanctioned writer) - module docstring states the boundary: a finite AST checker over common forms, not a prohibition; novel forms are caught by review
…g in AGENTS.md; drop narrating comment
a9753b8 to
78fde32
Compare
|
/lgtm |
Core-first slice of the drift ratchet (XWING-2210): a tool that stops new hand-wiring from creeping in, while leaving existing code alone. "New code follows the platform pattern, and CI stops new drift."
What this adds
traust_core.v1.checks.ratchet— an AST-based checker (comments/docstrings never count) over six hand-wiring kinds, each mapped to the platform API to use instead:sqlite3.connect/psycopg.connectSqlUnitOfWork/SqlRepositorysubprocessProcessRunner/ aToolProvideros.getenv/os.environConfig/Contextjson.dump/write_text(json…)ObjectStore/ArtifactPublisherdatetime.now()/utcnow()Clockrequests/httpx/urllib.requestSourceProvider,FeedProvider, tool provider)CLI:
python -m traust_core.v1.checks.ratchet <roots> --baseline .ratchet.json [--reference <base>]. A new hit fails withpath:line kind → use <API>; a dropped count fails with "baseline can shrink" so baselines only go down;--referencefails any baseline growth past the base branch's, so a PR cannot raise its own baseline to cover new hand-wiring..ratchet.json— baseline for core's ownsrc. Only the sanctioned wrapper sites (providers/process.pysubprocess+env,providers/clock.pyclock); core is otherwise clean.ratchetjob inci.yml— enforces the baseline on every PR against the base branch's baseline as the reference.AGENTS.md — the table above + "new code uses core; touch old code only when cheap."
Scope note
A finite AST checker over the common hand-wiring forms — direct calls, keyword arguments, simple concatenation — not a prohibition on all environment access or JSON persistence (the module docstring says so). Novel forms are caught by review and added here when they appear.
Acceptance criteria (this PR = the traust-core slice)
Follow-up Work
Consumer rollout lives in the progress-tracker plans Fence traust-engine and Fence traust: a consumer rule set (
traust_contracts/traust_core/sqlalchemy/psycopgimports, engine-internal imports, state-file writes; the answer is always the platform API viatraust-client), run from CI only (uvx --from traust-core@<sha>), never a runtime dependency. traust-ledger becomes a pure library and gets a "no I/O" rule set or none.Rebased onto
development; the CI reference is now the PR's base branch.Tests
tests/v1/test_ratchet.py— 16 tests (per kind + exempt-repo + docstring + new-hit/shrink/matching-baseline + keyword/concatenated JSON writes +os.environ.copy+ baseline-growth enforcement). Full suite 791 passed; ratchet runs clean against its baseline and the reference;ruffclean.