Files
Teamup/src/Modules/TeamUp.Modules.Skills/Domain/Skill.cs
T
soroush.asadi 401e3e69af M2: skill index — SKILL.md parsing, pgvector index, query by role
Skills module (references SharedKernel only):
- Skill entity + SkillsDbContext (schema "skills") + InitialSkills migration: roles/tools/
  context as text[], risk-tagged actions and golden tests as jsonb, a nullable vector(384)
  embedding, unique (SkillKey, Version).
- SkillMarkdownParser: YAML frontmatter (YamlDotNet) + markdown body → SkillManifest.
- HashingSkillEmbedder: placeholder deterministic embedder so the pgvector path is real now;
  swapped for ONNX/BYOK embeddings at M3-M4 (384-dim to match MiniLM/bge).
- SkillIndexer: parse → hash → embed → upsert; structural publish gate (roles + >=1 golden
  test). Executing golden tests against a model + gating on edit distance lands at M4.
- Endpoints: GET /api/skills (filter by role/visibility), GET /api/skills/{key},
  POST /api/skills/index (manual/admin) — all authenticated.

Verified: build green; ArchitectureTests 8/8 (Skills references only SharedKernel);
IntegrationTests 21/21 incl. a new skill-registry flow — index a SKILL.md, it publishes,
is queryable by role (and not under others), re-index dedups, malformed is 400, catalogue
needs auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:01:37 +03:30

99 lines
3.8 KiB
C#

using Pgvector;
using TeamUp.SharedKernel.Domain;
namespace TeamUp.Modules.Skills.Domain;
/// <summary>
/// An indexed skill atom: the projection of a SKILL.md (Git is the source of truth) into a
/// queryable Postgres + pgvector row. Identified by (SkillKey, Version).
/// </summary>
internal sealed class Skill : Entity
{
public string SkillKey { get; private set; } = null!;
public string Name { get; private set; } = null!;
public string Version { get; private set; } = null!;
public string? Summary { get; private set; }
public List<string> Roles { get; private set; } = [];
public string? Inputs { get; private set; }
public string? Outputs { get; private set; }
public List<SkillAction> Actions { get; private set; } = [];
public List<string> Tools { get; private set; } = [];
public List<string> Context { get; private set; } = [];
public List<GoldenExample> GoldenTests { get; private set; } = [];
public SkillVisibility Visibility { get; private set; }
public SkillTier MinTier { get; private set; }
public SkillStatus Status { get; private set; }
public string Body { get; private set; } = null!;
public string ContentHash { get; private set; } = null!;
public string? SourceRepo { get; private set; }
public string? SourcePath { get; private set; }
public string? SourceCommit { get; private set; }
public Vector? Embedding { get; private set; }
public DateTimeOffset IndexedAtUtc { get; private set; }
public DateTimeOffset UpdatedAtUtc { get; private set; }
private Skill()
{
}
public static Skill Create(string skillKey, string version, DateTimeOffset nowUtc) =>
new() { SkillKey = skillKey, Version = version, IndexedAtUtc = nowUtc };
/// <summary>(Re)projects a parsed manifest + body onto this row. Used for both insert and update.</summary>
public void Index(
SkillManifest manifest,
string body,
string contentHash,
string? sourceRepo,
string? sourcePath,
string? sourceCommit,
Vector? embedding,
SkillStatus status,
DateTimeOffset nowUtc)
{
Name = string.IsNullOrWhiteSpace(manifest.Name) ? manifest.Id : manifest.Name;
Version = manifest.Version;
Summary = manifest.Summary;
Roles = manifest.Roles;
Inputs = manifest.Inputs;
Outputs = manifest.Outputs;
Actions = manifest.Actions
.Select(a => new SkillAction { Name = a.Name, Risk = ParseRisk(a.Risk), Description = a.Description })
.ToList();
Tools = manifest.Tools;
Context = manifest.Context;
GoldenTests = manifest.GoldenTests;
Visibility = ParseVisibility(manifest.Visibility);
MinTier = ParseTier(manifest.MinTier);
Status = status;
Body = body;
ContentHash = contentHash;
SourceRepo = sourceRepo;
SourcePath = sourcePath;
SourceCommit = sourceCommit;
Embedding = embedding;
UpdatedAtUtc = nowUtc;
}
private static string Normalize(string value) => value.Trim().Replace("-", string.Empty).Replace("_", string.Empty);
private static ActionRisk ParseRisk(string value) => Normalize(value).ToLowerInvariant() switch
{
"draft" => ActionRisk.Draft,
"publish" => ActionRisk.Publish,
"destructive" => ActionRisk.Destructive,
_ => ActionRisk.Read,
};
private static SkillVisibility ParseVisibility(string value) =>
Normalize(value).ToLowerInvariant() is "privatetoorg" or "private" ? SkillVisibility.PrivateToOrg : SkillVisibility.Public;
private static SkillTier ParseTier(string value) => Normalize(value).ToLowerInvariant() switch
{
"team" => SkillTier.Team,
"scale" => SkillTier.Scale,
"enterprise" => SkillTier.Enterprise,
_ => SkillTier.Free,
};
}