OPC # 0001: Extract OPC into standalone repo

This commit is contained in:
amadzarak
2026-04-25 17:26:42 -04:00
commit 42383bdc03
170 changed files with 21365 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
using ControlPlane.Api.Services;
using ControlPlane.Core.Models;
using LibGit2Sharp;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace ControlPlane.Api.Endpoints;
public static class OpcEndpoints
{
private static readonly JsonSerializerOptions JsonOpts =
new(JsonSerializerDefaults.Web) { WriteIndented = false };
public static IEndpointRouteBuilder MapOpcEndpoints(this IEndpointRouteBuilder app)
{
var g = app.MapGroup("/api/opc").WithTags("OPC");
// ── OPC CRUD ──────────────────────────────────────────────────────────
g.MapGet ("", ListOpcs);
g.MapGet ("/next-number", GetNextNumber);
g.MapPost ("", CreateOpc);
g.MapGet ("/{id:guid}", GetOpc);
g.MapPatch ("/{id:guid}", UpdateOpc);
g.MapDelete("/{id:guid}", DeleteOpc);
// ── Notes ─────────────────────────────────────────────────────────────
g.MapGet ("/{id:guid}/notes", ListNotes);
g.MapPost ("/{id:guid}/notes", AddNote);
// ── Artifacts ─────────────────────────────────────────────────────────
g.MapGet ("/{id:guid}/artifacts", ListArtifacts);
g.MapPost ("/{id:guid}/artifacts", CreateArtifact);
g.MapPatch ("/artifacts/{artifactId:guid}", UpdateArtifact);
g.MapDelete("/artifacts/{artifactId:guid}", DeleteArtifact);
// ── Pinned commits ────────────────────────────────────────────────────
g.MapGet ("/{id:guid}/pinned-commits", ListPinnedCommits);
g.MapPost ("/{id:guid}/pinned-commits", PinCommit);
g.MapDelete("/{id:guid}/pinned-commits/{hash}", UnpinCommit);
// ── AI assist (proxies to OpenRouter, key stays on server) ────────────
g.MapPost ("/ai-assist", AiAssist);
return app;
}
// ── OPC handlers ──────────────────────────────────────────────────────────
private static async Task<IResult> ListOpcs(
OpcService svc,
string? type = null, string? status = null,
CancellationToken ct = default)
{
var list = await svc.ListAsync(type, status, ct);
return Results.Ok(list);
}
private static async Task<IResult> GetNextNumber(
OpcService svc, CancellationToken ct)
{
var number = await svc.NextNumberAsync(ct);
return Results.Ok(new { number });
}
private static async Task<IResult> CreateOpc(
CreateOpcRequest req, OpcService svc, CancellationToken ct)
{
var opc = await svc.CreateAsync(req, ct);
return Results.Created($"/api/opc/{opc.Id}", opc);
}
private static async Task<IResult> GetOpc(
Guid id, OpcService svc, CancellationToken ct)
{
var opc = await svc.GetAsync(id, ct);
return opc is null ? Results.NotFound() : Results.Ok(opc);
}
private static async Task<IResult> UpdateOpc(
Guid id, UpdateOpcRequest req, OpcService svc, CancellationToken ct)
{
var opc = await svc.UpdateAsync(id, req, ct);
return opc is null ? Results.NotFound() : Results.Ok(opc);
}
private static async Task<IResult> DeleteOpc(
Guid id, OpcService svc, CancellationToken ct)
{
return await svc.DeleteAsync(id, ct) ? Results.NoContent() : Results.NotFound();
}
// ── Note handlers ─────────────────────────────────────────────────────────
private static async Task<IResult> ListNotes(
Guid id, OpcService svc, CancellationToken ct)
{
var notes = await svc.ListNotesAsync(id, ct);
return Results.Ok(notes);
}
private static async Task<IResult> AddNote(
Guid id, AddNoteRequest req, OpcService svc, CancellationToken ct)
{
var note = await svc.AddNoteAsync(id, req, ct);
return Results.Created($"/api/opc/{id}/notes/{note.Id}", note);
}
// ── Artifact handlers ─────────────────────────────────────────────────────
private static async Task<IResult> ListArtifacts(
Guid id, OpcService svc,
string? type = null, CancellationToken ct = default)
{
var artifacts = await svc.ListArtifactsAsync(id, type, ct);
return Results.Ok(artifacts);
}
private static async Task<IResult> CreateArtifact(
Guid id, UpsertArtifactRequest req, OpcService svc, CancellationToken ct)
{
var artifact = await svc.UpsertArtifactAsync(id, req, ct);
return Results.Created($"/api/opc/{id}/artifacts/{artifact.Id}", artifact);
}
private static async Task<IResult> UpdateArtifact(
Guid artifactId, UpsertArtifactRequest req, OpcService svc, CancellationToken ct)
{
var artifact = await svc.UpdateArtifactAsync(artifactId, req, ct);
return artifact is null ? Results.NotFound() : Results.Ok(artifact);
}
private static async Task<IResult> DeleteArtifact(
Guid artifactId, OpcService svc, CancellationToken ct)
{
return await svc.DeleteArtifactAsync(artifactId, ct)
? Results.NoContent()
: Results.NotFound();
}
// ── Pinned commit handlers ────────────────────────────────────────────────
private static async Task<IResult> ListPinnedCommits(
Guid id, OpcService svc, CancellationToken ct)
{
var commits = await svc.ListPinnedCommitsAsync(id, ct);
return Results.Ok(commits);
}
private static async Task<IResult> PinCommit(
Guid id, PinCommitRequest req, OpcService svc, IConfiguration config, CancellationToken ct)
{
var repoPath = config["Docker:RepoRoot"];
string fullHash = req.Hash;
string shortHash = req.Hash.Length >= 7 ? req.Hash[..7] : req.Hash;
string subject = string.Empty;
string author = string.Empty;
if (!string.IsNullOrWhiteSpace(repoPath) && Directory.Exists(repoPath))
{
using var repo = new Repository(repoPath);
var commit = repo.Lookup<Commit>(req.Hash);
if (commit is null) return Results.NotFound("Commit not found in repository.");
fullHash = commit.Sha;
shortHash = commit.Sha[..7];
subject = commit.MessageShort;
author = commit.Author.Name;
}
var pinned = await svc.PinCommitAsync(id, fullHash, shortHash, subject, author, req.PinnedBy, ct);
return pinned is null
? Results.NotFound()
: Results.Created($"/api/opc/{id}/pinned-commits/{fullHash}", pinned);
}
private static async Task<IResult> UnpinCommit(
Guid id, string hash, OpcService svc, CancellationToken ct)
{
return await svc.UnpinCommitAsync(id, hash, ct) ? Results.NoContent() : Results.NotFound();
}
// ── AI assist ─────────────────────────────────────────────────────────────
private static async Task<IResult> AiAssist(
AiAssistRequest req,
IConfiguration config,
IHttpClientFactory http,
CancellationToken ct)
{
var apiKey = config["OpenRouter:ApiKey"];
if (string.IsNullOrWhiteSpace(apiKey))
return Results.Problem("OpenRouter API key not configured. Add OpenRouter:ApiKey to appsettings.");
var systemPrompt =
"You are an assistant helping a software engineering team write clear, concise " +
"OPC (Online Project Communication) content — requirements, change descriptions, " +
"QA test paths, and specifications. Be direct, structured, and professional. " +
"Respond with plain text only (no markdown wrapping).";
var messages = new List<object>
{
new { role = "system", content = systemPrompt },
};
if (!string.IsNullOrWhiteSpace(req.Context))
messages.Add(new { role = "user", content = $"Context:\n{req.Context}" });
messages.Add(new { role = "user", content = req.Prompt });
var body = new
{
model = "anthropic/claude-3.5-haiku",
messages,
max_tokens = 1024,
};
var client = http.CreateClient("openrouter");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.Add("HTTP-Referer", "https://controlplane.clarity.internal");
client.DefaultRequestHeaders.Add("X-Title", "Clarity ControlPlane OPC");
var response = await client.PostAsync(
"https://openrouter.ai/api/v1/chat/completions",
new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"),
ct);
if (!response.IsSuccessStatusCode)
{
var err = await response.Content.ReadAsStringAsync(ct);
return Results.Problem($"OpenRouter error {response.StatusCode}: {err}");
}
var json = await response.Content.ReadAsStringAsync(ct);
using var doc = JsonDocument.Parse(json);
var text = doc.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString() ?? string.Empty;
return Results.Ok(new { text });
}
}