gomcp/internal/application/tools/pulse.go
DmitrL-dev 694e32be26 refactor: rename identity to syntrex, add root orchestration and CI/CD
- Rename Go module: sentinel-community/gomcp -> syntrex/gomcp (50+ files)
- Rename npm package: sentinel-dashboard -> syntrex-dashboard
- Update Cargo.toml repository URL to syntrex/syntrex
- Update all doc references from DmitrL-dev/AISecurity to syntrex
- Add root Makefile (build-all, test-all, lint-all, clean-all)
- Add MIT LICENSE
- Add .editorconfig (Go/Rust/TS/C cross-language)
- Add .github/workflows/ci.yml (Go + Rust + Dashboard)
- Add dashboard next.config.ts and .env.example
- Clean ARCHITECTURE.md: remove brain/immune/strike/micro-swarm, fix 61->67 engines
2026-03-11 15:30:49 +10:00

123 lines
3.2 KiB
Go

package tools
import (
"context"
"fmt"
"strings"
"time"
"github.com/syntrex/gomcp/internal/domain/memory"
)
// ProjectPulse generates auto-documentation from L0/L1 facts (v3.7 Cerebro).
// Extracts facts from memory, groups by domain, and produces a structured
// markdown report reflecting the current state of the project.
type ProjectPulse struct {
facts *FactService
}
// NewProjectPulse creates an auto-documentation generator.
func NewProjectPulse(facts *FactService) *ProjectPulse {
return &ProjectPulse{facts: facts}
}
// PulseSection is a domain section of the auto-generated documentation.
type PulseSection struct {
Domain string `json:"domain"`
Facts []string `json:"facts"`
Count int `json:"count"`
}
// PulseReport is the full auto-generated documentation.
type PulseReport struct {
GeneratedAt time.Time `json:"generated_at"`
ProjectName string `json:"project_name"`
Sections []PulseSection `json:"sections"`
TotalFacts int `json:"total_facts"`
Markdown string `json:"markdown"`
}
// Generate produces a documentation report from L0 (project) and L1 (domain) facts.
func (p *ProjectPulse) Generate(ctx context.Context) (*PulseReport, error) {
// Get L0 facts (project-level).
l0Facts, err := p.facts.GetL0Facts(ctx)
if err != nil {
return nil, fmt.Errorf("pulse: L0 facts: %w", err)
}
// Get L1 facts (domain-level) by listing domains.
domains, err := p.facts.ListDomains(ctx)
if err != nil {
return nil, fmt.Errorf("pulse: list domains: %w", err)
}
report := &PulseReport{
GeneratedAt: time.Now(),
ProjectName: "GoMCP",
}
// L0 section.
if len(l0Facts) > 0 {
section := PulseSection{Domain: "Project (L0)", Count: len(l0Facts)}
for _, f := range l0Facts {
section.Facts = append(section.Facts, factSummary(f))
}
report.Sections = append(report.Sections, section)
report.TotalFacts += len(l0Facts)
}
// L1 sections per domain.
for _, domain := range domains {
domainFacts, err := p.facts.ListFacts(ctx, ListFactsParams{Domain: domain})
if err != nil {
continue
}
// Filter to L1 only.
var filtered []*memory.Fact
for _, f := range domainFacts {
if f.Level <= 1 {
filtered = append(filtered, f)
}
}
if len(filtered) == 0 {
continue
}
section := PulseSection{Domain: domain, Count: len(filtered)}
for _, f := range filtered {
section.Facts = append(section.Facts, factSummary(f))
}
report.Sections = append(report.Sections, section)
report.TotalFacts += len(filtered)
}
report.Markdown = renderPulseMarkdown(report)
return report, nil
}
func factSummary(f *memory.Fact) string {
s := f.Content
if len(s) > 120 {
s = s[:120] + "..."
}
label := ""
if f.IsGene {
label = " 🧬"
}
return fmt.Sprintf("- %s%s", s, label)
}
func renderPulseMarkdown(r *PulseReport) string {
var b strings.Builder
fmt.Fprintf(&b, "# %s — Project Pulse\n\n", r.ProjectName)
fmt.Fprintf(&b, "> Auto-generated: %s | %d facts\n\n", r.GeneratedAt.Format("2006-01-02 15:04"), r.TotalFacts)
for _, section := range r.Sections {
fmt.Fprintf(&b, "## %s (%d facts)\n\n", section.Domain, section.Count)
for _, fact := range section.Facts {
fmt.Fprintln(&b, fact)
}
fmt.Fprintln(&b)
}
return b.String()
}