Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
# The console dist is BUILT here and embedded by Goldpath.Console: this is the ONLY
# place Node is needed, and it is why adopters never run it. The package refuses to
# pack without it (see its `RefuseAnEmptyConsole` target).
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: ui/console/pnpm-lock.yaml
- name: build the console
run: bash scripts/build-console.sh

- name: sanity build
run: dotnet build Goldpath.sln --nologo -c Release
- name: pack the train
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ coverage/

# The console's registry is per-DEPLOYMENT config; the smoke writes its own.
ui/console/public/console.config.json

# The console dist is BUILT (scripts/build-console.sh), never committed: it changes with
# every UI edit and CI lays it before packing.
packages/Goldpath.Console/wwwroot/*
!packages/Goldpath.Console/wwwroot/.gitkeep
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ not propose anything that contradicts them; changes only via a superseding ADR).
Phases 0–1 and the hardening set (H1–H8) are complete; the `0.1.0-preview.3` train is on
nuget.org (plus `specdrift` 0.4.2 as tool/MCP/Docker/Action). Phase D shipped the CorPay
reference app (`samples/corpay`, proven nightly against the published packages). The UI
phase is active (`docs/rfc/goldpath-console.md`: U1–U3 met — all five module
panels are proven nightly by the console smoke against one real app; U4 next). Live status ledgers —
phase is active (`docs/rfc/goldpath-console.md`: U1–U3 met, U4 in flight —
the registry, the triage home and `MapGoldpathConsole()` ship; the console smoke drives
three real apps and the app-SERVED console). Live status ledgers —
keep them updated in the same PR that changes reality: `docs/strategy/ai-sdlc-status.md`
(AI-assisted SDLC vs reality), `docs/strategy/coverage-matrix.md` (capability × sample),
and `docs/strategy/open-threads.md` (deferred work with its TRIGGER and the proof that
Expand Down
30 changes: 30 additions & 0 deletions Goldpath.sln
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Goldpath.Campaign", "packag
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Goldpath.Campaign.Tests", "tests\Goldpath.Campaign.Tests\Goldpath.Campaign.Tests.csproj", "{FF196F5A-D8A4-45D1-9EC8-A4A1EABEBC52}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Goldpath.Console", "packages\Goldpath.Console\Goldpath.Console.csproj", "{1616453C-824E-42DB-8FB1-63E824736568}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Goldpath.Console.Tests", "tests\Goldpath.Console.Tests\Goldpath.Console.Tests.csproj", "{C44229F0-A168-466E-8E29-EC6321827C59}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -639,6 +643,30 @@ Global
{FF196F5A-D8A4-45D1-9EC8-A4A1EABEBC52}.Release|x64.Build.0 = Release|Any CPU
{FF196F5A-D8A4-45D1-9EC8-A4A1EABEBC52}.Release|x86.ActiveCfg = Release|Any CPU
{FF196F5A-D8A4-45D1-9EC8-A4A1EABEBC52}.Release|x86.Build.0 = Release|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Debug|x64.ActiveCfg = Debug|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Debug|x64.Build.0 = Debug|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Debug|x86.ActiveCfg = Debug|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Debug|x86.Build.0 = Debug|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Release|Any CPU.Build.0 = Release|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Release|x64.ActiveCfg = Release|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Release|x64.Build.0 = Release|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Release|x86.ActiveCfg = Release|Any CPU
{1616453C-824E-42DB-8FB1-63E824736568}.Release|x86.Build.0 = Release|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Debug|x64.ActiveCfg = Debug|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Debug|x64.Build.0 = Debug|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Debug|x86.ActiveCfg = Debug|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Debug|x86.Build.0 = Debug|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Release|Any CPU.Build.0 = Release|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Release|x64.ActiveCfg = Release|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Release|x64.Build.0 = Release|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Release|x86.ActiveCfg = Release|Any CPU
{C44229F0-A168-466E-8E29-EC6321827C59}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -688,5 +716,7 @@ Global
{74BC3CD6-452C-40BB-A225-9D430138979D} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{64264F48-A69F-48B3-911F-5C3536ECA88F} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
{FF196F5A-D8A4-45D1-9EC8-A4A1EABEBC52} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{1616453C-824E-42DB-8FB1-63E824736568} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
{C44229F0-A168-466E-8E29-EC6321827C59} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
9 changes: 8 additions & 1 deletion docs/rfc/goldpath-console.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,14 @@ custom-develop ON, with the same kit, the same way they add features to the back
- **D1 — Stack: React + Tailwind, dist-shipped (supersedes the earlier RCL wording).**
One design system and one stack across the product family (Mockifyr/Praxis lineage);
adopters NEVER run Node — CI builds the dist, `Goldpath.Console` ships it as embedded
static assets served by `MapGoldpathConsole()` on the management head. The "no Node
static assets served by `MapGoldpathConsole()` on the management head. SHIPPED (U4,
2026-07-27): the package refuses to PACK without a built console; the registry comes
from the app's own configuration (`AddService`), not a JSON file someone has to copy
beside the dist; the console sits behind the same ops floor as the admin surfaces; a
missing asset 404s rather than being answered with the page (a blank screen with a green
status code is the worst failure a console can have); and an unknown path 404s too,
because the console has no client-side routes YET — when it gains them, the server must
also inject a `<base href>` (open-threads T9). The "no Node
in generated apps" principle holds by construction. Custom pages: the kit npm package
for teams that build UI, and a config-driven link/iframe-free tile row for those that
do not.
Expand Down
2 changes: 1 addition & 1 deletion docs/strategy/ai-sdlc-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ document exists to prevent.
| Skill quality (evals) | eval set per skill; a skill that fails evals is not released (§6.2) | **PARTIAL** — as of 2026-07-24 the 4 acceptance runners are portable (published specdrift tool, no local-checkout assumption; failures print their output) and the deterministic half runs nightly (`evals-acceptance`: runner syntax + breaker vs CorPay). The LLM half — running the skills themselves per fixture — stays deferred until an agent-in-CI story exists (P2) | nightly `evals-acceptance` job | agent-in-CI story (P2) unlocks full skill-run evals |
| Model proficiency matrix | skill × model → pass-rate matrix (§6.2) | **NOT BUILT** — Phase 2 (needs the eval lane first) | — | after the eval lane |
| Release / DevOps | release train + delivery telemetry (§6.5) | Train **SHIPPED-PROVEN** (OIDC trusted publishing, license gate, roll script); delivery report **NOT BUILT** — Phase 2 | release workflow + license gate | — |
| Operations | admin APIs + ops packs + console (§7.1) | Admin APIs + dashboards + runbooks **SHIPPED-PROVEN** per module; console **PARTIAL** (U1–U3 met: the run console plus all four module panels, each driving its module's real verbs in the nightly smoke; U4 — cross-service registry, triage home, auth story — open) | admin contract frozen + integration proofs | console phases U1–U4 |
| Operations | admin APIs + ops packs + console (§7.1) | Admin APIs + dashboards + runbooks **SHIPPED-PROVEN** per module; console **PARTIAL** (U1–U3 met; U4 in flight — the cross-service registry, the TODAY triage home and `MapGoldpathConsole()` ship, each proven in the nightly smoke against three real apps; the auth story and the CorPay proof remain) | admin contract frozen + integration proofs | console phases U1–U4 |
| Pipeline (chained skills + human gates) | §6.1 level 3 | **NOT BUILT** — Phase 2 by written deferral (§12) | — | — |
| Domain memory | project knowledge that grows with every run (§6.3) | **NOT BUILT** — `domain-memory-v1.md` is a v0.1 draft | — | Phase 2/3 |

Expand Down
1 change: 1 addition & 0 deletions docs/strategy/open-threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pending forever.
| T5 | **LLM-half skill evals** — running the skills themselves per fixture | Deterministic acceptance runs nightly; running the skills needs agent-in-CI | agent-in-CI exists | a skill that fails its evals cannot be released ([ai-sdlc-status.md](ai-sdlc-status.md) §2) |
| T6 | **CLI-as-MCP** and **plugin packaging** | Both are distribution shapes with no adopter asking yet | CLI-as-MCP: the Insurance sample's first run · packaging: the first brownfield adopter | the shape is exercised by that adopter's own flow, not by a demo we write for it |
| T7 | **`Goldpath.Ai`** (runtime AI as an opt-in module, ADR-0011) | The console phase owns the calendar; the RFC is written and the ADR is accepted | after the UI phase | the module composes in and out cleanly (a manifest without it has NO AI code), and its evals run in the same lane as the rest |
| T9 | Client-side ROUTES for the console (a section is state today, not a URL) | Nobody has asked to bookmark a panel yet, and a fake fallback is worse than none: serving the page at an arbitrary path answers 200 with a document whose relative assets resolve a directory too deep — a blank screen with a green status code | the first operator who wants to share a link to a panel | `MapGoldpathConsole` serves the page for real routes AND injects a `<base href>` so assets resolve; the served-console e2e stops asserting 404 and asserts the panel |
| T8 | Bulk's **`?definition=` filter** on `/batches` ([#72](https://github.com/omercelikdev/goldpath/issues/72)) | The console refuses to fake it client-side: narrowing one take-bounded page would read as "no batches" while more exist | the next bulk surface change | the panel filters by definition through the SERVER, and the smoke walks a definition with more batches than one page |

## Closed threads
Expand Down
46 changes: 46 additions & 0 deletions packages/Goldpath.Console/Goldpath.Console.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<PackageId>Goldpath.Console</PackageId>
<Description>The Goldpath operations console, served by YOUR management head: MapGoldpathConsole() maps the built single-page console as embedded static assets, behind the same ops floor as the admin surfaces. Adopters never run Node — the dist is built once by Goldpath's CI and shipped inside this package.</Description>
<RootNamespace>Goldpath</RootNamespace>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../Goldpath.Abstractions/Goldpath.Abstractions.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Goldpath.Console.Tests" />
</ItemGroup>

<ItemGroup>
<!-- The ops floor, from the same shared source every admin mapper links (H2). -->
<Compile Include="../shared/AdminSurfaceGuard.cs" Link="AdminSurfaceGuard.cs" />
</ItemGroup>

<ItemGroup>
<!-- The console's dist, embedded. `scripts/build-console.sh` fills wwwroot/; the
PACK gate below refuses to publish a package whose console was never built. -->
<EmbeddedResource Include="wwwroot/**/*" Exclude="wwwroot/.gitkeep" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" PrivateAssets="all" />
<AdditionalFiles Include="PublicAPI.Shipped.txt" />
<AdditionalFiles Include="PublicAPI.Unshipped.txt" />
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>

<Target Name="RefuseAnEmptyConsole" BeforeTargets="GenerateNuspec">
<Error Condition="!Exists('$(MSBuildProjectDirectory)/wwwroot/index.html')"
Text="Goldpath.Console has no built console in wwwroot/. Run scripts/build-console.sh before packing — a package that maps an empty console would 404 in every adopter's face." />
</Target>

</Project>
123 changes: 123 additions & 0 deletions packages/Goldpath.Console/GoldpathConsoleEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;

namespace Goldpath;

/// <summary>One service the console can drive: the operator's word for it, and its head.</summary>
public sealed record GoldpathConsoleService(string Name, string AdminBaseUrl);

/// <summary>
/// How the console is served. The registry lives HERE rather than in a file next to the
/// dist: an adopter who already declares their services in configuration should not also
/// have to drop JSON into a static folder.
/// </summary>
public sealed class GoldpathConsoleOptions
{
/// <summary>The cross-service registry. Empty means "this app only" — the common case.</summary>
public IList<GoldpathConsoleService> Services { get; } = [];

/// <summary>Adds a service the console should offer (console RFC §3).</summary>
public GoldpathConsoleOptions AddService(string name, string adminBaseUrl)
{
Services.Add(new GoldpathConsoleService(name, adminBaseUrl));
return this;
}
}

/// <summary>
/// Serves the Goldpath console from the app's own management head (console RFC D1).
/// <para>
/// The console is a single page that talks to the FROZEN admin contract with the
/// operator's own credentials — it adds no capability the API does not already expose.
/// Its assets are embedded in this package: adopters never run Node, and a generated app
/// stays Node-free by construction.
/// </para>
/// </summary>
public static class GoldpathConsoleEndpoints
{
private static readonly FileExtensionContentTypeProvider ContentTypes = new();

/// <summary>
/// Maps the console under <paramref name="prefix"/>, behind the SAME ops floor as the
/// admin surfaces (H2). `exposeUnsecured: true` is for hosts that have no auth at all
/// — the guard logs the choice, exactly as the admin mappers do.
/// </summary>
public static IEndpointRouteBuilder MapGoldpathConsole(
this IEndpointRouteBuilder endpoints,
string prefix = "/goldpath/console",
bool exposeUnsecured = false,
Action<GoldpathConsoleOptions>? configure = null)
{
var options = new GoldpathConsoleOptions();
configure?.Invoke(options);

var assembly = typeof(GoldpathConsoleEndpoints).Assembly;
// The resource prefix follows the ROOT NAMESPACE (Goldpath), not the assembly name —
// `Goldpath.Console.wwwroot` would find nothing and every request would 404.
var files = new EmbeddedFileProvider(assembly, "Goldpath.wwwroot");
var group = endpoints.MapGroup(prefix);
AdminSurfaceGuard.Apply(endpoints, group, prefix, exposeUnsecured);

// The registry, as the console reads it. Served from CONFIG so the same dist works
// for every adopter; an empty registry is simply absent, which the console reads
// as "this service only".
group.MapGet("/console.config.json", () =>
{
var payload = JsonSerializer.Serialize(new
{
services = options.Services.Select(service => new { name = service.Name, adminBaseUrl = service.AdminBaseUrl }),
});
return Results.Content(payload, "application/json", Encoding.UTF8);
});

group.MapGet("/{**path}", (string? path) => Serve(files, path));

return endpoints;
}

/// <summary>
/// Resolves one request against the embedded console. Internal rather than private so
/// the three outcomes an operator can hit — the page, a missing asset, and a package
/// built without a console — are proven deterministically instead of depending on
/// whether the machine running the tests happened to build the dist.
/// </summary>
internal static IResult Serve(IFileProvider files, string? path)
{
var requested = path?.Replace('\\', '/').Trim('/') ?? "";

// The PAGE is the prefix root, and only the root. The console has no client-side
// routes yet (sections are state, not URLs), so there is nothing to fall back TO:
// serving the page at an arbitrary path would answer 200 with a document whose
// relative asset URLs resolve one directory too deep — a blank screen with a green
// status code. When real routes arrive, this is the place that must also inject a
// <base href> (open-threads T9).
if (requested.Length == 0)
{
var page = files.GetFileInfo("index.html");
return page.Exists
? Results.Stream(page.CreateReadStream(), ContentTypeOf("index.html"))
: Results.Problem(
"This build of Goldpath.Console carries no console. The package is built by Goldpath's CI from ui/console; a locally built one needs scripts/build-console.sh first.",
statusCode: StatusCodes.Status500InternalServerError);
}

var asset = files.GetFileInfo(requested);
// A missing asset must 404 rather than answer with HTML the browser cannot parse —
// that is how a console "loads" into a blank screen with nothing in the log.
return asset.Exists ? Results.Stream(asset.CreateReadStream(), ContentTypeOf(requested)) : Results.NotFound();
}

/// <summary>
/// The framework's own extension→type map (ADR-0003: compose, never rewrite). Anything
/// it does not know is served as bytes rather than guessed at — a guessed content type
/// is how a console ends up rendered as text.
/// </summary>
private static string ContentTypeOf(string path)
=> ContentTypes.TryGetContentType(path, out var contentType) ? contentType : "application/octet-stream";

}
1 change: 1 addition & 0 deletions packages/Goldpath.Console/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
13 changes: 13 additions & 0 deletions packages/Goldpath.Console/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#nullable enable
Goldpath.GoldpathConsoleEndpoints
Goldpath.GoldpathConsoleOptions
Goldpath.GoldpathConsoleOptions.AddService(string! name, string! adminBaseUrl) -> Goldpath.GoldpathConsoleOptions!
Goldpath.GoldpathConsoleOptions.GoldpathConsoleOptions() -> void
Goldpath.GoldpathConsoleOptions.Services.get -> System.Collections.Generic.IList<Goldpath.GoldpathConsoleService!>!
Goldpath.GoldpathConsoleService
Goldpath.GoldpathConsoleService.AdminBaseUrl.get -> string!
Goldpath.GoldpathConsoleService.AdminBaseUrl.init -> void
Goldpath.GoldpathConsoleService.GoldpathConsoleService(string! Name, string! AdminBaseUrl) -> void
Goldpath.GoldpathConsoleService.Name.get -> string!
Goldpath.GoldpathConsoleService.Name.init -> void
static Goldpath.GoldpathConsoleEndpoints.MapGoldpathConsole(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! prefix = "/goldpath/console", bool exposeUnsecured = false, System.Action<Goldpath.GoldpathConsoleOptions!>? configure = null) -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder!
Loading
Loading