Compare commits
5 Commits
dev/multi-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9896e7c657 | |||
| 4574aa17c3 | |||
| bb010d565e | |||
| e809ce63d3 | |||
| 59c0d0c3b4 |
@ -1,6 +1,6 @@
|
||||
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":50,"PLN":129,"LSN":66,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0065","status":"open","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[{"id":"AGD-0068","file":"AGD-0068-multi-frontend-avoid-premature-abstractions.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0064","status":"open","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[{"id":"AGD-0067","file":"AGD-0067-multi-frontend-architectural-tests.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":52,"PLN":131,"LSN":68,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0065","status":"done","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0067","file":"discussion/lessons/DSC-0065-multi-frontend-avoid-premature-abstractions/LSN-0067-multi-frontend-phase-rejects-premature-infrastructure.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
{"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
{"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
{"type":"discussion","id":"DSC-0062","status":"done","ticket":"multi-frontend-pvm-neutrality","title":"Neutralidade da PVM e identificacao PBX independente de PBS","created_at":"2026-07-15","updated_at":"2026-09-18","tags":["vm-arch","runtime","pvm","pbx","compiler","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0061","file":"discussion/lessons/DSC-0062-multi-frontend-pvm-neutrality/LSN-0061-pbx-runtime-boundary.md","status":"done","created_at":"2026-09-18","updated_at":"2026-09-18"}]}
|
||||
{"type":"discussion","id":"DSC-0061","status":"done","ticket":"multi-frontend-sdk-canonical-definition","title":"Auditoria e centralizacao gradual da definicao canonica do SDK","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","sdk","stdlib","hostcalls","intrinsics","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0064","file":"discussion/lessons/DSC-0061-multi-frontend-sdk-canonical-definition/LSN-0064-runtime-owns-console-abi-pbs-is-delivery.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
|
||||
@ -0,0 +1,151 @@
|
||||
---
|
||||
id: LSN-0066
|
||||
ticket: multi-frontend-architectural-tests
|
||||
title: ArchUnit guards Java multi-frontend boundaries
|
||||
created: 2026-09-19
|
||||
tags: [compiler, compiler-general, studio, frontend, architecture, tests, multi-frontend]
|
||||
---
|
||||
|
||||
# ArchUnit guards Java multi-frontend boundaries
|
||||
|
||||
## Original Problem
|
||||
|
||||
The multi-frontend boundaries already existed in specs and lessons: common
|
||||
code selects a frontend by `languageId`, `IRBackend` is language-neutral,
|
||||
lifecycle assembly is common, and PBS is one provider. The automated
|
||||
protection did not match that model.
|
||||
|
||||
The old guards walked Java source and searched for substrings:
|
||||
|
||||
- `import p.studio.compiler.pbs`
|
||||
- `new PBSFrontendPhaseService(`
|
||||
- `new PbsEditorialSupportService(`
|
||||
|
||||
Those checks missed field, signature, and constructor coupling. They also
|
||||
treated `p.studio.compiler.pbs` as the whole PBS surface. Facades such as
|
||||
`PBSFrontendProvider`, `PBSDefinitions`, and `PBSFrontendPhaseService` live
|
||||
in the PBS module but not in that package. `prometeu-app` and
|
||||
`prometeu-lsp-v1` declared an `implementation` dependency on PBS without
|
||||
importing any PBS type.
|
||||
|
||||
## Consolidated Decision
|
||||
|
||||
Java multi-frontend boundaries are enforced by **ArchUnit + JUnit 5**, not by
|
||||
source substring search, except for the `__pbs.` literal scan that bytecode
|
||||
cannot see.
|
||||
|
||||
Durable locks from DEC-0050:
|
||||
|
||||
- ArchUnit is test-only, pinned in `gradle/libs.versions.toml`.
|
||||
- Rules live in the test-only Gradle project `:prometeu-architecture-tests`.
|
||||
That project has no production sources. It is not a plugin loader.
|
||||
- A PBS type is a class whose bytecode originates from
|
||||
`:prometeu-compiler:frontends:prometeu-frontend-pbs`. Package
|
||||
`p.studio.compiler.pbs` is not the complete boundary. Name prefixes
|
||||
`PBS*` / `Pbs*` are not the identity.
|
||||
- Production may depend on PBS types only in:
|
||||
- the PBS module itself;
|
||||
- `prometeu-frontend-registry` / `FrontendRegistryService`, and only on
|
||||
`PBSFrontendProvider` and `PBSDefinitions`.
|
||||
- `AppContainer` is not a PBS composition root. It may call
|
||||
`FrontendRegistryService.bootstrapDefaults()`. It must not instantiate PBS
|
||||
types.
|
||||
- `synth` is not in the production allowlist and must not be registered in
|
||||
`bootstrapDefaults()`.
|
||||
- Runtime, PVM, and the Rust repository stay out of this discussion
|
||||
(LSN-0061).
|
||||
- `IRBackendExecutableContractTest` remains the public-contract shape audit.
|
||||
ArchUnit covers module-origin dependence.
|
||||
|
||||
Do not migrate PBS facades into another package just to make the test
|
||||
easier. Do not ban the word `PBS` in docs, fixtures, or comments.
|
||||
|
||||
## Final Implementation
|
||||
|
||||
`:prometeu-architecture-tests` imports production classes under `p.studio`
|
||||
with `ImportOption.DoNotIncludeTests` and applies three ArchUnit rules:
|
||||
|
||||
1. classes outside the PBS module and the registry must not depend on
|
||||
PBS-module types;
|
||||
2. registry production may depend only on `PBSFrontendProvider` and
|
||||
`PBSDefinitions` from that module;
|
||||
3. frontend-api `p.studio.compiler.models` must not depend on PBS-module
|
||||
types.
|
||||
|
||||
A separate JUnit test walks `build.gradle.kts` files and allows the PBS
|
||||
project dependency only for:
|
||||
|
||||
- `:prometeu-compiler:prometeu-frontend-registry` as `implementation`/`api`;
|
||||
- `:prometeu-architecture-tests` as `testImplementation` (needed to classify
|
||||
class origin).
|
||||
|
||||
The `__pbs.` source scan moved to
|
||||
`CommonBackendPbsPrefixScanTest` and still covers `backend/`, `lifecycle/`,
|
||||
and `workspaces/stages/`.
|
||||
|
||||
Gradle graph cleanup:
|
||||
|
||||
- `prometeu-app` and `prometeu-lsp-v1` no longer `implementation` PBS.
|
||||
Runtime classloading of `PBSFrontendProvider` still comes through the
|
||||
registry's implementation dependency.
|
||||
- `prometeu-studio` no longer `testImplementation` PBS. No studio test
|
||||
compiled against PBS types.
|
||||
|
||||
Source-walk import/constructor tests were deleted.
|
||||
`FrontendProviderBoundaryTest` keeps only
|
||||
`compileOnlyFrontendProviderMayOmitLanguageService`.
|
||||
|
||||
Specs 19 §11 and 20 now say common/platform code must not depend on types
|
||||
from the PBS frontend module. Matrix rows G19-11.6, G20-4.1.2, and G20-4.3.3
|
||||
point at the ArchUnit rules and the `__pbs.` scan.
|
||||
|
||||
## Examples
|
||||
|
||||
Good:
|
||||
|
||||
- ArchUnit failure names the class that depends on a PBS-module type,
|
||||
including a facade in package `p.studio.compiler`.
|
||||
- Registry `bootstrapDefaults()` constructs `PBSFrontendProvider`. That is
|
||||
the allowed composition root.
|
||||
- App calls `FrontendRegistryService.bootstrapDefaults()` and has no PBS
|
||||
project dependency.
|
||||
|
||||
Bad:
|
||||
|
||||
- Searching source for `import p.studio.compiler.pbs` and calling the
|
||||
boundary protected.
|
||||
- Treating `AppContainer` as the place that may `new PBSFrontendProvider()`.
|
||||
- Identifying PBS by class name prefix, so a future `PbsLike` helper in
|
||||
common code is banned or a PBS facade is missed.
|
||||
- Adding ArchUnit to `java-common-conventions` for every module.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Same Java package does not mean same Gradle module.
|
||||
`FrontendRegistryService` and `PBSFrontendProvider` both sit in
|
||||
`p.studio.compiler`. Origin is the module path or JAR name.
|
||||
- `@AnalyzeClasses` classes must not mix `@ArchTest` and Jupiter `@Test` on
|
||||
the same class. The ArchUnit engine owns `@ArchTest`.
|
||||
- `DoNotIncludeTests` is required. The architecture-test sources themselves
|
||||
reference PBS types in order to name the allowlist.
|
||||
- Removing PBS from app/lsp compile classpath is safe only because registry
|
||||
still `implementation`s PBS, which is runtime-transitive.
|
||||
- `testImplementation` of PBS on studio is not a production leak, but if no
|
||||
studio test needs PBS types it is still a lying graph. The allowlist test
|
||||
rejects it.
|
||||
- Do not point ArchUnit at the Rust runtime. `__pbs.` in Java source is a
|
||||
string contract, not a type dependency.
|
||||
|
||||
## References
|
||||
|
||||
- Decision: `DEC-0050`
|
||||
- Plan: `PLN-0129`
|
||||
- Related lessons: `LSN-0055`, `LSN-0059`, `LSN-0062`, `LSN-0063`,
|
||||
`LSN-0061`, `LSN-0065`
|
||||
- `docs/specs/compiler/19. Verification and Safety Checks Specification.md`
|
||||
- `docs/specs/compiler/20. IRBackend to IRVM Lowering Specification.md`
|
||||
- `docs/specs/compiler/22. Backend Spec-to-Test Conformance Matrix.md`
|
||||
- `prometeu-architecture-tests/src/test/java/p/studio/architecture/PbsModuleBoundaryArchTest.java`
|
||||
- `prometeu-architecture-tests/src/test/java/p/studio/architecture/PbsGradleDependencyAllowlistTest.java`
|
||||
- `prometeu-compiler/prometeu-frontend-registry/src/main/java/p/studio/compiler/FrontendRegistryService.java`
|
||||
- `prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java`
|
||||
@ -0,0 +1,167 @@
|
||||
---
|
||||
id: LSN-0067
|
||||
ticket: multi-frontend-avoid-premature-abstractions
|
||||
title: Multi-frontend phase rejects premature infrastructure
|
||||
created: 2026-09-19
|
||||
tags: [compiler, compiler-general, studio, frontend, architecture, multi-frontend, simplicity]
|
||||
---
|
||||
|
||||
# Multi-frontend phase rejects premature infrastructure
|
||||
|
||||
## Original Problem
|
||||
|
||||
The multi-frontend preparation needed a standing simplicity rule. The original
|
||||
agenda asked for that rule *before* the other multi-frontend discussions.
|
||||
Those discussions already closed (DSC-0054 through DSC-0064). The remaining
|
||||
risk was not a missing plugin platform.
|
||||
|
||||
The remaining risk was twofold:
|
||||
|
||||
1. using "keep it simple" to put PBS construction back into common compiler,
|
||||
Studio, LSP, or app code;
|
||||
2. inventing marketplace, JAR loading, RPC, validator-core, or a generic SPI
|
||||
"for a future language" that does not exist in this repository.
|
||||
|
||||
`FrontendRegistryService` is a static singleton. The original agenda listed
|
||||
a global service locator as out of scope. Without an explicit exception, a
|
||||
later change could inject a `Registry` into the pipeline for purity, which
|
||||
DSC-0063 already refused.
|
||||
|
||||
## Consolidated Decision
|
||||
|
||||
This is standing policy for *future* work in the current phase: PBS as the
|
||||
only product frontend, `synth` as a test-only provider, frontends in-repo.
|
||||
It is not an audit of closed discussions and must not rewrite them.
|
||||
|
||||
Durable locks from DEC-0051:
|
||||
|
||||
- Implementation of this discussion is the decision plus this lesson. There
|
||||
is no new spec, no new ArchUnit rule, and no production refactor.
|
||||
- Spec 23 §2 and §8.3 remain the discovery MUST NOT surface. Do not copy
|
||||
those sentences into a new spec.
|
||||
- Architectural tests stay owned by LSN-0066.
|
||||
|
||||
### Static registry is allowed
|
||||
|
||||
`FrontendRegistryService` as a static singleton, with public `register` and
|
||||
`unregister` and `bootstrapDefaults()` registering only PBS, is the
|
||||
composition mechanism of this phase. Treat that as an explicit exception,
|
||||
not as a banned service locator.
|
||||
|
||||
Discovery must not use `ServiceLoader`, classpath scanning, reflection-based
|
||||
discovery, an external JAR, or runtime plugin installation.
|
||||
|
||||
Do not inject a `Registry` into the pipeline, LSP, or `AppContainer`.
|
||||
`AppContainer` is not a PBS composition root. It may call
|
||||
`FrontendRegistryService.bootstrapDefaults()`. It must not instantiate PBS
|
||||
types.
|
||||
|
||||
### Real in-repo consumer
|
||||
|
||||
A new interface, module, SPI, generator, or framework is allowed only when
|
||||
a real in-repo consumer exists. In this phase that consumer is one of:
|
||||
|
||||
- the PBS product frontend;
|
||||
- the `synth` test frontend;
|
||||
- a second product frontend accepted by its own decision.
|
||||
|
||||
"For a future language", "for the marketplace", or "when we have plugins"
|
||||
is not a consumer.
|
||||
|
||||
`FrontendProvider`, `IRBackend`, common lifecycle assembly, and test-only
|
||||
ArchUnit are not premature. They already have consumers.
|
||||
|
||||
### Forbidden in this phase
|
||||
|
||||
Until a later decision authorizes it with a real consumer, do not introduce:
|
||||
|
||||
1. dynamic frontend discovery (plugin, `ServiceLoader`, scan, external JAR,
|
||||
runtime install);
|
||||
2. RPC, sockets, a process supervisor, or version negotiation between
|
||||
compiler and frontend;
|
||||
3. a marketplace, hot reload, or frontend sandbox;
|
||||
4. a validator-core or generic validation framework;
|
||||
5. an SDK schema/generator or `HOSTCALLS.csv` as canonical ABI (LSN-0064);
|
||||
6. a product module `frontends/synth`;
|
||||
7. an injected `Registry`, or `AppContainer` as the PBS composition root.
|
||||
|
||||
This list is not a monorepo-wide simplicity charter. It does not govern
|
||||
JavaFX, packer, or LSP protocol design. Owner is `compiler/general`, with
|
||||
impact on `studio` on the multi-frontend axis.
|
||||
|
||||
### Neighbors stay closed
|
||||
|
||||
Do not reinterpret, relax, "simplify", or reimplement:
|
||||
|
||||
- provider/registry (LSN-0055 / spec 23 §8.3);
|
||||
- common lifecycle;
|
||||
- console ABI / PBS delivery;
|
||||
- `synth`;
|
||||
- ArchUnit / PBS module identity.
|
||||
|
||||
Using this policy to keep `new PBSFrontendPhaseService()` (or equivalent)
|
||||
in common code is refused. That is PBS recoupling, not simplicity.
|
||||
|
||||
A second product frontend needs its own decision. This lesson neither
|
||||
replaces that decision nor blocks it.
|
||||
|
||||
## Final Implementation
|
||||
|
||||
No production Java, Gradle, spec, or ArchUnit file changed.
|
||||
|
||||
The map above is the implementation. Future compiler/studio plans on this
|
||||
axis should cite it when they propose a new module, SPI, or discovery
|
||||
mechanism.
|
||||
|
||||
Code that already matches the phase:
|
||||
|
||||
- `FrontendProvider` exposes `specification()`, `compiler()`, and optional
|
||||
`languageService()`;
|
||||
- `FrontendRegistryService.bootstrapDefaults()` registers PBS only;
|
||||
- `AppContainer` only calls that bootstrap;
|
||||
- `synth` lives in build-pipeline test sources;
|
||||
- ArchUnit lives in `:prometeu-architecture-tests`.
|
||||
|
||||
## Examples
|
||||
|
||||
Good:
|
||||
|
||||
- add a method to `FrontendProvider` because PBS or `synth` cannot compile
|
||||
or register without it;
|
||||
- keep `unregister` on the static registry so `synth` tests can clean up;
|
||||
- reject a proposal to load language JARs from a plugin directory.
|
||||
|
||||
Bad:
|
||||
|
||||
- inject `Registry` into `BuilderPipelineService` because static state looks
|
||||
like a service locator;
|
||||
- create `frontends/synth` as a product module;
|
||||
- add validator-core "so the next language has somewhere to put checks";
|
||||
- keep `new PBSFrontendPhaseService()` in pipeline code and call that
|
||||
simpler than going through the provider.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- The original agenda order ("before the other agendas") is historical.
|
||||
Do not reopen DSC-0054..DSC-0064 to satisfy it.
|
||||
- Static registry plus explicit `register`/`unregister` is visible. Hidden
|
||||
`ServiceLoader` discovery is not the same thing.
|
||||
- A second product frontend *is* a real consumer. Do not use this lesson to
|
||||
refuse that frontend; use it to refuse infrastructure that frontend does
|
||||
not yet need.
|
||||
- Spec 23 already forbids plugin discovery. Do not duplicate it. Do not
|
||||
treat silence in spec 23 as permission for RPC or validator-core.
|
||||
- Simplicity does not mean PBS may leak into common modules. LSN-0066 still
|
||||
owns that boundary.
|
||||
|
||||
## References
|
||||
|
||||
- Decision: `DEC-0051`
|
||||
- Plan: `PLN-0130`
|
||||
- Related lessons: `LSN-0055`, `LSN-0060`, `LSN-0063`, `LSN-0064`,
|
||||
`LSN-0065`, `LSN-0066`
|
||||
- `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md`
|
||||
(§2, §8.3)
|
||||
- `prometeu-compiler/prometeu-frontend-registry/src/main/java/p/studio/compiler/FrontendRegistryService.java`
|
||||
- `prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendProvider.java`
|
||||
- `prometeu-app/src/main/java/p/studio/AppContainer.java`
|
||||
@ -1,79 +0,0 @@
|
||||
---
|
||||
id: AGD-0067
|
||||
ticket: multi-frontend-architectural-tests
|
||||
title: Testes arquiteturais para fronteiras multi-frontend
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [compiler, compiler-general, studio, frontend, architecture, tests, multi-frontend]
|
||||
---
|
||||
|
||||
# Agenda - Criar testes arquiteturais
|
||||
|
||||
## Objetivo
|
||||
|
||||
Domain owner: `compiler/general`, com impacto em `studio` e `vm-arch`.
|
||||
|
||||
Definir testes que protejam fronteiras: backend comum sem PBS, IR comum sem AST PBS, Studio comum sem instanciacao PBS fora do composition root e runtime neutro.
|
||||
|
||||
## Contexto atual
|
||||
|
||||
O repo ja tem testes Java para backend, bytecode e IRVM. ArchUnit pode ser util, mas o alinhamento proibe adicionar biblioteca arquitetural quando testes simples bastarem.
|
||||
|
||||
## Escopo
|
||||
|
||||
- Escolher ferramenta minima.
|
||||
- Definir regras por pacote.
|
||||
- Adicionar testes que sejam baratos e claros.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Cobrir toda arquitetura do monorepo.
|
||||
- Adicionar ArchUnit sem justificativa concreta.
|
||||
- Bloquear referencias PBS em testes PBS ou no frontend PBS.
|
||||
|
||||
## Arquivos e componentes a inspecionar
|
||||
|
||||
- `build.gradle.kts` e version catalog se houver.
|
||||
- `prometeu-compiler/prometeu-build-pipeline/src/test/java/...`
|
||||
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/...`
|
||||
- `prometeu-studio/src/test/java/...`
|
||||
- pacotes runtime/PVM quando presentes
|
||||
|
||||
## Alteracoes propostas
|
||||
|
||||
Opcao A: testes Java/reflection simples para imports, nomes de tipos e packages publicos.
|
||||
|
||||
Opcao B: ArchUnit caso a quantidade de regras e modulos justifique.
|
||||
|
||||
Recomendacao inicial: comecar com testes simples; reavaliar ArchUnit se as regras virarem frageis.
|
||||
|
||||
## Estrategia de implementacao
|
||||
|
||||
Definir allowlist de packages PBS, composition root e testes PBS. Escrever verificacoes independentes por dominio para falhas legiveis.
|
||||
|
||||
## Testes necessarios
|
||||
|
||||
- Backend/common nao importa `frontend.pbs`, `parser.pbs` ou `semantic.pbs`.
|
||||
- IR comum nao expoe tipos PBS.
|
||||
- Studio comum nao instancia servicos PBS fora da allowlist.
|
||||
- Runtime nao usa nomenclatura ou semantica PBS.
|
||||
|
||||
## Criterios de aceitacao
|
||||
|
||||
- Regras rodam no build local.
|
||||
- Falhas apontam arquivo/pacote responsavel.
|
||||
- Excecoes autorizadas estao documentadas.
|
||||
|
||||
## Riscos
|
||||
|
||||
- Teste textual produzir falso positivo por docs ou fixtures.
|
||||
- Criar barreira rigida antes de definir provider e lifecycle.
|
||||
|
||||
## Decisoes que devem ser registradas
|
||||
|
||||
- Ferramenta escolhida.
|
||||
- Allowlist de packages.
|
||||
- Ordem de introducao das regras.
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
---
|
||||
id: AGD-0068
|
||||
ticket: multi-frontend-avoid-premature-abstractions
|
||||
title: Evitar abstracoes prematuras na preparacao multi-frontend
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [compiler, compiler-general, studio, frontend, architecture, multi-frontend, simplicity]
|
||||
---
|
||||
|
||||
# Agenda - Evitar abstracoes prematuras
|
||||
|
||||
## Objetivo
|
||||
|
||||
Domain owner: `compiler/general`, com impacto em `studio`.
|
||||
|
||||
Definir limites para a preparacao multi-frontend: remover acoplamento com baixo custo sem construir marketplace, hot reload, RPC, sandbox ou infraestrutura generica antes da necessidade real.
|
||||
|
||||
## Contexto atual
|
||||
|
||||
O PBS continuara sendo o unico frontend implementado no curto prazo. O objetivo e preparar contratos, fronteiras e testes de neutralidade, nao criar uma plataforma completa de plugins.
|
||||
|
||||
## Escopo
|
||||
|
||||
- Estabelecer principios para as agendas derivadas.
|
||||
- Definir o que e explicitamente proibido nesta fase.
|
||||
- Criar criterio para rejeitar abstracoes sem uso real.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Plugin marketplace.
|
||||
- Carregamento dinamico de JARs.
|
||||
- RPC, sockets, process supervisor ou version negotiation complexa.
|
||||
- Service locator global.
|
||||
|
||||
## Arquivos e componentes a inspecionar
|
||||
|
||||
- `prometeu-compiler/prometeu-frontend-registry/...`
|
||||
- `prometeu-app/src/main/java/p/studio/AppContainer.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/...`
|
||||
- planos futuros derivados das discussions multi-frontend
|
||||
|
||||
## Alteracoes propostas
|
||||
|
||||
Opcao A: registrar esta decisao como guardrail transversal antes das demais.
|
||||
|
||||
Opcao B: deixar cada agenda repetir suas restricoes locais.
|
||||
|
||||
Recomendacao inicial: criar uma decisao curta e transversal apos discussao, para guiar provider, language services, synthetic frontend e testes arquiteturais.
|
||||
|
||||
## Estrategia de implementacao
|
||||
|
||||
Transformar o alinhamento em criterios de review: interface pequena, registro explicito, DI/composicao, records imutaveis e testes; rejeitar infra que nao seja exigida pelo PBS ou pelo frontend sintetico de teste.
|
||||
|
||||
## Testes necessarios
|
||||
|
||||
- Nao ha teste direto para simplicidade, mas plans devem incluir criterio de aceite contra reflection/scanning/RPC quando aplicavel.
|
||||
- Testes arquiteturais podem proteger contra service locator global ou acoplamentos indevidos.
|
||||
|
||||
## Criterios de aceitacao
|
||||
|
||||
- Plans derivados nao introduzem infraestrutura de plugin antes da necessidade.
|
||||
- Registro manual do PBS e suficiente.
|
||||
- O frontend sintetico prova neutralidade sem virar plataforma externa.
|
||||
|
||||
## Riscos
|
||||
|
||||
- Usar "evitar abstracao" como desculpa para manter acoplamento PBS.
|
||||
- Criar contratos tao pequenos que precisem ser quebrados imediatamente.
|
||||
|
||||
## Decisoes que devem ser registradas
|
||||
|
||||
- Guardrails obrigatorios para esta fase.
|
||||
- Lista de mecanismos proibidos.
|
||||
- Criterio para aceitar uma nova abstracao.
|
||||
|
||||
@ -210,4 +210,4 @@ This specification MUST NOT be read as authorizing:
|
||||
- extracting PBS linking;
|
||||
- or reopening lifecycle-assembly ownership.
|
||||
|
||||
Platform validators MUST NOT import `p.studio.compiler.pbs` and MUST NOT discover roles from `__pbs.` prefixes.
|
||||
Platform validators MUST NOT depend on types from the PBS frontend module (`prometeu-frontend-pbs`). The package `p.studio.compiler.pbs` is not the complete boundary; facades shipped by that module remain PBS-owned. Platform validators MUST NOT discover roles from `__pbs.` prefixes.
|
||||
|
||||
@ -92,7 +92,7 @@ The declaration MUST represent, using ordered collections and typed ids:
|
||||
|
||||
References MUST use `ModuleId`, `FileId`, `CallableId`, a typed role, and source attribution. The public shape MUST NOT expose maps, sets, callbacks, compiler services, or frontend-owned types.
|
||||
|
||||
`AssembleLifecyclePipelineStage` MUST run after `FrontendPhasePipelineStage` and before `LowerToIRVMPipelineStage`. `LifecycleAssemblerService` is the only owner of derived lifecycle artifacts: module init, project-init wrapper, published frame wrapper, physical entrypoint, and hidden boot guard. Those artifacts MUST use deterministic neutral internal identities. Common code MUST NOT discover roles from `__pbs.*` prefixes or depend on PBS packages.
|
||||
`AssembleLifecyclePipelineStage` MUST run after `FrontendPhasePipelineStage` and before `LowerToIRVMPipelineStage`. `LifecycleAssemblerService` is the only owner of derived lifecycle artifacts: module init, project-init wrapper, published frame wrapper, physical entrypoint, and hidden boot guard. Those artifacts MUST use deterministic neutral internal identities. Common code MUST NOT discover roles from `__pbs.*` prefixes or depend on types from the PBS frontend module (`prometeu-frontend-pbs`). The package `p.studio.compiler.pbs` is not the complete boundary.
|
||||
|
||||
The declaration MUST remain available on the assembled `IRBackend` as typed provenance. Downstream inspection MUST use roles, ids, and origins rather than synthetic names.
|
||||
|
||||
@ -104,7 +104,7 @@ The common backend contract MUST NOT expose or require:
|
||||
2. PBS parser cursors, parse contexts, parse nodes, or token kinds,
|
||||
3. PBS semantic validator internals,
|
||||
4. PBS editorial or language-service objects,
|
||||
5. or any type under a language frontend package as part of the public backend handoff.
|
||||
5. or any type from the PBS frontend module (`prometeu-frontend-pbs`) or an equivalent language frontend module as part of the public backend handoff.
|
||||
|
||||
## 4.2 Serializable-by-Design Contract Shape
|
||||
|
||||
|
||||
@ -45,17 +45,17 @@ to concrete positive/negative test evidence and current status.
|
||||
| G19-11.3 | Platform diagnostics MUST use `COMMON_*` / `LOWER_IRVM_*` / `MARSHAL_*` and MUST copy origin/span when present. | `ValidationOwnershipSpecTest#spec19MustBeCompilerGeneralAndPublishValidationOwnership`; `LifecycleAssemblerServiceTest#assembleMustRejectMissingFrameRootWithoutRepair` | N/A | pass | Code families and span policy are normative in spec 19 section 11.1. |
|
||||
| G19-11.4 | The same fact MAY exist as a language diagnostic and a platform diagnostic with distinct identities. | `ValidationOwnershipSpecTest#spec19MustBeCompilerGeneralAndPublishValidationOwnership` | N/A | pass | Dual reporting is required when a check reads both source and common contract. |
|
||||
| G19-11.5 | Host admission MUST split source attribute shape from canonical `IRReservedMetadata` identity without moving the PBS validator in this plan. | `ValidationOwnershipSpecTest#spec19MustBeCompilerGeneralAndPublishValidationOwnership` | N/A | pass | Classified as platform; validator remains in the PBS package. |
|
||||
| G19-11.6 | Platform validators MUST NOT import PBS packages or match `__pbs.` prefixes. | `CommonBackendArchitectureTest#commonBackendMustNotImportPbsFrontendPackages`; `CommonBackendArchitectureTest#commonLifecycleAndBackendMustNotDiscoverRolesFromPbsNamePrefixes` | N/A | pass | Guard covers backend, lifecycle, and all common pipeline stages. |
|
||||
| G19-11.6 | Platform validators MUST NOT depend on PBS frontend module types or match `__pbs.` prefixes. | `PbsModuleBoundaryArchTest#productionOutsideAllowlistMustNotDependOnPbsModuleTypes`; `CommonBackendPbsPrefixScanTest#commonLifecycleAndBackendMustNotDiscoverRolesFromPbsNamePrefixes` | N/A | pass | ArchUnit covers PBS module origin, including facades outside `p.studio.compiler.pbs`; source scan still rejects `__pbs.` in backend, lifecycle, and common pipeline stages. |
|
||||
| PBS12-11 | PBS diagnostics MUST NOT claim common structural lifecycle/IRVM failures as PBS required coverage. | `ValidationOwnershipSpecTest#pbsDiagnosticsMustNotClaimCommonStructuralFailures` | N/A | pass | PBS spec 12 points those failures at spec 19 section 11 and spec 20. |
|
||||
| G20-4.1.1 | `IRBackend` MUST be the common frontend-to-backend executable handoff. | `LowerToIRVMServiceTest#lowerMustAcceptManuallyConstructedCommonIRBackend` | N/A | pass | Direct backend lowering from manually constructed common IRBackend proves PBS parser/frontend services are not required. |
|
||||
| G20-4.1.2 | Common backend code MUST NOT depend on `p.studio.compiler.pbs`. | `CommonBackendArchitectureTest#commonBackendMustNotImportPbsFrontendPackages` | N/A | pass | Source-level architectural guard rejects PBS frontend imports in common backend packages. |
|
||||
| G20-4.1.2 | Common backend code MUST NOT depend on types from the PBS frontend module. | `PbsModuleBoundaryArchTest#productionOutsideAllowlistMustNotDependOnPbsModuleTypes`; `PbsModuleBoundaryArchTest#irBackendModelsMustNotDependOnPbsModuleTypes` | N/A | pass | ArchUnit rejects PBS-module origin types in common production code, including facades outside `p.studio.compiler.pbs`. |
|
||||
| G20-4.1.3 | Public `IRBackend` contract MUST NOT expose PBS AST, token, parser, semantic, or editorial types. | `IRBackendExecutableContractTest#publicIRBackendContractMustNotExposePbsTypes` | N/A | pass | Reflection guard covers public contract fields, constructors, methods, and record components. |
|
||||
| G20-4.2.1 | Public `IRBackend` contract types MUST remain modelable as an acyclic, deterministic data graph. | `IRBackendExecutableContractTest#publicIRBackendContractMustExposeOnlySerializableShapes`; `IRBackendExecutableContractTest#aggregatorMustEmitDeterministicOrderedPublicContractShape` | N/A | pass | Covers public shape guardrails, generic type arguments, ordered aggregate emission, table remapping, and capability de-duplication order. |
|
||||
| G20-4.2.2 | Public `IRBackend` contract types MUST NOT expose callbacks, services, mutable/unordered collection contracts, frontend-owned objects, process-dependent lazy values, cyclic public references, or wire-format commitments. | `IRBackendExecutableContractTest#publicIRBackendContractMustExposeOnlySerializableShapes` | N/A | pass | Reflection guard covers public contract fields, constructors, methods, record components, and generic type arguments. |
|
||||
| G20-4.2.3 | Public `IRBackend` cross-object references MUST use explicit ids or stable symbolic keys. | `IRBackendExecutableContractTest#publicIRBackendContractAuditMustClassifyAllowedSupportTypes`; `IRBackendExecutableContractTest#publicIRBackendContractMustUseExplicitIdsOrStableKeysForKnownReferences` | N/A | pass | Current audit found no concrete public serialization leak requiring model correction. Existing `FileId`, `ModuleId`, `CallableId`, and `IntrinsicId` remain explicit table-scoped identifiers. |
|
||||
| G20-4.3.1 | Executable `IRBackend` MUST carry a typed, ordered, id-based `IRLifecycleDeclaration` as provenance. | `IRBackendExecutableContractTest#aggregatorMustRemapTypedLifecycleReferencesAndPreserveDeclaredModuleOrder`; `LifecycleAssemblerServiceTest#assembleMustMaterializeNeutralLifecycleArtifactsInDeclaredOrder` | `IRBackendExecutableContractTest#aggregatorMustOmitLifecycleDeclarationWhenNoFileContributesOne` | pass | Declaration uses `ReadOnlyList`, typed ids, roles, and source attribution; aggregator remaps ids with explicit module order. |
|
||||
| G20-4.3.2 | Missing lifecycle declaration MUST mean a deliberately non-executable handoff and MUST NOT synthesize entrypoints. | `LifecycleAssemblerServiceTest#assembleMustNoOpWhenLifecycleDeclarationIsAbsent`; `AssembleLifecyclePipelineStageTest#runMustNoOpForDeliberatelyNonExecutableHandoff` | N/A | pass | Assembler and stage leave the backend unchanged when the declaration is absent. |
|
||||
| G20-4.3.3 | Common lifecycle assembly MUST be the exclusive owner of derived artifacts and MUST use neutral identities. | `LifecycleAssemblerServiceTest#assembleMustMaterializeNeutralLifecycleArtifactsInDeclaredOrder`; `CommonBackendArchitectureTest#commonLifecycleAndBackendMustNotDiscoverRolesFromPbsNamePrefixes` | `LifecycleAssemblerServiceTest#assembleMustRejectPreMaterializedDerivedArtifacts` | pass | Derived wrappers and boot guard are common-owned; architecture guard rejects PBS imports and `__pbs.` prefix matching. |
|
||||
| G20-4.3.3 | Common lifecycle assembly MUST be the exclusive owner of derived artifacts and MUST use neutral identities. | `LifecycleAssemblerServiceTest#assembleMustMaterializeNeutralLifecycleArtifactsInDeclaredOrder`; `CommonBackendPbsPrefixScanTest#commonLifecycleAndBackendMustNotDiscoverRolesFromPbsNamePrefixes`; `PbsModuleBoundaryArchTest#productionOutsideAllowlistMustNotDependOnPbsModuleTypes` | `LifecycleAssemblerServiceTest#assembleMustRejectPreMaterializedDerivedArtifacts` | pass | Derived wrappers and boot guard are common-owned; ArchUnit rejects PBS-module types and the source scan rejects `__pbs.` prefix matching. |
|
||||
| G20-6.2 | `IRVM_EXT` MUST declare structural metadata (`pops/pushes/is_branch/is_terminator`). | `IRVMValidatorTest#validateMustApplyStructuralMetadataForCustomInternalExtension`; `IRVMValidatorTest#validateMustRejectCustomInternalExtensionWhenStructuralMetadataUnderflowsStack`; `IRVMOp` record contract (`pops/pushes/branch/terminator/internal`) | N/A | pass | Dedicated extension fixtures now assert structural metadata is consumed by validation behavior. |
|
||||
| G20-6.3 | `IRVM_EXT` MUST be eliminable before bytecode emission. | `OptimizeIRVMServiceTest#optimizeDefaultPassesMustEliminateUnreachableInternalExtensionBeforeEmission` | `EmitBytecodePipelineStageTest#runMustFailWhenInternalOpcodesRemain`; `EmitBytecodePipelineStageTest#runMustFailWhenInternalOpcodesRemainEvenWithNonEmptyEmissionPlan`; `IRVMValidatorTest#validateMustRejectInternalOpcodeWhenConfigured` | pass | Optimizer elimination path and emit-stage hard rejection path are both covered. |
|
||||
| G20-6.4 | IRVM MUST preserve per-function slot and identity headers. | `IRVMProgramTest#constructorMustRejectModuleAndEmissionPlanMismatch` | `IRVMProgramTest#constructorMustRejectModuleAndEmissionPlanMismatch` | pass | Header mismatch is rejected deterministically. |
|
||||
|
||||
@ -5,6 +5,7 @@ jackson = "2.18.2"
|
||||
jacoco = "0.8.12"
|
||||
lombok = "1.18.32"
|
||||
junit-jupiter = "5.12.1"
|
||||
archunit = "1.4.2"
|
||||
slf4j = "2.0.7"
|
||||
lsp4j = "1.0.0"
|
||||
|
||||
@ -19,6 +20,7 @@ apache-commons-collections = { group = "org.apache.commons", name = "commons-col
|
||||
jacoco-core = { group = "org.jacoco", name = "org.jacoco.core", version.ref = "jacoco" }
|
||||
lombok = { group = "org.projectlombok", name = "lombok", version.ref = "lombok" }
|
||||
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" }
|
||||
archunit-junit5 = { group = "com.tngtech.archunit", name = "archunit-junit5", version.ref = "archunit" }
|
||||
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
|
||||
lsp4j = { group = "org.eclipse.lsp4j", name = "org.eclipse.lsp4j", version.ref = "lsp4j" }
|
||||
|
||||
@ -7,7 +7,6 @@ dependencies {
|
||||
implementation(project(":prometeu-infra"))
|
||||
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
|
||||
implementation(project(":prometeu-lsp:prometeu-lsp-v1"))
|
||||
implementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs"))
|
||||
implementation(project(":prometeu-compiler:prometeu-frontend-registry"))
|
||||
implementation(project(":prometeu-packer:prometeu-packer-api"))
|
||||
implementation(project(":prometeu-packer:prometeu-packer-v1"))
|
||||
|
||||
23
prometeu-architecture-tests/build.gradle.kts
Normal file
23
prometeu-architecture-tests/build.gradle.kts
Normal file
@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
id("gradle.java-library-conventions")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(libs.archunit.junit5)
|
||||
|
||||
testImplementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs"))
|
||||
testImplementation(project(":prometeu-compiler:prometeu-frontend-registry"))
|
||||
testImplementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
||||
testImplementation(project(":prometeu-compiler:prometeu-frontend-api"))
|
||||
testImplementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
||||
testImplementation(project(":prometeu-compiler:prometeu-deps"))
|
||||
testImplementation(project(":prometeu-infra"))
|
||||
testImplementation(project(":prometeu-lsp:prometeu-lsp-api"))
|
||||
testImplementation(project(":prometeu-lsp:prometeu-lsp-v1"))
|
||||
testImplementation(project(":prometeu-studio"))
|
||||
testImplementation(project(":prometeu-app"))
|
||||
}
|
||||
|
||||
tasks.named<JacocoReport>("jacocoTestReport") {
|
||||
enabled = false
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package p.studio.architecture;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class CommonBackendPbsPrefixScanTest {
|
||||
private static final String COMPILER_SOURCE_ROOT =
|
||||
"prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler";
|
||||
private static final List<String> GUARDED_RELATIVE_ROOTS = List.of(
|
||||
"backend",
|
||||
"lifecycle",
|
||||
"workspaces/stages");
|
||||
private static final String FORBIDDEN_PBS_PREFIX = "__pbs.";
|
||||
|
||||
@Test
|
||||
void commonLifecycleAndBackendMustNotDiscoverRolesFromPbsNamePrefixes() throws IOException {
|
||||
final var violations = new ArrayList<String>();
|
||||
final var repoRoot = locateRepoRoot();
|
||||
final var compilerRoot = repoRoot.resolve(COMPILER_SOURCE_ROOT);
|
||||
for (final var relativeRoot : GUARDED_RELATIVE_ROOTS) {
|
||||
final var root = compilerRoot.resolve(relativeRoot);
|
||||
if (!Files.exists(root)) {
|
||||
fail("guarded source root is missing: " + root);
|
||||
}
|
||||
try (final var paths = Files.walk(root)) {
|
||||
paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> {
|
||||
try {
|
||||
if (Files.readString(path).contains(FORBIDDEN_PBS_PREFIX)) {
|
||||
violations.add(repoRoot.relativize(path).toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("failed to read source file: " + path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
assertTrue(
|
||||
violations.isEmpty(),
|
||||
"common lifecycle/backend code must not inspect __pbs. prefixes: " + violations);
|
||||
}
|
||||
|
||||
private static Path locateRepoRoot() {
|
||||
var cursor = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
while (cursor != null) {
|
||||
final var hasDocs = Files.isDirectory(cursor.resolve("docs/specs/compiler"));
|
||||
final var hasCompiler = Files.isDirectory(cursor.resolve("prometeu-compiler"));
|
||||
if (hasDocs && hasCompiler) {
|
||||
return cursor;
|
||||
}
|
||||
cursor = cursor.getParent();
|
||||
}
|
||||
fail("could not locate repository root from working directory");
|
||||
throw new IllegalStateException("unreachable");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.architecture;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
final class DescribedPredicates {
|
||||
private DescribedPredicates() {
|
||||
}
|
||||
|
||||
static DescribedPredicate<JavaClass> notBelongingTo(final Class<?>... allowed) {
|
||||
final Set<String> allowedNames = Arrays.stream(allowed)
|
||||
.map(Class::getName)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
return DescribedPredicate.describe(
|
||||
"do not belong to " + allowedNames,
|
||||
javaClass -> !allowedNames.contains(javaClass.getName()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package p.studio.architecture;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class PbsGradleDependencyAllowlistTest {
|
||||
private static final Pattern PBS_PROJECT_DEP = Pattern.compile(
|
||||
"(\\w+)\\(\\s*project\\(\\s*\"" + Pattern.quote(PbsModuleOrigins.PBS_GRADLE_PATH) + "\"\\s*\\)");
|
||||
private static final Map<String, Set<String>> ALLOWED_CONFIGURATIONS = Map.of(
|
||||
":prometeu-compiler:prometeu-frontend-registry", Set.of("implementation", "api"),
|
||||
":prometeu-architecture-tests", Set.of("testImplementation"));
|
||||
|
||||
@Test
|
||||
void onlyAllowlistedProjectsMayDependOnThePbsFrontendModule() throws IOException {
|
||||
final var repoRoot = locateRepoRoot();
|
||||
final var violations = new ArrayList<String>();
|
||||
|
||||
try (final var paths = Files.walk(repoRoot)) {
|
||||
paths.filter(path -> path.getFileName().toString().equals("build.gradle.kts"))
|
||||
.filter(path -> !path.toString().contains("/buildSrc/"))
|
||||
.filter(path -> !path.toString().contains("/build/"))
|
||||
.forEach(path -> collectViolations(repoRoot, path, violations));
|
||||
}
|
||||
|
||||
assertTrue(violations.isEmpty(), "PBS Gradle project dependency allowlist violated: " + violations);
|
||||
}
|
||||
|
||||
private static void collectViolations(
|
||||
final Path repoRoot,
|
||||
final Path buildFile,
|
||||
final ArrayList<String> violations) {
|
||||
final var projectPath = toProjectPath(repoRoot, buildFile);
|
||||
if (PbsModuleOrigins.PBS_GRADLE_PATH.equals(projectPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String content;
|
||||
try {
|
||||
content = Files.readString(buildFile);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("failed to read " + buildFile, e);
|
||||
}
|
||||
|
||||
final var matcher = PBS_PROJECT_DEP.matcher(content);
|
||||
while (matcher.find()) {
|
||||
final var configuration = matcher.group(1);
|
||||
final var allowed = ALLOWED_CONFIGURATIONS.getOrDefault(projectPath, Set.of());
|
||||
if (!allowed.contains(configuration)) {
|
||||
violations.add(projectPath + " " + configuration + " " + PbsModuleOrigins.PBS_GRADLE_PATH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String toProjectPath(final Path repoRoot, final Path buildFile) {
|
||||
final var relative = repoRoot.relativize(buildFile.getParent()).toString().replace('\\', '/');
|
||||
if (relative.isEmpty() || ".".equals(relative)) {
|
||||
return ":";
|
||||
}
|
||||
return ":" + relative.replace('/', ':');
|
||||
}
|
||||
|
||||
private static Path locateRepoRoot() {
|
||||
var cursor = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
while (cursor != null) {
|
||||
final var hasDocs = Files.isDirectory(cursor.resolve("docs/specs/compiler"));
|
||||
final var hasCompiler = Files.isDirectory(cursor.resolve("prometeu-compiler"));
|
||||
if (hasDocs && hasCompiler) {
|
||||
return cursor;
|
||||
}
|
||||
cursor = cursor.getParent();
|
||||
}
|
||||
fail("could not locate repository root from working directory");
|
||||
throw new IllegalStateException("unreachable");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package p.studio.architecture;
|
||||
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import p.studio.compiler.PBSDefinitions;
|
||||
import p.studio.compiler.PBSFrontendProvider;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
import static p.studio.architecture.PbsModuleOrigins.fromFrontendApiModule;
|
||||
import static p.studio.architecture.PbsModuleOrigins.fromPbsModule;
|
||||
import static p.studio.architecture.PbsModuleOrigins.fromRegistryModule;
|
||||
|
||||
@AnalyzeClasses(packages = "p.studio", importOptions = ImportOption.DoNotIncludeTests.class)
|
||||
class PbsModuleBoundaryArchTest {
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule productionOutsideAllowlistMustNotDependOnPbsModuleTypes = noClasses()
|
||||
.that(fromPbsModule().negate())
|
||||
.and(fromRegistryModule().negate())
|
||||
.should()
|
||||
.dependOnClassesThat(fromPbsModule())
|
||||
.because("DEC-0050: only prometeu-frontend-pbs and prometeu-frontend-registry may depend on PBS module types");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule registryMustOnlyDependOnPbsProviderFacade = noClasses()
|
||||
.that(fromRegistryModule())
|
||||
.should()
|
||||
.dependOnClassesThat(
|
||||
fromPbsModule()
|
||||
.and(DescribedPredicates.notBelongingTo(PBSFrontendProvider.class, PBSDefinitions.class)))
|
||||
.because("DEC-0050: registry may depend only on PBSFrontendProvider and PBSDefinitions");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule irBackendModelsMustNotDependOnPbsModuleTypes = noClasses()
|
||||
.that(fromFrontendApiModule())
|
||||
.and()
|
||||
.resideInAPackage("p.studio.compiler.models..")
|
||||
.should()
|
||||
.dependOnClassesThat(fromPbsModule())
|
||||
.because("DEC-0050: public IRBackend contract types must not depend on PBS module types");
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package p.studio.architecture;
|
||||
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class PbsModuleClasspathTest {
|
||||
|
||||
@Test
|
||||
void importerMustSeePbsAndRegistryModuleOrigins() {
|
||||
final var classes = new ClassFileImporter()
|
||||
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
|
||||
.importPackages("p.studio");
|
||||
|
||||
final var pbsSample = classes.stream()
|
||||
.filter(PbsModuleOrigins::isPbsModule)
|
||||
.findFirst();
|
||||
final var registrySample = classes.stream()
|
||||
.filter(PbsModuleOrigins::isRegistryModule)
|
||||
.findFirst();
|
||||
|
||||
if (pbsSample.isEmpty() || registrySample.isEmpty()) {
|
||||
final var uris = classes.stream()
|
||||
.limit(20)
|
||||
.map(javaClass -> javaClass.getName() + " -> " + PbsModuleOrigins.sourceUri(javaClass))
|
||||
.toList();
|
||||
fail("PBS/registry module origins were not visible to ArchUnit. sample URIs: " + uris);
|
||||
}
|
||||
|
||||
assertTrue(pbsSample.isPresent());
|
||||
assertTrue(registrySample.isPresent());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package p.studio.architecture;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
|
||||
final class PbsModuleOrigins {
|
||||
static final String PBS_GRADLE_PATH = ":prometeu-compiler:frontends:prometeu-frontend-pbs";
|
||||
|
||||
private PbsModuleOrigins() {
|
||||
}
|
||||
|
||||
static boolean isPbsModule(final JavaClass javaClass) {
|
||||
return uriContains(javaClass, "/prometeu-frontend-pbs/")
|
||||
|| uriContains(javaClass, "prometeu-frontend-pbs.jar");
|
||||
}
|
||||
|
||||
static boolean isRegistryModule(final JavaClass javaClass) {
|
||||
return uriContains(javaClass, "/prometeu-frontend-registry/")
|
||||
|| uriContains(javaClass, "prometeu-frontend-registry.jar");
|
||||
}
|
||||
|
||||
static boolean isFrontendApiModule(final JavaClass javaClass) {
|
||||
return uriContains(javaClass, "/prometeu-frontend-api/")
|
||||
|| uriContains(javaClass, "prometeu-frontend-api.jar");
|
||||
}
|
||||
|
||||
static DescribedPredicate<JavaClass> fromPbsModule() {
|
||||
return DescribedPredicate.describe(
|
||||
"originate from the PBS frontend module",
|
||||
PbsModuleOrigins::isPbsModule);
|
||||
}
|
||||
|
||||
static DescribedPredicate<JavaClass> fromRegistryModule() {
|
||||
return DescribedPredicate.describe(
|
||||
"originate from the frontend registry module",
|
||||
PbsModuleOrigins::isRegistryModule);
|
||||
}
|
||||
|
||||
static DescribedPredicate<JavaClass> fromFrontendApiModule() {
|
||||
return DescribedPredicate.describe(
|
||||
"originate from the frontend-api module",
|
||||
PbsModuleOrigins::isFrontendApiModule);
|
||||
}
|
||||
|
||||
static String sourceUri(final JavaClass javaClass) {
|
||||
return javaClass.getSource()
|
||||
.map(source -> normalize(source.getUri().toString()))
|
||||
.orElse("<no source>");
|
||||
}
|
||||
|
||||
private static boolean uriContains(final JavaClass javaClass, final String fragment) {
|
||||
return javaClass.getSource()
|
||||
.map(source -> normalize(source.getUri().toString()).contains(fragment))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private static String normalize(final String uri) {
|
||||
return uri.replace('\\', '/');
|
||||
}
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
package p.studio.compiler.specs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class CommonBackendArchitectureTest {
|
||||
private static final String COMPILER_SOURCE_ROOT =
|
||||
"prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler";
|
||||
private static final List<String> GUARDED_RELATIVE_ROOTS = List.of(
|
||||
"backend",
|
||||
"lifecycle",
|
||||
"workspaces/stages");
|
||||
private static final String FORBIDDEN_PBS_PACKAGE = "p.studio.compiler.pbs";
|
||||
private static final String FORBIDDEN_PBS_PREFIX = "__pbs.";
|
||||
|
||||
@Test
|
||||
void commonBackendMustNotImportPbsFrontendPackages() throws IOException {
|
||||
final var violations = new ArrayList<String>();
|
||||
walkGuardedSources(path -> assertNoPbsImport(path, violations));
|
||||
assertTrue(
|
||||
violations.isEmpty(),
|
||||
"common backend must not import PBS frontend packages: " + violations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void commonLifecycleAndBackendMustNotDiscoverRolesFromPbsNamePrefixes() throws IOException {
|
||||
final var violations = new ArrayList<String>();
|
||||
walkGuardedSources(path -> assertNoPbsPrefixMatching(path, violations));
|
||||
assertTrue(
|
||||
violations.isEmpty(),
|
||||
"common lifecycle/backend code must not inspect __pbs. prefixes: " + violations);
|
||||
}
|
||||
|
||||
private static void walkGuardedSources(final java.util.function.Consumer<Path> consumer) throws IOException {
|
||||
final var compilerRoot = locateRepoRoot().resolve(COMPILER_SOURCE_ROOT);
|
||||
for (final var relativeRoot : GUARDED_RELATIVE_ROOTS) {
|
||||
final var root = compilerRoot.resolve(relativeRoot);
|
||||
if (Files.isRegularFile(root)) {
|
||||
consumer.accept(root);
|
||||
continue;
|
||||
}
|
||||
try (final var paths = Files.walk(root)) {
|
||||
paths.filter(path -> path.toString().endsWith(".java")).forEach(consumer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertNoPbsImport(
|
||||
final Path path,
|
||||
final ArrayList<String> violations) {
|
||||
try {
|
||||
final var content = Files.readString(path);
|
||||
if (content.contains("import " + FORBIDDEN_PBS_PACKAGE)
|
||||
|| content.contains("import static " + FORBIDDEN_PBS_PACKAGE)) {
|
||||
violations.add(locateRepoRoot().relativize(path).toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("failed to read source file: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertNoPbsPrefixMatching(
|
||||
final Path path,
|
||||
final ArrayList<String> violations) {
|
||||
try {
|
||||
final var content = Files.readString(path);
|
||||
if (content.contains(FORBIDDEN_PBS_PREFIX)) {
|
||||
violations.add(locateRepoRoot().relativize(path).toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("failed to read source file: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path locateRepoRoot() {
|
||||
var cursor = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
while (cursor != null) {
|
||||
final var hasDocs = Files.isDirectory(cursor.resolve("docs/specs/compiler"));
|
||||
final var hasCompiler = Files.isDirectory(cursor.resolve("prometeu-compiler"));
|
||||
if (hasDocs && hasCompiler) {
|
||||
return cursor;
|
||||
}
|
||||
cursor = cursor.getParent();
|
||||
}
|
||||
fail("could not locate repository root from working directory");
|
||||
throw new IllegalStateException("unreachable");
|
||||
}
|
||||
}
|
||||
@ -11,93 +11,11 @@ import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
import p.studio.utilities.structures.ReadOnlySet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class FrontendProviderBoundaryTest {
|
||||
|
||||
@Test
|
||||
void commonModulesMustNotInstantiatePbsFrontendPhaseServiceDirectly() throws IOException {
|
||||
final var repoRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize());
|
||||
final var commonSourceRoots = List.of(
|
||||
repoRoot.resolve("prometeu-compiler/prometeu-build-pipeline/src/main/java"),
|
||||
repoRoot.resolve("prometeu-compiler/prometeu-deps/src/main/java"),
|
||||
repoRoot.resolve("prometeu-studio/src/main/java"),
|
||||
repoRoot.resolve("prometeu-app/src/main/java"));
|
||||
|
||||
for (final var sourceRoot : commonSourceRoots) {
|
||||
if (!Files.exists(sourceRoot)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try (final var files = Files.walk(sourceRoot)) {
|
||||
final var violations = files
|
||||
.filter(path -> path.toString().endsWith(".java"))
|
||||
.filter(path -> contains(path, "new PBSFrontendPhaseService("))
|
||||
.toList();
|
||||
if (!violations.isEmpty()) {
|
||||
fail("common modules must resolve frontend compiler behavior through FrontendProvider: " + violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void commonModulesMustNotInstantiatePbsEditorialSupportServiceDirectly() throws IOException {
|
||||
final var repoRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize());
|
||||
final var sourceRoots = List.of(
|
||||
repoRoot.resolve("prometeu-compiler/prometeu-build-pipeline/src/main/java"),
|
||||
repoRoot.resolve("prometeu-compiler/prometeu-deps/src/main/java"),
|
||||
repoRoot.resolve("prometeu-lsp/prometeu-lsp-v1/src/main/java"),
|
||||
repoRoot.resolve("prometeu-studio/src/main/java"),
|
||||
repoRoot.resolve("prometeu-app/src/main/java"));
|
||||
|
||||
for (final var sourceRoot : sourceRoots) {
|
||||
if (!Files.exists(sourceRoot)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try (final var files = Files.walk(sourceRoot)) {
|
||||
final var violations = files
|
||||
.filter(path -> path.toString().endsWith(".java"))
|
||||
.filter(path -> contains(path, "new PbsEditorialSupportService("))
|
||||
.toList();
|
||||
if (!violations.isEmpty()) {
|
||||
fail("common modules must resolve PBS editor behavior through FrontendProvider.languageService(): " + violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void commonLspMustNotImportPbsImplementationPackages() throws IOException {
|
||||
final var repoRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize());
|
||||
final var sourceRoot = repoRoot.resolve("prometeu-lsp/prometeu-lsp-v1/src/main/java");
|
||||
final var prohibitedNeedles = List.of(
|
||||
"p.studio.compiler.PBSFrontendLanguageService",
|
||||
"p.studio.compiler.services.PBSFrontendPhaseService",
|
||||
"p.studio.compiler.pbs",
|
||||
"PbsAst",
|
||||
"PbsEditorial");
|
||||
|
||||
try (final var files = Files.walk(sourceRoot)) {
|
||||
final var violations = files
|
||||
.filter(path -> path.toString().endsWith(".java"))
|
||||
.filter(path -> prohibitedNeedles.stream().anyMatch(needle -> contains(path, needle)))
|
||||
.toList();
|
||||
if (!violations.isEmpty()) {
|
||||
fail("common LSP code must consume generic frontend language-service contracts instead of PBS implementation packages: "
|
||||
+ violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void compileOnlyFrontendProviderMayOmitLanguageService() {
|
||||
final FrontendProvider provider = new CompileOnlyFrontendProvider();
|
||||
@ -110,26 +28,6 @@ class FrontendProviderBoundaryTest {
|
||||
BuildingIssueSink.empty()));
|
||||
}
|
||||
|
||||
private boolean contains(final Path path, final String needle) {
|
||||
try {
|
||||
return Files.readString(path).contains(needle);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("failed to read " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path findRepoRoot(final Path start) {
|
||||
var current = start;
|
||||
while (current != null) {
|
||||
if (Files.exists(current.resolve("settings.gradle.kts"))) {
|
||||
return current;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
fail("unable to locate repository root from " + start);
|
||||
return start;
|
||||
}
|
||||
|
||||
private static final class CompileOnlyFrontendProvider implements FrontendProvider {
|
||||
private final FrontendSpec specification = FrontendSpec.builder()
|
||||
.languageId("compile-only")
|
||||
|
||||
@ -43,8 +43,11 @@ class ValidationOwnershipSpecTest {
|
||||
content.contains("Canonical host-binding identity"),
|
||||
"spec 19 matrix must include the host-admission split");
|
||||
assertTrue(
|
||||
content.contains("MUST NOT import `p.studio.compiler.pbs`"),
|
||||
"spec 19 must forbid PBS imports in platform validators");
|
||||
content.contains("MUST NOT depend on types from the PBS frontend module"),
|
||||
"spec 19 must forbid PBS frontend module types in platform validators");
|
||||
assertTrue(
|
||||
content.contains("`p.studio.compiler.pbs` is not the complete boundary"),
|
||||
"spec 19 must not treat package p.studio.compiler.pbs as the complete PBS boundary");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -7,7 +7,6 @@ dependencies {
|
||||
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
||||
implementation(project(":prometeu-compiler:prometeu-frontend-api"))
|
||||
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
||||
implementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs"))
|
||||
implementation(project(":prometeu-compiler:prometeu-frontend-registry"))
|
||||
implementation(project(":prometeu-compiler:prometeu-deps"))
|
||||
implementation(libs.lsp4j)
|
||||
|
||||
@ -14,7 +14,6 @@ dependencies {
|
||||
implementation(libs.javafx.controls)
|
||||
implementation(libs.javafx.fxml)
|
||||
implementation(libs.richtextfx)
|
||||
testImplementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs"))
|
||||
testImplementation(project(":prometeu-packer:prometeu-packer-v1"))
|
||||
}
|
||||
|
||||
|
||||
@ -22,3 +22,5 @@ include("prometeu-compiler:prometeu-frontend-registry")
|
||||
include("prometeu-studio")
|
||||
|
||||
include("prometeu-app")
|
||||
|
||||
include("prometeu-architecture-tests")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user