You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Context: Baton Phase 1 (#304). The coherence engine's contract-shape extractor (experiment-baton/src/codescan.rs) is calibrated to the one dogfood oracle, Sentinel, whose stack is a Go backend ⇄ TypeScript frontend. StrayMark targets a broader audience; this issue records the language boundary explicitly, the one real design smell, the generalization seam, and the trigger to act — so a second adopter on a different stack doesn't hit the assumption without warning.
What is already language-agnostic (keep it)
The ContractId = normalized endpoint. The engine's whole premise — producer and consumer provably share the normalized endpoint (scan::normalize_endpoint) — is neutral to language. A FastAPI handler and a Swift client share /api/v1/services/{id}/health exactly as a Go handler and a TS type do. The conceptual core already generalizes; only the extraction around it is stack-specific.
Declaration parsing — parse_go (struct + json: tags, const (...) enums) and parse_ts (interface, string-union). Each new language needs its own shape extractor.
Role assignment — Go → Producer, * → Consumer (extract_file). This is the load-bearing design smell: it conflates language with architectural role. A Python backend is a producer; a Python client is a consumer. Role is not a property of language — it only looks that way because Sentinel's backend happens to be Go and its frontend TS.
Role ≠ language. The extractor should infer producer vs consumer from signal (a serialization-tagged struct reachable from a route registration = producer; a type consumed at an HTTP call site = consumer), not from which language a file is written in. Baking Go = producer will mislead the first adopter whose backend isn't Go (or whose frontend and backend share a language — Rust/wasm, TS/Node, Python full-stack).
The seam (for when it generalizes — not now)
A LanguageAdapter abstraction — trait or config-driven descriptor — exposing:
Pragmatically, mirror the precedent StrayMark already set: Loom solved exactly this with a config-driven, language-agnostic scanner via an architecture: block in config.yml (#279). Baton's codescan is the direct analog and should follow that pattern — a baton:/contracts: block where the adopter declares producer/consumer source globs + languages — rather than inventing a new mechanism.
Why deferred (deliberate, not an oversight)
Baton is a Phase-1 experiment whose graduation gate was literally "catch one real drift on one oracle (Sentinel)". Building an N-language adapter system before the concept is proven beyond N=1 is premature generalization: a plugin designed from a single example bakes in that example's assumptions anyway (axis 3 is proof). The honest sequence is prove the concept → onboard a second adopter on a different stack → let that real second stack drive the first abstraction, grounded in two cases instead of speculation.
Done when (the trigger is N=2, not a date)
When a second adopter with a non-Go⇄TS stack is on the table:
Context: Baton Phase 1 (#304). The coherence engine's contract-shape extractor (
experiment-baton/src/codescan.rs) is calibrated to the one dogfood oracle, Sentinel, whose stack is a Go backend ⇄ TypeScript frontend. StrayMark targets a broader audience; this issue records the language boundary explicitly, the one real design smell, the generalization seam, and the trigger to act — so a second adopter on a different stack doesn't hit the assumption without warning.What is already language-agnostic (keep it)
The
ContractId= normalized endpoint. The engine's whole premise — producer and consumer provably share the normalized endpoint (scan::normalize_endpoint) — is neutral to language. A FastAPI handler and a Swift client share/api/v1/services/{id}/healthexactly as a Go handler and a TS type do. The conceptual core already generalizes; only the extraction around it is stack-specific.What is hardcoded to Go ⇄ TS today
walk_codewhitelists.go/.ts/.tsxonly.parse_go(struct +json:tags,const (...)enums) andparse_ts(interface, string-union). Each new language needs its own shape extractor.Go → Producer, * → Consumer(extract_file). This is the load-bearing design smell: it conflates language with architectural role. A Python backend is a producer; a Python client is a consumer. Role is not a property of language — it only looks that way because Sentinel's backend happens to be Go and its frontend TS.IDENT<Type>('/route')(added in Baton: per-type → endpoint contract keying for generated type files (unblocks C2/C3) #313) is a typed-TS-client idiom. Python (httpx.get(...) → Pydantic model), Java/Kotlin (@GetMapping), Go-huma(Baton: producer-side route keying for handler-framework Go (huma route-registration ↔ output struct) #319), Rust (reqwest+ serde), C# (HttpClient+ records) each bind type↔route differently.The smell, stated once
Role ≠ language. The extractor should infer producer vs consumer from signal (a serialization-tagged struct reachable from a route registration = producer; a type consumed at an HTTP call site = consumer), not from which language a file is written in. Baking
Go = producerwill mislead the first adopter whose backend isn't Go (or whose frontend and backend share a language — Rust/wasm, TS/Node, Python full-stack).The seam (for when it generalizes — not now)
A
LanguageAdapterabstraction — trait or config-driven descriptor — exposing:extensions()— which files to walk,parse_decls(content)— language-specific shape extraction (fields + enums),scan_bindings(content)— language-specific type↔route call-site shapes,Pragmatically, mirror the precedent StrayMark already set: Loom solved exactly this with a config-driven, language-agnostic scanner via an
architecture:block inconfig.yml(#279). Baton'scodescanis the direct analog and should follow that pattern — abaton:/contracts:block where the adopter declares producer/consumer source globs + languages — rather than inventing a new mechanism.Why deferred (deliberate, not an oversight)
Baton is a Phase-1 experiment whose graduation gate was literally "catch one real drift on one oracle (Sentinel)". Building an N-language adapter system before the concept is proven beyond N=1 is premature generalization: a plugin designed from a single example bakes in that example's assumptions anyway (axis 3 is proof). The honest sequence is prove the concept → onboard a second adopter on a different stack → let that real second stack drive the first abstraction, grounded in two cases instead of speculation.
Done when (the trigger is N=2, not a date)
When a second adopter with a non-
Go⇄TSstack is on the table:LanguageAdapterseam (or theconfig.ymlanalog of architecture generate: language/structure-agnostic scanner — configurable extensions + container dirs (Java/Maven collapses to one 'main' box) #279), decoupling role from language.Until then this stays open as the explicit boundary marker.
Relation
scan_bindings).humaroute keying — the Go adapter'sscan_bindings).