using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using Pgvector; using TeamUp.Modules.Skills.Domain; using TeamUp.Modules.Skills.Parsing; using TeamUp.Modules.Skills.Persistence; namespace TeamUp.Modules.Skills.Indexing; /// Parses a SKILL.md, computes its embedding, and upserts the Skill row (by key+version). internal sealed class SkillIndexer(SkillsDbContext db, ISkillEmbedder embedder, TimeProvider clock) { public async Task IndexAsync( string content, string? sourceRepo, string? sourcePath, string? sourceCommit, CancellationToken cancellationToken = default) { var parsed = SkillMarkdownParser.Parse(content); var manifest = parsed.Manifest; var now = clock.GetUtcNow(); var contentHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content))); var embeddingText = $"{manifest.Name}\n{manifest.Summary}\n{string.Join(' ', manifest.Roles)}\n{parsed.Body}"; var embedding = new Vector(embedder.Embed(embeddingText)); // M2 publish gate (structural): a skill is published only if it declares roles and carries // at least one well-formed golden test. Executing the golden tests against a model — and // gating on edit distance — lands in M4 when the assembler/runtime exists. var status = manifest.Roles.Count > 0 && manifest.GoldenTests.Count > 0 ? SkillStatus.Published : SkillStatus.Draft; var skill = await db.Skills .FirstOrDefaultAsync(s => s.SkillKey == manifest.Id && s.Version == manifest.Version, cancellationToken); var isNew = skill is null; skill ??= Skill.Create(manifest.Id, manifest.Version, now); skill.Index(manifest, parsed.Body, contentHash, sourceRepo, sourcePath, sourceCommit, embedding, status, now); if (isNew) { db.Skills.Add(skill); } await db.SaveChangesAsync(cancellationToken); return skill; } }