chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)

Snapshot de WIP acumulado de sesiones previas antes de merge wave 1
del flow 0008 (kanban_cpp + agent_runner_api + DoD schema).

Incluye:
- dev/flows/0008-kanban-cpp-and-agent-workflows.md
- dev/issues/0112-0119*.md (7 sub-issues)
- WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 18:17:08 +02:00
parent ddb5366884
commit b9716a7cd6
119 changed files with 14929 additions and 3084 deletions
@@ -0,0 +1,21 @@
-- 014: service: bloque del frontmatter de app.md → columnas apps + tabla service_targets
-- Issue 0105
ALTER TABLE apps ADD COLUMN service_port INTEGER NOT NULL DEFAULT 0;
ALTER TABLE apps ADD COLUMN service_health_endpoint TEXT NOT NULL DEFAULT '';
ALTER TABLE apps ADD COLUMN service_health_timeout_s INTEGER NOT NULL DEFAULT 0;
ALTER TABLE apps ADD COLUMN service_systemd_unit TEXT NOT NULL DEFAULT '';
ALTER TABLE apps ADD COLUMN service_systemd_scope TEXT NOT NULL DEFAULT '';
ALTER TABLE apps ADD COLUMN service_restart_policy TEXT NOT NULL DEFAULT '';
ALTER TABLE apps ADD COLUMN service_runtime TEXT NOT NULL DEFAULT '';
ALTER TABLE apps ADD COLUMN service_is_local_only INTEGER NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS service_targets (
app_id TEXT NOT NULL,
pc_id TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'primary',
PRIMARY KEY (app_id, pc_id)
);
CREATE INDEX IF NOT EXISTS idx_service_targets_pc ON service_targets(pc_id);
CREATE INDEX IF NOT EXISTS idx_service_targets_app ON service_targets(app_id);
+6
View File
@@ -0,0 +1,6 @@
-- 015: semver per-app
-- Cada app declara `version: X.Y.Z` en su app.md (default 0.1.0).
-- Bumped por /version cuando /fix-issue (u otro flujo) toca codigo de la app.
-- Trazabilidad: ## Capability growth log dentro del propio app.md.
ALTER TABLE apps ADD COLUMN version TEXT NOT NULL DEFAULT '0.1.0';
+35 -19
View File
@@ -105,25 +105,41 @@ type Type struct {
// App represents an entry in the apps table.
type App struct {
ID string `json:"id"`
Name string `json:"name"`
Lang string `json:"lang"`
Domain string `json:"domain"`
Description string `json:"description"`
Tags []string `json:"tags"`
UsesFunctions []string `json:"uses_functions"`
UsesTypes []string `json:"uses_types"`
UsesModules []string `json:"uses_modules"`
Framework string `json:"framework"`
EntryPoint string `json:"entry_point"`
Documentation string `json:"documentation"`
Notes string `json:"notes"`
DirPath string `json:"dir_path"`
ContentHash string `json:"content_hash"`
RepoURL string `json:"repo_url"`
ProjectID string `json:"project_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id"`
Name string `json:"name"`
Lang string `json:"lang"`
Domain string `json:"domain"`
Version string `json:"version"`
Description string `json:"description"`
Tags []string `json:"tags"`
UsesFunctions []string `json:"uses_functions"`
UsesTypes []string `json:"uses_types"`
UsesModules []string `json:"uses_modules"`
Framework string `json:"framework"`
EntryPoint string `json:"entry_point"`
Documentation string `json:"documentation"`
Notes string `json:"notes"`
DirPath string `json:"dir_path"`
ContentHash string `json:"content_hash"`
RepoURL string `json:"repo_url"`
ProjectID string `json:"project_id"`
Service *ServiceSpec `json:"service,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ServiceSpec describes how an app runs as a long-lived service.
// Populated from the `service:` block of app.md frontmatter (issue 0105).
type ServiceSpec struct {
Port int `json:"port,omitempty"`
HealthEndpoint string `json:"health_endpoint,omitempty"`
HealthTimeoutS int `json:"health_timeout_s,omitempty"`
SystemdUnit string `json:"systemd_unit,omitempty"`
SystemdScope string `json:"systemd_scope,omitempty"`
RestartPolicy string `json:"restart_policy,omitempty"`
Runtime string `json:"runtime,omitempty"`
IsLocalOnly bool `json:"is_local_only,omitempty"`
PCTargets []string `json:"pc_targets,omitempty"`
}
// Analysis represents an entry in the analysis table.
+49 -12
View File
@@ -75,18 +75,33 @@ type rawType struct {
// rawApp mirrors the YAML frontmatter of an app .md file.
type rawApp struct {
Name string `yaml:"name"`
Lang string `yaml:"lang"`
Domain string `yaml:"domain"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
UsesFunctions []string `yaml:"uses_functions"`
UsesTypes []string `yaml:"uses_types"`
UsesModules []string `yaml:"uses_modules"`
Framework string `yaml:"framework"`
EntryPoint string `yaml:"entry_point"`
DirPath string `yaml:"dir_path"`
RepoURL string `yaml:"repo_url"`
Name string `yaml:"name"`
Lang string `yaml:"lang"`
Domain string `yaml:"domain"`
Version string `yaml:"version"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
UsesFunctions []string `yaml:"uses_functions"`
UsesTypes []string `yaml:"uses_types"`
UsesModules []string `yaml:"uses_modules"`
Framework string `yaml:"framework"`
EntryPoint string `yaml:"entry_point"`
DirPath string `yaml:"dir_path"`
RepoURL string `yaml:"repo_url"`
Service *rawService `yaml:"service"`
}
// rawService mirrors the `service:` block of an app.md frontmatter (issue 0105).
type rawService struct {
Port *int `yaml:"port"`
HealthEndpoint string `yaml:"health_endpoint"`
HealthTimeoutS int `yaml:"health_timeout_s"`
SystemdUnit string `yaml:"systemd_unit"`
SystemdScope string `yaml:"systemd_scope"`
RestartPolicy string `yaml:"restart_policy"`
Runtime string `yaml:"runtime"`
IsLocalOnly bool `yaml:"is_local_only"`
PCTargets []string `yaml:"pc_targets"`
}
// rawAnalysis mirrors the YAML frontmatter of an analysis .md file.
@@ -325,11 +340,16 @@ func ParseAppMD(path string, root string) (*App, error) {
sections := extractSections(body)
if raw.Version == "" {
raw.Version = "0.1.0"
}
a := &App{
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
Name: raw.Name,
Lang: raw.Lang,
Domain: raw.Domain,
Version: raw.Version,
Description: raw.Description,
Tags: raw.Tags,
UsesFunctions: raw.UsesFunctions,
@@ -343,6 +363,23 @@ func ParseAppMD(path string, root string) (*App, error) {
RepoURL: raw.RepoURL,
}
if raw.Service != nil {
spec := &ServiceSpec{
HealthEndpoint: raw.Service.HealthEndpoint,
HealthTimeoutS: raw.Service.HealthTimeoutS,
SystemdUnit: raw.Service.SystemdUnit,
SystemdScope: raw.Service.SystemdScope,
RestartPolicy: raw.Service.RestartPolicy,
Runtime: raw.Service.Runtime,
IsLocalOnly: raw.Service.IsLocalOnly,
PCTargets: raw.Service.PCTargets,
}
if raw.Service.Port != nil {
spec.Port = *raw.Service.Port
}
a.Service = spec
}
return a, nil
}
+105 -3
View File
@@ -303,18 +303,70 @@ func (db *DB) InsertApp(a *App) error {
a.ID = GenerateID(a.Name, a.Lang, a.Domain)
}
var (
svcPort int
svcHealth string
svcHealthTO int
svcUnit string
svcScope string
svcRestart string
svcRuntime string
svcLocalOnly int
)
if a.Service != nil {
svcPort = a.Service.Port
svcHealth = a.Service.HealthEndpoint
svcHealthTO = a.Service.HealthTimeoutS
svcUnit = a.Service.SystemdUnit
svcScope = a.Service.SystemdScope
svcRestart = a.Service.RestartPolicy
svcRuntime = a.Service.Runtime
if a.Service.IsLocalOnly {
svcLocalOnly = 1
}
}
if a.Version == "" {
a.Version = "0.1.0"
}
_, err := db.conn.Exec(`
INSERT OR REPLACE INTO apps (
id, name, lang, domain, description, tags,
uses_functions, uses_types, framework, entry_point,
documentation, notes, dir_path, content_hash, created_at, updated_at, repo_url, project_id, uses_modules
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
documentation, notes, dir_path, content_hash, created_at, updated_at, repo_url, project_id, uses_modules,
service_port, service_health_endpoint, service_health_timeout_s,
service_systemd_unit, service_systemd_scope, service_restart_policy,
service_runtime, service_is_local_only, version
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
a.ID, a.Name, a.Lang, a.Domain, a.Description, marshalStrings(a.Tags),
marshalStrings(a.UsesFunctions), marshalStrings(a.UsesTypes), a.Framework, a.EntryPoint,
a.Documentation, a.Notes, a.DirPath, a.ContentHash, a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339),
a.RepoURL, a.ProjectID, marshalStrings(a.UsesModules),
svcPort, svcHealth, svcHealthTO, svcUnit, svcScope, svcRestart, svcRuntime, svcLocalOnly, a.Version,
)
return err
if err != nil {
return err
}
// Replace service_targets for this app (idempotent).
if _, err := db.conn.Exec("DELETE FROM service_targets WHERE app_id = ?", a.ID); err != nil {
return fmt.Errorf("clearing service_targets for %s: %w", a.ID, err)
}
if a.Service != nil {
for _, pc := range a.Service.PCTargets {
if pc == "" {
continue
}
if _, err := db.conn.Exec(
"INSERT OR REPLACE INTO service_targets (app_id, pc_id, role) VALUES (?, ?, 'primary')",
a.ID, pc,
); err != nil {
return fmt.Errorf("inserting service_target %s/%s: %w", a.ID, pc, err)
}
}
}
return nil
}
// GetApp returns a single app by ID.
@@ -374,12 +426,16 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
var a App
var tagsJSON, usesFnJSON, usesTypJSON, usesModJSON string
var createdAt, updatedAt string
var svcPort, svcHealthTO, svcLocalOnly int
var svcHealth, svcUnit, svcScope, svcRestart, svcRuntime string
err := rows.Scan(
&a.ID, &a.Name, &a.Lang, &a.Domain, &a.Description, &tagsJSON,
&usesFnJSON, &usesTypJSON, &a.Framework, &a.EntryPoint,
&a.Documentation, &a.Notes, &a.DirPath, &createdAt, &updatedAt, &a.ContentHash,
&a.RepoURL, &a.ProjectID, &usesModJSON,
&svcPort, &svcHealth, &svcHealthTO, &svcUnit, &svcScope, &svcRestart, &svcRuntime, &svcLocalOnly,
&a.Version,
)
if err != nil {
return nil, fmt.Errorf("scanning app: %w", err)
@@ -392,11 +448,47 @@ func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error)
a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
a.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
if svcPort != 0 || svcHealth != "" || svcUnit != "" || svcScope != "" || svcRestart != "" || svcRuntime != "" || svcLocalOnly != 0 {
a.Service = &ServiceSpec{
Port: svcPort,
HealthEndpoint: svcHealth,
HealthTimeoutS: svcHealthTO,
SystemdUnit: svcUnit,
SystemdScope: svcScope,
RestartPolicy: svcRestart,
Runtime: svcRuntime,
IsLocalOnly: svcLocalOnly != 0,
}
}
result = append(result, a)
}
return result, nil
}
// GetServicePCTargets returns the pc_ids declared in service_targets for an app.
// Empty slice when the app has no declared targets. Issue 0105.
func (db *DB) GetServicePCTargets(appID string) ([]string, error) {
rows, err := db.conn.Query(
"SELECT pc_id FROM service_targets WHERE app_id = ? ORDER BY pc_id",
appID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []string
for rows.Next() {
var pc string
if err := rows.Scan(&pc); err != nil {
return nil, err
}
out = append(out, pc)
}
return out, nil
}
// Purge deletes all data from functions, types, apps, analysis, projects, vaults and modules. Used before re-indexing.
func (db *DB) Purge() error {
if _, err := db.conn.Exec("DELETE FROM functions"); err != nil {
@@ -408,6 +500,9 @@ func (db *DB) Purge() error {
if _, err := db.conn.Exec("DELETE FROM apps"); err != nil {
return err
}
if _, err := db.conn.Exec("DELETE FROM service_targets"); err != nil {
return err
}
if _, err := db.conn.Exec("DELETE FROM analysis"); err != nil {
return err
}
@@ -435,11 +530,18 @@ func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs, localProjectIDs map[
if _, err := db.conn.Exec("DELETE FROM apps WHERE id = ?", id); err != nil {
return err
}
if _, err := db.conn.Exec("DELETE FROM service_targets WHERE app_id = ?", id); err != nil {
return err
}
}
// Delete apps without repo_url (legacy local-only apps not yet pushed)
if _, err := db.conn.Exec("DELETE FROM apps WHERE repo_url = '' OR repo_url IS NULL"); err != nil {
return err
}
// Orphan service_targets cleanup
if _, err := db.conn.Exec("DELETE FROM service_targets WHERE app_id NOT IN (SELECT id FROM apps)"); err != nil {
return err
}
// Same for analysis
for id := range localAnalysisIDs {
if _, err := db.conn.Exec("DELETE FROM analysis WHERE id = ?", id); err != nil {