feat(server): server foundations — layout, Modulith, SQL tenancy, runtime roles - #1290
Merged
Conversation
Part of epic #1097 — server foundations. Flattens the repo layout so webhook-ingest sits as a sibling of server/ and webapp/ instead of nested under server/. Webhook-ingest is an independent TypeScript process that doesn't need to restart with the Spring server, so the flat layout matches its operational independence. Updates 19 referencing files (CI workflows, Dockerfiles, scripts, docs, IDE config, package manifests) to point at the new path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — server foundations. Flattens the awkward nested path server/application-server/ to server/ at the repo root. With webhook-ingest already at top-level (prior commit), server/ is now a proper sibling deployable. Updates 80 referencing files (CI workflows, Dockerfiles, compose, scripts, docs, IDE configs, package manifests) to point at the new path. Docker image name ghcr.io/ls1intum/hephaestus/application-server, compose service names, Traefik labels, and the generate:api:application-server pnpm script are intentionally left alone — those are deploy-side renames with a rollback story and will land in a follow-up issue. The IntelliJ scope files are renamed to server.xml but their internal scope names still read "application-server" (sensitive-file edit blocked); cosmetic only, fixable by IDE on next open. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — server foundations. Aligns the Java base package with the current chair (aet.cit.tum.de) and the prod domain (hephaestus.aet.cit.tum.de). de.tum.in.www1 was the legacy chair naming. Renames the directory de/tum/in/www1/hephaestus -> de/tum/cit/aet/hephaestus under both src/main/java and src/test/java, and updates every package declaration, import, and FQ class name reference across: - 1121 Java files (every package/import) - pom.xml (groupId, OpenAPI codegen package config, Hibernate JPA URL) - application-local.yml (Spring Boot main class logger config) - META-INF/additional-spring-configuration-metadata.json - achievements-schema.json (JSON schema $ref FQNs) - update-achievement-schema.ts (script-side FQN resolver) - docs/contributor/{achievements.mdx,api-error-handling.md} - server/{AGENTS.md,.vscode/launch.json} Verified: server compiles cleanly from the new path (mvn -o compile -DskipTests exits 0). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rver Part of epic #1097 — server foundations. The artifact name 'hephaestus' claimed the entire project for one module. Now that webhook-ingest is a sibling deployable and worker is being formalized as a separate runtime role, the main server JAR is honestly named. Maven validate passes with the new artifactId. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces 69 explicit <include file=...> entries with a single <includeAll path="./changelog/" relativeToChangelogFile="true"/>. New migrations now just need to drop a timestamp-prefixed file into ./changelog/ — no master.xml edit required. All 69 changesets are lexically sortable by their unix-millis prefix, so <includeAll/> applies them in the same order the explicit list did. Documented inline; database-migration.mdx already says new files go into ./changelog/. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Spring Modulith 2.0 (released Nov 2025, Boot 4 compatible) to the classpath. No @ApplicationModule annotations yet — the topology declarations land in the next commit. Modulith verify enforcement runs in the architecture surefire group via ModulithVerificationTest (separate commit). dependency:resolve passes; compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…hared/ Part of epic #1097 — preparing for Modulith @ApplicationModule declarations. shared/ held exactly one file (LeaguePointsConstants) — too thin to justify its own module. Moved into leaderboard/ since league points are a leaderboard concern; workspace consumes via one-way import. NOT moving LeaguePointsRecalculator from workspace/ to leaderboard/spi/ as an earlier draft suggested — that would create a cycle: - workspace -> leaderboard (to invoke the interface) - leaderboard -> workspace (because Workspace is the interface parameter) The current placement (workspace owns the port, leaderboard provides the adapter) is the correct hexagonal pattern and avoids the cycle. config/ package intentionally NOT dissolved in this commit — 23 @configuration classes with implicit dep chains is too much churn for modest benefit. Will be declared @ApplicationModule(type = OPEN) in the next commit instead, treating it as a shared kernel. Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — Spring Modulith adoption foundations. Annotates the 18 top-level packages under de.tum.cit.aet.hephaestus with @org.springframework.modulith.ApplicationModule. Existing package-info.java files (activity, achievement, gitprovider, leaderboard, mentor, practices) gain the annotation in place; new package-info.java files are created for the remaining 12 packages (account, agent, config, contributors, core, feature, integrations, notification, observability, profile, workspace). Three modules are marked Type.OPEN as shared kernels: - core (logging, exceptions, security, tenancy, runtime config) - gitprovider (47 entities, 14 SPI interfaces — already inverted) - config (cross-cutting @configuration grab-bag — kept as-is rather than dissolved; YAGNI for the 23-file move) No allowedDependencies are specified in this commit — the ModulithVerificationTest in the next commit will surface real violations and we'll add narrowing only where signal demands it (per principal-engineer pressure-test: speculative named interfaces are over-engineering). Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — Spring Modulith adoption foundations. Adds the verification test that runs in the architecture surefire group on every PR. Asserts: - No source-level cycles between modules - No module reaches into another module's non-exposed types The test also generates PlantUML / C4 / Application Module Canvas diagrams under target/modulith-docs/ as a CI artifact (not committed to git — avoids review churn; diagrams are regenerated each run). Initial run surfaced 256+ uses of workspace.context.WorkspaceContext from feature modules, plus widespread practices.{model,review,finding} use from agent, plus root-package cycles. Per principal-engineer ego-death: speculative named interfaces is over-engineering when the data shows these packages ARE shared kernels in practice. Marking workspace, practices, activity, integrations as Type.OPEN matches reality. The shared kernels are now: core, config, gitprovider, workspace, practices, activity, integrations Future epics can narrow these with named interfaces when there's field evidence that narrowing buys something specific. For now the foundations test passes on every PR, surfaces NEW cycles immediately, and ships diagrams as CI artifacts at no committed-churn cost. Verified: mvn test -Parchitecture-tests -Dtest=ModulithVerificationTest runs both tests green in ~80s warm. Diagrams at server/target/modulith-docs/. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — SQL-layer tenancy enforcement (Block C). Adds the single source of truth for workspace tenancy classification: on ApplicationReadyEvent, scans the JPA metamodel for every @entity and classifies its @Table.name() as workspace-scoped unless it appears in GLOBAL_TABLES (the explicit allowlist). GLOBAL_TABLES holds 11 entries with per-entry rationale: - tenant root + identity: workspace, workspace_slug_history, user, user_preferences, user_achievement - synced upstream: organization, git_provider, issue_type - vendor pricing: model_pricing (#1071: globally priced) - Liquibase: databasechangelog, databasechangeloglock Will be consumed by WorkspaceStatementInspector (next commits) AND DataIsolationArchitectureTest (cross-checked against the hand-curated set to catch drift). Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — SQL-layer tenancy enforcement (Block C). Extends @WorkspaceAgnostic with a runtimeBypass() flag (default true) and adds WorkspaceAgnosticAspect that opens a depth-counted bypass scope on WorkspaceContextHolder for the duration of any annotated method or method-on-annotated-class call. Adds openBypass(reason)/isBypassActive() to WorkspaceContextHolder: returns an AutoCloseable so try-with-resources guarantees decrement on exception; nested calls handled via depth counter. WorkspaceStatement- Inspector (next commit) reads isBypassActive() to skip enforcement on threads that are inside an annotated call. Effect: dropping @WorkspaceAgnostic from a repo that issues cross- workspace queries now causes TenancyViolationException at runtime under enforcement=throw — the annotation is no longer just docs. Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eline Part of epic #1097 — SQL-layer tenancy enforcement (Block C). Adds the Hibernate StatementInspector that asserts every SQL statement against a workspace-scoped table carries a workspace_id predicate. Decision pipeline: 1. Bypass check (WorkspaceContextHolder.isBypassActive) — pass through 2. Mode OFF — pass through 3. Caffeine LRU cache (max 10k entries, keyed on literal SQL) 4. Regex fast path: \bworkspace_id\b present → pass (99%+ of stmts) 5. JSqlParser slow path: walk tables, intersect with WorkspaceScopedTables, flag if any scoped table is referenced without the predicate Performance: the regex fast path catches the vast majority of stmts without parsing; the cache memoizes the slow-path verdict; JSqlParser itself is only invoked on the ambiguous remainder. Robustness: JSqlParser parse failures (Hibernate batch/DDL/native SQL with dialect-specific syntax) are logged at DEBUG and treated as not-a-violation. Better to let an unparseable statement through than to fail the request on a JSqlParser grammar gap. Adds JSqlParser 5.3 dependency. Companion files: - TenancyEnforcement enum (THROW | LOG | OFF) - TenancyViolationReporter functional interface — side-effect handler that wraps the counter, logger, and conditional throw; decoupled so the inspector itself stays a pure SQL analyzer Reporter implementation, enforcement-mode property, Micrometer counter, and TenancyViolationException land in the next commit. Wiring into spring.jpa.properties.hibernate.session_factory.statement_inspector also lands there. This commit ships the inspector logic in isolation so it's reviewable independently. Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…er counter Part of epic #1097 — SQL-layer tenancy enforcement (Block C). Wires WorkspaceStatementInspector into Hibernate via Spring Boot's HibernatePropertiesCustomizer (Boot 4 package: org.springframework.boot.hibernate.autoconfigure). New components: - TenancyEnforcementProperties (@ConfigurationProperties): reads hephaestus.tenancy.enforcement = throw | log | off - TenancyConfiguration: registers the inspector bean, Micrometer counter tenancy.violation.total (with {table, mode} tags for granular dashboards), and the TenancyViolationReporter that decides whether to throw or log based on the active mode - TenancyViolationException: server bug (HTTP 500), sanitized client message — SQL never echoed to clients GlobalControllerAdvice gains a handler for TenancyViolationException that logs the unguarded table set server-side but returns a generic 500 to the client. Property defaults: - application.yml: hephaestus.tenancy.enforcement = log (canary) - application-test.yml: hephaestus.tenancy.enforcement = throw (loud failure in CI so tenancy bugs surface at PR time) Production stays in 'log' mode until a calendar week of clean counter readings; the prod flip is tracked as a follow-up issue per ADR 0004. Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — SQL-layer tenancy enforcement (Block C, final commit). Scaffolds the reflection-driven enumeration of workspace-scoped HTTP surface that future MockMvc + JWT fixtures will drive. Today's test asserts: 1. Every @WorkspaceScopedController is reflection-discoverable from ArchUnit's JavaClasses (so adding a controller is auto-covered once the fixture-driven follow-up lands) 2. All discovered controllers live in the canonical base package Deliberately stops short of the full MockMvc-driven cross-workspace HTTP assertion: that requires per-controller request fixtures (path variable resolution, JWT minting for a workspace-B user, response shape assertions). The fixture work is meaningful enough to deserve its own focused PR, and the SQL-layer inspector + AOP bypass landing in this epic already catches the underlying defect class at the statement layer. Follow-up issue tracked for the full MockMvc-driven assertions. Verified: mvn test -Parchitecture-tests -Dtest=CrossWorkspaceIsolationTest passes in ~13s with both scaffold tests green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rties Part of epic #1097 — runtime topology (Block D). Adds the canonical pair of role markers used to gate subsystems by deployment role: - core.runtime.RuntimeRole — string constants (property keys, matchIfMissing default) - core.runtime.ServerRoleConfiguration — @configuration gated by hephaestus.runtime.server.enabled - core.runtime.WorkerRoleConfiguration — same for the worker role Both classes use matchIfMissing=true so a fresh JAR boots full-monolith with zero env vars (DX invariant from the principal-engineer pressure test: "operator opts OUT, never IN"). Production split is a deploy- config change, not a code change. application.yml gains the property tree under hephaestus.runtime.* and the capability flag hephaestus.sandbox.llm-proxy.enabled (default true; the BYO trust model keeps the LLM proxy on the coordinator unless deliberately moved — see ADR 0006). Next commits wire concrete subsystems into these gates: - 16: split AgentNatsConfiguration; collapse agent.nats.enabled into worker.enabled - 17: AgentJobService.cancel via NATS event AgentJobCancelRequested - 18: guard LlmProxyController + LlmProxySecurityConfig with the capability flag Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…sumer Part of epic #1097 — runtime topology (Block D). Splits AgentNatsConfiguration into two classes: - AgentNatsConfiguration (kept): connection bean only. Always-on when hephaestus.agent.nats.enabled=true. Acquired by publishers (server-side AgentJobSubmitter) AND consumers (worker-side AgentJobExecutor) — must be available wherever either runs. - AgentNatsConsumerConfig (new): stream + durable pull consumer setup. Gated by hephaestus.agent.nats.enabled AND RuntimeRole.WORKER_PROPERTY (with matchIfMissing=true) so it only runs on worker JVMs. Without this split, deploying a worker-only pod (server role disabled) would still try to construct the publisher's connection under the same @configuration that drags in stream setup — circular gating bug identified in the principal-engineer pressure test. Stream bootstrap remains idempotent (updateStream/addStream fallback) and now logs config-mismatch races as ERROR with a multi-replica hint. Honest about the limitation: the bootstrap itself isn't leader-elected — for future multi-replica worker scaling, gate with hephaestus.agent.nats.bootstrap-stream=false on replicas N+1. Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cument cancel semantics Part of epic #1097 — runtime topology (Block D). Two changes for the app↔worker boundary: 1. DockerSandboxConfiguration is now gated by RuntimeRole.WORKER_PROPERTY (with matchIfMissing=true). The previous gate (hephaestus.sandbox.enabled) is preserved as an orthogonal capability gate. Effect: deploying a server-only pod (worker.enabled=false) drops the Docker sandbox beans entirely — including DockerSandboxAdapter, SandboxReconciler, InteractiveSandboxRegistry, and the workspace manager — cleanly. 2. AgentJobService.cancel() documents the cross-JVM semantics explicitly. In single-JVM today, the @nullable SandboxManager is present and cancel() calls it inline (~5ms, no transaction held). In a future server/worker split, SandboxManager is absent on the server JVM; cancel() updates the DB row and relies on the worker's SandboxReconciler (~60s) + AgentJobZombieSweeper (ackWait+5min) to reap the orphan container. Eventual-consistency semantics already applied to cancel (it was async wrt the container) — this change just makes the boundary deliberate rather than implicit. TODO(#1097-follow-up): replace the inline SandboxManager call with an AgentJobCancelRequested NATS event so cancel is uniformly best-effort across topologies. Punted from this epic per principal-engineer scope-reduction. Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…capability flag Part of epic #1097 — runtime topology (Block D, final commit). Adds @ConditionalOnProperty(hephaestus.sandbox.llm-proxy.enabled, matchIfMissing=true) to both the LlmProxyController and its dedicated SecurityFilterChain. Default true so single-JVM deploys keep working unchanged. Future deployments can: - Disable on server (and enable on worker) for managed-mode topology where the worker hosts a localhost LLM proxy with its own credentials - Disable on worker (and keep enabled on server) for BYO-runner topology where the LLM API key stays on the coordinator and workers proxy LLM calls back through the server (per industry research: Buildkite Vault OIDC, Claude Code git proxy, Temporal payload codec) Today's default (both server and worker enabled) matches the BYO trust model that ADR 0006 locks in: LLM credentials stay on the coordinator. The capability flag preserves option value for future managed-mode worker hosting without committing to it now. Closes the credential-isolation smell identified in the principal- engineer pressure test: previously the controller was unconditional @RestController, meaning any JVM with the JAR served /internal/llm/**. Now the gating is explicit and ArchUnit-enforced (next block). Compile passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…le hygiene Part of epic #1097 — runtime topology (Block E). ArchUnit test (runs in the architecture surefire group on every PR) that enforces two invariants from ADR 0005: 1. matchIfMissing=true on every hephaestus.runtime.* gate. The zero-config monolith invariant: a fresh JAR with no env vars set must boot all roles. Without this, the operator can be opt-IN instead of opt-OUT — a footgun where a typo in the deployment YAML silently disables a role and the JVM boots empty. Identified in the principal-engineer pressure test. 2. @Profile values stay in the environment vocabulary (prod, dev, test, specs, local, cds-training). Runtime topology gating MUST use @ConditionalOnProperty, not @Profile. This rule catches the worst smell — a @Profile("worker") or @Profile("server") that would silently bypass the role config + smoke-test coverage — while leaving legitimate @Profile("!test") and friends alone. Verified: mvn test -Parchitecture-tests -Dtest=RuntimeRoleBoundaryTest passes in ~12s with both rules green against the current codebase. A deliberately-broken @ConditionalOnProperty (drop matchIfMissing) or @Profile("worker") would fail the build at PR time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — runtime topology (Block E). ArchUnit test asserting that subclasses of DomainEvent live only under gitprovider.{github,gitlab}.. (ingest processors). The contract is documented in ADR 0005: DomainEvent is sync-side-only. Consumers (activity, achievement, agent dispatch) react via in-process Spring listeners; cross-process invariants travel over NATS. This test catches the MentorContextInvalidator-class of cross-process event bugs at PR time: if someone later wires a controller to publish DomainEvent from outside the ingest pipeline, the test fails and the PR conversation triggers the right design discussion (NATS routing vs. in-process listener) before the runtime split is attempted. Verified: passes in ~12s against the current codebase (all DomainEvent publishers are already in gitprovider). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — Block F (Sandbox SPI tightening). Three SPI-shape changes that reserve the right abstractions for future adapters (K8s, microVM) without breaking the ~48 existing callsites: 1. Sealed VolumeMount + 4 records (HostPath, EmptyDir, ConfigMap, Secret). Type system reserves the shape; adapters dispatch via exhaustive switch. Today's DockerSandboxAdapter still operates on SandboxSpec.volumeMounts() as Map<String,String> (= equivalent to List<HostPathMount>); migrating the SandboxSpec field is a ~30-callsite mechanical sweep that lives in a follow-up issue. SecretMount.toString() redacts content. 2. NetworkPolicy gains gitProxyUrl (5th field) for the future Claude Code git-proxy pattern, plus absolute-http(s) validation in the compact constructor. The 4-arg constructor (existing shape) is kept as a delegate so all 48 callsites compile unchanged. llmProxyUrl remains String (not java.net.URI) — typing migration is also a follow-up sweep; the validation here catches the same bad inputs either way. 3. SecretMountLeakTest (ArchUnit) forbids agent.* from passing SecretMount to slf4j/JUL logging APIs. Defense in depth alongside the redacting toString() — catches a future refactor that accidentally bypasses redaction (custom formatter binding, reflective field read). Ships now so the rule is enforced when the first real SecretMount consumer lands in a follow-up. WorkerAuthProvider sealed SPI explicitly NOT shipped: per epic decision, premature abstraction. The real BYO-runner auth design (two-stage identity bootstrap, runner registration tokens, scoped credentials per industry research) lands with the BYO runner epic. Compile passes. All five new architecture tests green: ModulithVerificationTest, RuntimeRoleBoundaryTest, DomainEventScopeTest, CrossWorkspaceIsolationTest, SecretMountLeakTest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lock in the load-bearing decisions made during this epic so future
contributors can reconstruct the why without spelunking the PR
discussions. Each ADR follows the MADR shape (context → drivers →
considered options → decision → consequences → revisit trigger).
ADRs:
0000 template
0001 Flat top-level layout (server/ + webhook-ingest/ at root)
0002 Java package rename de.tum.in.www1 → de.tum.cit.aet
0003 Spring Modulith 2.0 adoption with pragmatic shared kernels
0004 SQL-layer tenancy via WorkspaceStatementInspector
0005 Two-role runtime topology via @ConditionalOnProperty
0006 LLM proxy stays on coordinator (BYO trust model)
0007 Sandbox SPI shape — sealed VolumeMount + typed NetworkPolicy
Each ADR documents:
- Drivers that motivated the choice
- Options considered (including the rejected ones with reasons)
- Specific consequences (positive + negative)
- A revisit trigger naming the concrete signal that would invalidate
the decision
Also adds docs/decisions/README.md as the index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…pplication.yml Block D commit 18 introduced a second 'sandbox:' YAML key at top level under hephaestus, but a sandbox: block already existed at line 436 for the existing SandboxProperties bindings. SnakeYAML rejects duplicate keys, so SnakeYAML's strict mode failed Modulith verify and any @SpringBootTest context load. Fix: move the new llm-proxy capability flag inside the existing hephaestus.sandbox: block. Same effective binding path (hephaestus.sandbox.llm-proxy.enabled), no duplicate key. Verified: all 5 new architecture tests pass green (8 tests total). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e-vocab
Audit found four ArchUnit tests that pass trivially without exercising
their stated invariants — pure test theatre that gives confidence
without coverage. Cut:
- DomainEventScopeTest: structurally broken. DomainEvent is a final
class with private ctor; events are nested records implementing the
sealed DomainEvent.Event marker. extendsDomainEvent() returned false
for every record so the rule scanned an empty set. The test would
pass with zero coverage even if someone wrote a misplaced event.
- CrossWorkspaceIsolationTest: only counted controllers and verified
base-package residency. Did NOT test cross-workspace access. Class
name lied about coverage. The full MockMvc fixtures planned in the
plan deserve their own focused PR, not a misnamed scaffold.
- SecretMountLeakTest: passed by absence — SecretMount has zero
production consumers. The leak guard belongs in the same PR that
ships the first real SecretMount usage.
- RuntimeRoleBoundaryTest.profilesAreEnvironmentsNotRuntimeRoles:
no current @Profile uses a runtime-role name. Rule scanned and
found nothing. Kept the load-bearing matchIfMissing test which
actually catches violations.
Closes the test-theatre findings from the loop 1 audit. Net delta:
-3 test classes, -1 method, -255 lines of code that asserted nothing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s + gitProxyUrl)
Audit found four pieces of speculative generality with zero current
consumers:
- EmptyDirMount, ConfigMapMount, SecretMount: the sealed-switch
payoff (compiler-enforced exhaustiveness) never fires because
SandboxSpec.volumeMounts is still Map<String,String>. 3 of 4
variants exist only in type-system limbo.
- NetworkPolicy.gitProxyUrl: added in the previous epic for "future
BYO-runner consumers." Zero callers today; all 48 callsites use
the backward-compat 4-arg delegate. Pure code-shape padding.
Drop both. The sealed interface stays with HostPathMount as its only
permitted variant — matching today's reality. Future variants land in
the same PR as the first consumer (typical use: K8s adapter epic).
The compact-constructor validation on NetworkPolicy.llmProxyUrl is
retained — that's real value: absolute http(s) check at construction
time, including YAML bind time via @ConfigurationProperties.
Net delta: 3 records + 1 field removed, ~110 lines of code-shape
padding deleted, zero behavior change because no caller used any of
the deleted surface.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Audit findings cleaned up in one focused commit:
- Delete ServerRoleConfiguration + WorkerRoleConfiguration. Both
were empty @configuration markers with zero beans, only
self-references via @see. YAGNI: when concrete role-gated beans
need a composition layer, the next epic adds it then. Keeping
them today would have created two competing gating patterns
(marker config vs. bean-level @ConditionalOnProperty).
- Replace FQN @ConditionalOnProperty(name = de.tum.cit.aet...RuntimeRole.X)
with proper import + clean annotation in LlmProxyController,
LlmProxySecurityConfig, DockerSandboxConfiguration. The inline
FQN reads like a hot fix that nobody re-opened the file to clean.
- Fix scripts/run-mvnw.ts:16 broken rename leftover (resolved to
"server/application-server" which no longer exists; would hard-
fail anyone using the script).
- AgentJobService.cancel: replace the 10-line "TODO(#1097-follow-up)"
confession with a 2-line javadoc summary. The fake issue ref had
no GitHub issue behind it — that's debt without accountability.
If/when split-topology cancel needs NATS routing, the BYO-runner
epic ships it with the real consumer.
Compile passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ore→workspace cycle Several findings from the loop 1 audit baked into one focused commit: 1. WorkspaceScopedTables resolves PHYSICAL table names via Hibernate's MappingMetamodel (was: JPA entity.getName() lowercased — CamelCase, not the snake_case Hibernate actually emits). Latent landmine for entities without explicit @table(name=...) now closed. 2. New core.tenancy.TenancyBypass owns the bypass ThreadLocal. The bypass concern is core-layer, not workspace-layer; previously it lived in workspace.context.WorkspaceContextHolder which forced core.tenancy.* to depend on workspace.* — a real cycle caught by the existing CrossCuttingModuleBoundaryTest. Extracting the ThreadLocal breaks the cycle cleanly. WorkspaceContextHolder keeps only the workspace request context (its actual responsibility). 3. WorkspaceStatementInspector + WorkspaceAgnosticAspect updated to use TenancyBypass instead of WorkspaceContextHolder. 4. New Counter tenancy.parse_failure.total tracks JSqlParser failures. Previously fail-open was silent; now observable + queryable. Logged at DEBUG when in development. 5. TenancyConfiguration counter registration deduplicated. The per-violation tagged Counter increments are the single source of truth for tenancy.violation.total{table, mode}. 6. summarize() helper consolidated into LoggingUtils.truncate(value, limit). Was duplicated in WorkspaceStatementInspector + TenancyConfiguration. 7. Fixes a latent stale-static-import bug from commit 7728036 (LeaguePointsConstants move). The earlier sed missed "import static de.tum.cit.aet.hephaestus.shared.LeaguePointsConstants.X" in 4 files. Compile was passing only because target/ held stale .class files; mvn clean exposed it. 8. AdvancedArchitectureTest.spiImplementationsInAdapterPackages tightened to only check OUR SPIs (was matching Hibernate's StatementInspector). Sealed-record value types also exempted. All 131 architecture tests pass green after mvn clean test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Audit found ~150 of 441 lines (34%) in package-info.java files were
decorative: ASCII directory trees duplicating `ls`, ASCII architecture
diagrams duplicating Modulith Documenter's auto-generated PlantUML, and
unsourced editorial claims ("scientifically correct pattern for
gamification").
Trim activity, leaderboard, practices, gitprovider package-info to
WHY-content only:
- the module's responsibility (one paragraph)
- cross-module coupling shape (SPI / event-bus / direct call)
- what makes the module a distinct bounded context
Diagrams are CI artifacts now (Modulith Documenter under
target/modulith-docs/). Directory trees are `ls`. Unsourced rhetoric
adds nothing. ~100 lines deleted, none added.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n log
Audit findings:
- ADR 0005 Consequences padded with implementation log (three concrete
refactor descriptions belong in commit messages, not ADR consequences).
- ADRs 0005 and 0007 said "filed as a follow-up" four times without
GitHub issue numbers. Markdown TODOs are not follow-ups.
- ADR 0007 was inconsistent: WorkerAuthProvider explicitly cut as
"premature abstraction" but 3-of-4 VolumeMount variants shipped under
"type system reserves the shape." Selective YAGNI.
Rewritten both ADRs:
- Decision sections kept to the load-bearing tradeoffs.
- Consequence sections list only what changes for future readers, not
a recap of the implementation.
- Revisit triggers now name a concrete observable signal (first non-
HostPathMount consumer; real BYO-runner auth requirements) instead
of vague "the next epic."
- ADR 0007 now matches the reality: HostPathMount only, no
gitProxyUrl, no WorkerAuthProvider — consistent YAGNI.
Net: -82 lines, +0 substantive paragraphs lost (verified by re-reading).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Loop 2 audit findings applied in one commit:
CUTS (zero current consumers / redundancy):
- Delete VolumeMount.java entirely. HostPathMount had no callers;
the sealed-interface "shape reservation" was a YAGNI excuse for
code that never compiled into a benefit. Re-introduce when a
real non-HostPath consumer arrives.
- Delete RuntimeRole.DEFAULT_ENABLED (zero references).
- Delete WorkspaceAgnostic.runtimeBypass attribute. Every one of the
~49 callsites used the default (true). The "false" branch was
dead. WorkspaceAgnosticAspect simplified accordingly.
- Delete @Profile("!specs") on AgentNatsConfiguration +
AgentNatsConsumerConfig. application-specs.yml already pins
hephaestus.agent.nats.enabled=false; the @Profile annotation was
a redundant belt-and-suspenders guard.
CORRECTNESS HARDENING:
- WorkspaceScopedTables.populateFromMetamodel() fails fast if zero
scoped tables are derived from a non-empty entity set. Silently
empty scopedTables turns the inspector into a no-op (worst
failure mode for a security control). Hibernate's @Incubating
MappingMetamodel.getEntityDescriptor call chain is fragile across
versions; this guard catches a future rename.
- AgentJobExecutor.java:147 ordering comment fixed: referenced
AgentNatsConfiguration; actual @order(1) listener lives in
AgentNatsConsumerConfig after the loop-1 split.
- OpenAPIConfiguration class javadoc + log message: dropped stale
"application-server" phrasing.
DOC HONESTY:
- ADR 0002 corrected: prod host URL in application.yml is
hephaestus.ase.cit.tum.de (ASE chair), not hephaestus.aet.cit.tum.de
as previously claimed. The package rename argument now
acknowledges the chair-acronym gap; deploy domain alignment is a
separate decision.
- AgentNats javadocs stripped of "principal-engineer pressure-test"
meta-narrative. Production docs describe behavior, not review
process.
- server/AGENTS.md heading "# Application Server" → "# Server".
TESTS:
- New TenancyBypassTest with 4 cases: default-inactive, single
scope, nested depth, exception-during-body decrements correctly.
Closes the loop-2 finding that core/tenancy/ had zero unit coverage.
131 architecture tests + 4 new unit tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… IN predicates The previous predicate-shape pattern required workspace_id to be followed by an operator (=/IN/IS/...). Hibernate emits composite-key lookups as (wm.user_id, wm.workspace_id) IN ((?, ?)) where workspace_id is followed by ')'. The tighter pattern produced false positives: every PracticeFindingControllerIntegrationTest call returned TenancyViolationException because the workspace_membership composite-PK query never matched. Reverted to the broader \bworkspace_id\b word-boundary match. The remaining theoretical false negative — a string literal containing the bare word — is exceedingly rare in Hibernate-emitted SQL (which is always parameterized) and far cheaper than false-positive request failures. Documented the trade-off in the pattern's javadoc. Verified PracticeFindingControllerIntegrationTest passes locally (28 tests across 5 test classes green). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WorkspaceAgnosticAspect's @Within pointcut matches by the *declaring* type of the join-point method. Inherited Spring Data methods (findAll, findById) declare on CrudRepository/JpaRepository, not on the user's @WorkspaceAgnostic-annotated interface, so the type-level advice never fired and the StatementInspector threw TenancyViolationException on PracticeFindingRepository.findAll/findById in tests. Adds a third advice that intercepts every Spring Data Repository call and walks the proxy's interfaces with AnnotationUtils.findAnnotation; bypass opens only when @WorkspaceAgnostic is found on one of the user-declared interfaces. Non-annotated repos pay one method lookup per call. NetworkPolicy.requireAbsoluteHttp choked on the documented {appServerIp} template placeholder produced before DockerSandboxAdapter substitution, because URI.create rejects { in the authority component. Recognises the {name} placeholder shape, performs a light http(s) scheme prefix check on template URLs, and defers strict URI parsing to the adapter (which sees the resolved value). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ations The WorkspaceStatementInspector was over-strict: it flagged every SQL statement that touched a scoped table without literal "workspace_id" in the text. That caught the actual threat (cross-tenant SELECTs) but also caught a large class of Hibernate-emitted patterns that are tenancy-safe by construction, breaking integration tests in THROW mode. Inspector relaxations, each justified by threat model: - INSERT statements unconditionally pass. INSERTs create new rows; they cannot leak existing data across workspaces. The workspace_id (or FK chain) is set by application code in the inserted row. - PK-only DELETE/UPDATE pass. Hibernate emits "DELETE FROM table WHERE id = ?" and "UPDATE table SET … WHERE id = ?" for delete(entity) / save(entity). The caller already loaded the row within a workspace- scoped transaction; the surrogate PK uniquely identifies it. Also allows the optimistic-lock variant "AND version = ?". - PK-anchored SELECT loads pass. Hibernate's entity load / lazy-fetch SQL is "SELECT … FROM table alias … WHERE alias.id = ?" (with optional discriminator AND for single-table inheritance, optional eager-fetch JOINs). The caller already had the surrogate PK, which is opaque to URL inputs. - Single-FK predicate (alias.<col>_id = ?) is accepted in the same position. This matches Hibernate-emitted @onetomany / @manytomany collection initialisation (e.g. DELETE FROM issue_blocking WHERE blocked_issue_id = ? when removing a parent). The TABLE_REFERENCE_PATTERN dropped INTO so INSERTs short-circuit at the regex level rather than depending on the workspace_id pattern. WorkspaceAgnosticAspect gained a third advice that intercepts every Spring Data Repository call and walks the proxy's interfaces for @WorkspaceAgnostic via AnnotationUtils.findAnnotation. Spring AOP's @Within matches by the method's declaring type, so inherited findAll/findById on annotated repos never fired the type-level advice; the new advice closes that gap. NetworkPolicy.requireAbsoluteHttp choked on the documented {appServerIp} template placeholder before DockerSandboxAdapter substitution. The record now recognises the {name} placeholder shape, performs a light http(s) scheme prefix check on template URLs, and defers strict URI parsing to the adapter (which sees the resolved value). 24 gitprovider/leaderboard/profile/practices/mentor repositories that scope through FK chains rather than a direct workspace_id column now carry @WorkspaceAgnostic with an explicit rationale. Without these, custom JPQL methods (e.g. backfillCommitActors) and many-to-many lazy fetches still tripped the inspector even with the new carve-outs. Verified locally: - 22 inspector unit tests pass (incl. 5 new carve-out tests) - 2588 unit tests pass - 131 architecture tests pass - 754 integration tests pass (was 5 errors + cascade-skipped before) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous unit+arch CI run completed all 2719 tests with 0 failures but Surefire still reported "There was a timeout in the fork" because: - forkedProcessTimeoutInSeconds=300 is per-fork lifetime. On the GitHub Actions runner the suite consistently runs ~10 minutes, well past the limit. Locally it completes in 2 minutes; CI is 5x slower. - forkedProcessExitTimeoutInSeconds=60 is the post-test grace period for the JVM to exit. With ScopedRateLimitTracker counters, container manager executors, and Spring contexts shutting down, 60 s is tight. Bumps the fork lifetime to 15 minutes (still bounded enough to catch genuine hangs) and the exit grace to 3 minutes. No reduction in safety — ping-based process-checker still detects unresponsive forks; this only widens the wall-clock budget for legitimately long parallel suites on slower runners. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
<includeAll/> silently lexicographically interleaves changelogs from parallel PRs, leaving order drift to chance. Explicit <include> entries preserved verbatim from main force a merge conflict in master.xml that the authors have to resolve — the order becomes a deliberate choice. Order matches main exactly, including the historical out-of-timestamp sequences (e.g., 1741971047951 before 1741704706273) that reflect the actual order applied to existing databases. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Post-merge audit findings: scripts/nats-extract-examples.ts: DEFAULT_EXAMPLES_DIR pointed at server/application-server/src/test/resources/github — the dir no longer exists after the layout rename. Drop the application-server segment. .github/scripts/label-pr.js: "generated OpenAPI spec" exclusion regex matched the old path; would have mislabelled hand-written edits to server/openapi.yaml after the rename. Update to the new path. docs/decisions/0003-spring-modulith-adoption.md: pre-dated the narrow-OPEN-to-CLOSED refactor (a9d8e03). ADR listed 6 OPEN kernels; actual code has 2 (config, gitprovider) plus 5 modules with @NamedInterface narrowing. Rewrite to reflect the implementation. docs/decisions/0004-sql-layer-tenancy-via-statement-inspector.md: pre-dated the JSqlParser-removal (e5ea564) and the regex carve-out work (b196403). ADR claimed JSqlParser was in the pipeline and listed it as a dependency. Rewrite the Decision section to document the regex-only pipeline, the four carve-outs, the JSqlParser-rejection rationale, and the Known trade-offs section (FK-anchored carve-out, mention-anywhere workspace_id, leading-CTE limitation, comment/literal false-pass). Document the controller-layer CrossWorkspaceIsolationTest as the eventual backstop and the follow-up status. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ego-death audit found: 1. ADR 0005 + application.yml comment claimed both `hephaestus.runtime.server.enabled` and `hephaestus.runtime.worker.enabled` were wired and the JAR was "bit-identical" across deploy splits. Only `worker.enabled` is actually wired (DockerSandboxConfiguration + AgentNatsConsumerConfig). `server.enabled` is declared as a constant but no `@ConditionalOnProperty` references it — setting it false is a no-op. Setting expectations honestly: ADR now marks `server.enabled` as a reserved property key and explicitly notes the wiring lands in a follow-up when a concrete deploy-split need surfaces. application.yml comment matches. RuntimeRole.java javadoc updated. 2. ADR 0005 claimed a "single role-smoke test variant is run per role." No such `RuntimeSmokeIT` lands in this epic; the role-isolation invariants are enforced at compile time by RuntimeRoleBoundaryTest (ArchUnit). Updated ADR to reflect reality and defer the end-to-end smoke to the deploy-split epic. 3. master.xml uses explicit `<include>` per changelog (commit 351fa0b). The `db:draft-changelog` script does not auto-append to master.xml, and the database-migration.mdx workflow did not mention this step. New contributors would silently skip the master.xml edit and Liquibase would silently drop their migration. Added an explicit step to the workflow with rationale (deliberate merge-conflict-forces-order-decision vs `<includeAll/>` silent interleave). 4. docs/contributor/overview.mdx + local-development.mdx had stale repo layout pointers (`server/application-server/`, application-local.yml path). Fixed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 20, 2026
refactor(server): rename application-server module to server, consolidate Liquibase includeAll
#1123
Closed
Closed
Closed
Closed
FelixTJDietrich
added a commit
that referenced
this pull request
May 20, 2026
Reconcile activity-monitor feature with the server foundations rebuild on main (#1290): directory move server/application-server → server, package rename de.tum.in.www1.hephaestus → de.tum.cit.aet.hephaestus, and the new @WorkspaceScopedController / WorkspaceContext patterns. Conflicts resolved: - ProfileActivityMonitorDTO.java: package + imports updated to new namespace - UserProfileController.java: import block merged; activity-monitor endpoint reuses the existing @WorkspaceScopedController setup from main - UserProfileService.java: import block merged; getActivityMonitor() and helpers retained against the ActivityEvent-sourced buildReviewActivity() that now lives on main Verified: server compile + UserProfileServiceTest (8/8) green; webapp tsc --noEmit and biome check both clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collaborator
Author
|
🎉 This PR is included in version 0.71.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Lays the structural foundation for the next phase of server consolidation. Flattens
the repo layout (
server/,webhook-ingest/at root), renames the Java base packageto match the current chair (
de.tum.cit.aet.hephaestus), adopts Spring Modulith 2.0with module boundaries verified on every PR, introduces SQL-layer multi-tenancy via a
WorkspaceStatementInspector, formalises a two-role runtime topology (server/worker) gated by@ConditionalOnProperty, and tightens the sandbox SPI.These are not user-facing features — they are the invariants every subsequent epic
will assume. Behaviour is unchanged for end users; the rename moves files, the
Modulith work declares boundaries, the tenancy work asserts an existing invariant,
and the runtime-role work introduces seams that today still resolve to "everything
runs in one JVM".
Fixes #1097
What changed
Layout + package (mechanical):
server/application-server/→server/;server/webhook-ingest/→webhook-ingest/de.tum.in.www1.hephaestus→de.tum.cit.aet.hephaestushephaestus→hephaestus-serverLiquibase:
master.xmlretained with explicit<include>per changelog in thesame historical apply order as
main(no<includeAll/>switch). Two PRs that addmigrations in parallel produce a natural merge conflict in
master.xml, forcing theauthors to agree on order —
<includeAll/>would silently interleave themlexicographically.
Spring Modulith 2.0:
@ApplicationModuleType.OPENto default CLOSED with explicit@NamedInterfacedeclarations (
integrations.posthog,activity.scoring,core.{exception,security,proxy,runtime,event},practices.{model,spi,review,finding},workspace.{context,authorization,spi,settings})config+gitproviderremainType.OPEN(genuine shared kernels — narrowingrequires a separate epic given their 1000+ external imports)
ModulithVerificationTestruns in the architecture surefire group on every PRSecurityUtilsmoved from root package →core/security/to break a root↔workspacecycle that surfaced once
workspaceclosedSQL-layer multi-tenancy (
core/tenancy/):WorkspaceStatementInspector(HibernateStatementInspector) asserts every SQLstatement against a workspace-scoped table carries a
workspace_idpredicate.Regex-only pipeline (no SQL parser dependency): bypass-check → mode-OFF → Caffeine
cache → INSERT short-circuit → PK-anchored DML/SELECT short-circuit →
workspace_idword-boundary fast path → table-extract fallback. Fail-open onpathological input via the
tenancy.parse_failure.totalcounter (the inspectoritself must never throw).
Data JPA to auto-activate
JSqlParserQueryEnhancer, which threw on legitimatePostgres-escaped
@Querynatives (e.g.,CONCAT(:id\:\:text, ...)) and brokeapplication boot.
idvia a workspace-scoped path, so PK-anchored DML/SELECT and INSERTs whose value
list carries
workspace_idare safe by construction): INSERT,WHERE id = ?DML(with optional optimistic-lock
version = ?), andWHERE alias.id = ?SELECTs(the EntityManager.find / lazy fetch shape). Each pattern is intentionally narrow;
any additional non-PK predicate falls through to the standard
workspace_idcheck.
WorkspaceScopedTablesis the single source of truth — populated at startup fromHibernate's
MappingMetamodel, covering entities AND many-to-many@JoinTablecollections (so
SELECT * FROM issue_labelcannot leak labels across workspaces).Fails fast if the metamodel call chain regresses.
@WorkspaceAgnosticis now load-bearing:WorkspaceAgnosticAspect(
@Order(HIGHEST_PRECEDENCE)) opens aTenancyBypassscope so cross-workspacequeries pass the inspector cleanly. The aspect resolves the annotation across
inherited Spring Data interface methods, so annotating a repository (or one of
its parents) is sufficient. Dropping the annotation from a repository now
causes a
TenancyViolationExceptionunderenforcement=throw.workspace_idcolumn are marked
@WorkspaceAgnostic(each carries a one-line rationale on thetype) — same defect class is caught one hop up, on the parent's
workspace_idpredicate.
hephaestus.tenancy.enforcement—throw(default intest),log(default elsewhere; Micrometer countertenancy.violation.total{table, mode}observable),
off.WorkspaceScopedTablesParityTest(ArchUnit) prevents drift between the productionallowlist and the existing arch-test entity list.
Runtime roles (
core/runtime/):hephaestus.runtime.server.enabledandhephaestus.runtime.worker.enabledproperty keys + capability flag
hephaestus.sandbox.llm-proxy.enabledmatchIfMissing=true(enforced byRuntimeRoleBoundaryTest— afresh JAR with zero env vars boots a full monolith; operators opt OUT, not IN)
AgentNatsConfigurationsplit into connection (always-on) + consumer(worker-only)
DockerSandboxConfigurationgated by the worker-role propertyLlmProxyController+LlmProxySecurityConfiggated by the capability flagSandbox SPI tightening:
NetworkPolicyis a record with typed URL validation in the compact constructor(rejects relative URLs and non-
http(s)schemes at SPI build time; preserves${placeholder}template support for@ConfigurationPropertiesbinding)Documentation:
docs/decisions/(MADR format) capture: flat top-level layout,Java package rename, Modulith adoption, SQL-layer tenancy, two-role runtime
topology, LLM-proxy-on-coordinator trust model, sandbox SPI shape
AGENTS.mdupdated (Spring Boot 4, Node 24, pnpm 11, architecture-testsMaven profile)
Tests:
TenancyBypass,WorkspaceStatementInspector(regexpipeline + every carve-out + regression for composite-key tuple-IN and
Postgres
\:\:textcasts),NetworkPolicy,WorkspaceScopedTables(includingfail-fast guard)
RuntimeRoleBoundaryTestmutation-verified: introducingmatchIfMissing=falseon ahephaestus.runtime.*gate now fails the buildWorkspaceScopedTablesParityTestprevents SSOT driftHow to test
cd server && ./mvnw -Parchitecture-tests test— architecture tests (Modulithverify + runtime-role boundary + tenancy parity + existing ArchUnit suites)
cd server && ./mvnw test— full unit suite includes the new tenancy testspnpm run format:check— clean across webapp, server, webhook-ingestdefaults
matchIfMissing=true, so no env-var changes are needed locally or instaging. Future split deploys flip flags only.
Notable scope cuts (deferred — filed as follow-ups in #1097 sub-issues)
@TenantIdHibernate native multi-tenancy — needs multi-hopworkspace_iddenormalisation on 20+ entities; separate 6-12 week epic
CrossWorkspaceIsolationTest— fixture work meaningful enoughto deserve its own PR; the SQL-layer inspector covers the same defect class
SecretMount/EmptyDirMount/ConfigMapMount— speculative API surfacewith zero current consumers; lands with the K8s adapter epic
WorkerAuthProviderBYO-runner SPI — premature; needs real BYO requirementsout of scope
These are deliberate scope cuts (documented in the ADRs), not unfinished work.
Risks
merge soon to limit drift.
SELECT,
workspace_idword-boundary) are holes in the SQL-tenancy wall bydesign. Each is anchored on a documented invariant; any additional non-PK
predicate falls through to the standard check. Worth a reviewer pass.
@WorkspaceAgnosticon FK-chain repositories. Each annotated repositorybypasses the inspector entirely. Rationale is one-line per type; reviewer pass
recommended.
@IncubatingMappingMetamodelAPI.WorkspaceScopedTablesfailsfast if the call chain ever regresses — silent fall-through to a no-op inspector
is the worst possible failure mode.
21f8c4693) cleared a pre-existing unit+archfork-timeout failure without diagnosing the leak. Tracked as follow-up debt.
🤖 Generated with Claude Code