dev/multi-frontend-provider-contract #14

Merged
bquarkz merged 6 commits from dev/multi-frontend-provider-contract into master 2026-07-15 07:42:36 +00:00
11 changed files with 105 additions and 21 deletions
Showing only changes of commit 44f03f771b - Show all commits

View File

@ -10,7 +10,7 @@
{"type":"discussion","id":"DSC-0057","status":"open","ticket":"multi-frontend-frontend-backend-contract","title":"Estabilizar contrato entre frontend e backend comum","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","ir","backend","multi-frontend"],"agendas":[{"id":"AGD-0060","file":"AGD-0060-multi-frontend-frontend-backend-contract.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0057","status":"open","ticket":"multi-frontend-frontend-backend-contract","title":"Estabilizar contrato entre frontend e backend comum","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","ir","backend","multi-frontend"],"agendas":[{"id":"AGD-0060","file":"AGD-0060-multi-frontend-frontend-backend-contract.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0056","status":"open","ticket":"multi-frontend-remove-pbs-branches","title":"Remover verificacoes explicitas de PBS do codigo comum","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[{"id":"AGD-0059","file":"AGD-0059-multi-frontend-remove-pbs-branches.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0056","status":"open","ticket":"multi-frontend-remove-pbs-branches","title":"Remover verificacoes explicitas de PBS do codigo comum","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[{"id":"AGD-0059","file":"AGD-0059-multi-frontend-remove-pbs-branches.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0055","status":"open","ticket":"multi-frontend-compiler-vs-language-services","title":"Separar compilacao de servicos editoriais de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","lsp","editor","frontend","multi-frontend"],"agendas":[{"id":"AGD-0058","file":"AGD-0058-multi-frontend-compiler-vs-language-services.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0055","status":"open","ticket":"multi-frontend-compiler-vs-language-services","title":"Separar compilacao de servicos editoriais de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","lsp","editor","frontend","multi-frontend"],"agendas":[{"id":"AGD-0058","file":"AGD-0058-multi-frontend-compiler-vs-language-services.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0054","status":"in_progress","ticket":"multi-frontend-provider-contract","title":"Introduzir provider completo de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","registry","multi-frontend"],"agendas":[{"id":"AGD-0057","file":"AGD-0057-multi-frontend-provider-contract.md","status":"accepted","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-frontendprovider-explicito-como-contrato-de-frontend.md","status":"accepted","created_at":"2026-07-15","updated_at":"2026-07-15","ref_agenda":"AGD-0057"}],"plans":[{"id":"PLN-0103","file":"PLN-0103-specify-frontend-provider-and-registry-contract.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0040"]},{"id":"PLN-0104","file":"PLN-0104-introduce-frontendprovider-registry-api.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0040"]},{"id":"PLN-0105","file":"PLN-0105-wrap-pbs-frontend-as-explicit-provider.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0040"]},{"id":"PLN-0106","file":"PLN-0106-migrate-common-consumers-to-provider-lookup.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0040"]}],"lessons":[]} {"type":"discussion","id":"DSC-0054","status":"in_progress","ticket":"multi-frontend-provider-contract","title":"Introduzir provider completo de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","registry","multi-frontend"],"agendas":[{"id":"AGD-0057","file":"AGD-0057-multi-frontend-provider-contract.md","status":"accepted","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-frontendprovider-explicito-como-contrato-de-frontend.md","status":"accepted","created_at":"2026-07-15","updated_at":"2026-07-15","ref_agenda":"AGD-0057"}],"plans":[{"id":"PLN-0103","file":"PLN-0103-specify-frontend-provider-and-registry-contract.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0040"]},{"id":"PLN-0104","file":"PLN-0104-introduce-frontendprovider-registry-api.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0040"]},{"id":"PLN-0105","file":"PLN-0105-wrap-pbs-frontend-as-explicit-provider.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0040"]},{"id":"PLN-0106","file":"PLN-0106-migrate-common-consumers-to-provider-lookup.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0040"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0053","status":"open","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[{"id":"AGD-0056","file":"AGD-0056-pbs-lsp-call-and-type-hierarchy.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0053","status":"open","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[{"id":"AGD-0056","file":"AGD-0056-pbs-lsp-call-and-type-hierarchy.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0052","status":"open","ticket":"pbs-lsp-document-links","title":"PBS LSP Document Links","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","document-links","imports"],"agendas":[{"id":"AGD-0055","file":"AGD-0055-pbs-lsp-document-links.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0052","status":"open","ticket":"pbs-lsp-document-links","title":"PBS LSP Document Links","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","document-links","imports"],"agendas":[{"id":"AGD-0055","file":"AGD-0055-pbs-lsp-document-links.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0051","status":"open","ticket":"pbs-lsp-diagnostics-ux","title":"PBS LSP Diagnostics UX","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","diagnostics","ux"],"agendas":[{"id":"AGD-0054","file":"AGD-0054-pbs-lsp-diagnostics-ux.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0051","status":"open","ticket":"pbs-lsp-diagnostics-ux","title":"PBS LSP Diagnostics UX","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","diagnostics","ux"],"agendas":[{"id":"AGD-0054","file":"AGD-0054-pbs-lsp-diagnostics-ux.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}

View File

@ -2,7 +2,7 @@
id: PLN-0106 id: PLN-0106
ticket: multi-frontend-provider-contract ticket: multi-frontend-provider-contract
title: Migrate common consumers to provider lookup title: Migrate common consumers to provider lookup
status: open status: done
created: 2026-07-15 created: 2026-07-15
ref_decisions: [DEC-0040] ref_decisions: [DEC-0040]
tags: [compiler, compiler-general, studio, frontend, registry, multi-frontend] tags: [compiler, compiler-general, studio, frontend, registry, multi-frontend]

View File

@ -17,13 +17,7 @@ public class FrontendPhasePipelineStage implements PipelineStage {
@Override @Override
public BuildingIssueSink run(final BuilderPipelineContext ctx, final LogAggregator logs) { public BuildingIssueSink run(final BuilderPipelineContext ctx, final LogAggregator logs) {
final var frontedSpec = ctx.resolvedWorkspace.frontendSpec(); final var frontedSpec = ctx.resolvedWorkspace.frontendSpec();
final var service = FrontendRegistryService.getFrontendPhaseService(frontedSpec.getLanguageId()); final var provider = FrontendRegistryService.require(frontedSpec.getLanguageId());
if (service.isEmpty()) {
return BuildingIssueSink.empty()
.report(builder -> builder
.error(true)
.message("[BUILD]: unable to find a service for frontend phase: " + frontedSpec.getLanguageId()));
}
final var projectTable = ctx.resolvedWorkspace.graph().projectTable(); final var projectTable = ctx.resolvedWorkspace.graph().projectTable();
final var fileTable = ctx.fileTable; final var fileTable = ctx.fileTable;
final var frontendPhaseContext = new FrontendPhaseContext( final var frontendPhaseContext = new FrontendPhaseContext(
@ -36,7 +30,7 @@ public class FrontendPhasePipelineStage implements PipelineStage {
assetSurfaceContextLoader.load(ctx.resolvedWorkspace.mainProject().getRootPath())); assetSurfaceContextLoader.load(ctx.resolvedWorkspace.mainProject().getRootPath()));
final var diagnostics = DiagnosticSink.empty(); final var diagnostics = DiagnosticSink.empty();
final var issues = BuildingIssueSink.empty(); final var issues = BuildingIssueSink.empty();
ctx.irBackend = service.get().compile(frontendPhaseContext, diagnostics, logs, issues); ctx.irBackend = provider.compiler().compile(frontendPhaseContext, diagnostics, logs, issues);
logs.using(log).debug("Frontend phase completed successfully!"); logs.using(log).debug("Frontend phase completed successfully!");
adaptDiagnostics(diagnostics, issues); adaptDiagnostics(diagnostics, issues);
return issues; return issues;

View File

@ -0,0 +1,59 @@
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.List;
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);
}
}
}
}
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;
}
}

View File

@ -60,7 +60,7 @@ public final class PrometeuManifestUtils {
.message("[DEPS]: invalid 'target' \"" + dto.target() + "\": " + manifestPathCanon)); .message("[DEPS]: invalid 'target' \"" + dto.target() + "\": " + manifestPathCanon));
} }
final var language = StringUtils.isBlank(dto.language()) final var language = StringUtils.isBlank(dto.language())
? FrontendRegistryService.getDefaultFrontendSpec().getLanguageId() ? FrontendRegistryService.getDefaultProvider().specification().getLanguageId()
: dto.language(); : dto.language();
final var dependencies = DependencyResolver.resolveDependencies(mainProjectRootPathCanon, dto.dependencies(), issuesLocal); final var dependencies = DependencyResolver.resolveDependencies(mainProjectRootPathCanon, dto.dependencies(), issuesLocal);
if (ReadOnlyCollection.isNotEmpty(issuesLocal)) { if (ReadOnlyCollection.isNotEmpty(issuesLocal)) {

View File

@ -55,23 +55,24 @@ public class DiscoverPhase implements DependencyPhase {
ctx.rootStdlib = manifest.get().stdlib(); ctx.rootStdlib = manifest.get().stdlib();
ctx.target = manifest.get().target(); ctx.target = manifest.get().target();
} }
final var frontendSpec = FrontendRegistryService.getFrontendSpec(manifest.get().language()); final var frontendProvider = FrontendRegistryService.getProvider(manifest.get().language());
// Returns empty when language is unknown // Returns empty when language is unknown
if (frontendSpec.isEmpty()) { if (frontendProvider.isEmpty()) {
issues.report(builder -> builder issues.report(builder -> builder
.error(true) .error(true)
.message("[DEPS]: unknown language " + manifest.get().language() + " for project " + manifest.get().name())); .message("[DEPS]: unknown language " + manifest.get().language() + " for project " + manifest.get().name()));
return Optional.empty(); return Optional.empty();
} }
final var frontendSpec = frontendProvider.get().specification();
// Enforces consistent language across projects // Enforces consistent language across projects
if (Objects.isNull(ctx.frontendSpec)) { if (Objects.isNull(ctx.frontendSpec)) {
ctx.frontendSpec = frontendSpec.get(); ctx.frontendSpec = frontendSpec;
} else if (!ctx.frontendSpec.getLanguageId().equals(frontendSpec.get().getLanguageId())) { } else if (!ctx.frontendSpec.getLanguageId().equals(frontendSpec.getLanguageId())) {
// Reports language inconsistency across projects // Reports language inconsistency across projects
issues.report(builder -> builder issues.report(builder -> builder
.error(true) .error(true)
.message(String.format("[DEPS]: inconsistent language: [ %s ] has \"%s\" but should be \"%s\"", .message(String.format("[DEPS]: inconsistent language: [ %s ] has \"%s\" but should be \"%s\"",
manifest.get().name(), frontendSpec.get().getLanguageId(), ctx.frontendSpec.getLanguageId()))); manifest.get().name(), frontendSpec.getLanguageId(), ctx.frontendSpec.getLanguageId())));
return Optional.empty(); return Optional.empty();
} }
final var projectInfo = ProjectInfo final var projectInfo = ProjectInfo

View File

@ -6,7 +6,7 @@ dependencies {
implementation(project(":prometeu-infra")) implementation(project(":prometeu-infra"))
implementation(project(":prometeu-compiler:prometeu-compiler-core")) implementation(project(":prometeu-compiler:prometeu-compiler-core"))
implementation(project(":prometeu-compiler:prometeu-frontend-api")) api(project(":prometeu-compiler:prometeu-frontend-api"))
implementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs")) implementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs"))
} }

View File

@ -21,7 +21,11 @@ public class FrontendRegistryService {
} }
public static FrontendSpec getDefaultFrontendSpec() { public static FrontendSpec getDefaultFrontendSpec() {
return require(PBSDefinitions.PBS.getLanguageId()).specification(); return getDefaultProvider().specification();
}
public static FrontendProvider getDefaultProvider() {
return require(PBSDefinitions.PBS.getLanguageId());
} }
public static Optional<FrontendSpec> getFrontendSpec(final String languageId) { public static Optional<FrontendSpec> getFrontendSpec(final String languageId) {

View File

@ -33,4 +33,12 @@ class FrontendRegistryServiceTest {
assertEquals(1, specs.size()); assertEquals(1, specs.size());
assertEquals(PBSDefinitions.PBS.getLanguageId(), specs.getFirst().getLanguageId()); assertEquals(PBSDefinitions.PBS.getLanguageId(), specs.getFirst().getLanguageId());
} }
@Test
void providerListingShouldExposeOnlyPbsByDefault() {
final var providers = FrontendRegistryService.listProviders();
assertEquals(1, providers.size());
assertEquals(PBSDefinitions.PBS.getLanguageId(), providers.getFirst().specification().getLanguageId());
}
} }

View File

@ -9,8 +9,8 @@ import java.util.Objects;
public final class ProjectLanguageCatalogService { public final class ProjectLanguageCatalogService {
public List<ProjectLanguageTemplate> listLanguageTemplates() { public List<ProjectLanguageTemplate> listLanguageTemplates() {
return FrontendRegistryService.listFrontendSpecs().stream() return FrontendRegistryService.listProviders().stream()
.map(this::toTemplate) .map(provider -> toTemplate(provider.specification()))
.sorted(Comparator.comparing(ProjectLanguageTemplate::languageId, String.CASE_INSENSITIVE_ORDER)) .sorted(Comparator.comparing(ProjectLanguageTemplate::languageId, String.CASE_INSENSITIVE_ORDER))
.toList(); .toList();
} }

View File

@ -0,0 +1,18 @@
package p.studio.projects;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ProjectLanguageCatalogServiceTest {
@Test
void shouldListLanguagesFromRegisteredProviders() {
final var service = new ProjectLanguageCatalogService();
final var templates = service.listLanguageTemplates();
assertEquals(1, templates.size());
assertEquals("pbs", templates.getFirst().languageId());
}
}