feat: executions, assertions y bucle reactivo en fn_operations
Añade Execution, Assertion, AssertionResult al paquete fn_operations. Motor de evaluación de assertions con reescritura SQL automática. Bucle reactivo: ExecuteAndReact evalúa assertions y cambia status de entities (corrupted/stale) + auto-crea proposals en registry. CLI fn ops: assertion (add/list/show/delete/eval) y execution (add/list/show). Migración 002_executions_assertions.sql con FTS para assertions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -31,6 +31,8 @@ func main() {
|
||||
cmdAdd(os.Args[2:])
|
||||
case "ops":
|
||||
cmdOps(os.Args[2:])
|
||||
case "proposal":
|
||||
cmdProposal(os.Args[2:])
|
||||
case "help", "-h", "--help":
|
||||
printUsage()
|
||||
default:
|
||||
@@ -49,7 +51,8 @@ Usage:
|
||||
fn list [-k kind] [-d domain] [-l lang]
|
||||
fn show <id> Muestra entrada completa
|
||||
fn add [-k kind] Abre $EDITOR con template
|
||||
fn ops <subcommand> Gestiona operations.db (fn ops help)`)
|
||||
fn ops <subcommand> Gestiona operations.db (fn ops help)
|
||||
fn proposal <add|list|show|update> Gestiona proposals`)
|
||||
}
|
||||
|
||||
func root() string {
|
||||
|
||||
+677
-1
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
ops "fn-registry/fn_operations"
|
||||
"fn-registry/registry"
|
||||
@@ -32,6 +33,10 @@ func cmdOps(args []string) {
|
||||
cmdOpsGraph()
|
||||
case "snapshot":
|
||||
cmdOpsSnapshot(args[1:])
|
||||
case "execution":
|
||||
cmdOpsExecution(args[1:])
|
||||
case "assertion":
|
||||
cmdOpsAssertion(args[1:])
|
||||
case "help", "-h", "--help":
|
||||
printOpsUsage()
|
||||
default:
|
||||
@@ -68,7 +73,21 @@ Relation flags:
|
||||
--id <id> --name <name> --from <entity_id> --to <entity_id>
|
||||
--via <function_id> --direction <uni|bi|inverse> --status <status>
|
||||
--purity <pure|impure> --weight <0.0-1.0> --description <desc>
|
||||
--tags <t1,t2> --notes <text>`)
|
||||
--tags <t1,t2> --notes <text>
|
||||
|
||||
Execution commands:
|
||||
fn ops execution add <flags> Registra ejecucion
|
||||
fn ops execution list [--pipeline-id <id>] [-s status]
|
||||
fn ops execution show <id> Muestra ejecucion
|
||||
|
||||
Assertion commands:
|
||||
fn ops assertion add <flags> Añade assertion
|
||||
fn ops assertion list [--entity-id <id>] Lista assertions
|
||||
fn ops assertion show <id> Muestra assertion
|
||||
fn ops assertion delete <id> Elimina assertion
|
||||
fn ops assertion eval --entity-id <id> Evalua assertions activas
|
||||
fn ops assertion result add <flags> Registra resultado manual
|
||||
fn ops assertion result list [--assertion-id <id>]`)
|
||||
}
|
||||
|
||||
// --- ops init ---
|
||||
@@ -722,6 +741,663 @@ func openOpsDB() *ops.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
// --- Execution subcommands ---
|
||||
|
||||
func cmdOpsExecution(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: fn ops execution <add|list|show>")
|
||||
os.Exit(1)
|
||||
}
|
||||
switch args[0] {
|
||||
case "add":
|
||||
cmdOpsExecutionAdd(args[1:])
|
||||
case "list":
|
||||
cmdOpsExecutionList(args[1:])
|
||||
case "show":
|
||||
cmdOpsExecutionShow(args[1:])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown execution command: %s\n", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdOpsExecutionAdd(args []string) {
|
||||
var id, pipelineID, relationID, status, startedAtStr, endedAtStr, errorMsg, metricsStr string
|
||||
var recordsIn, recordsOut *int64
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
switch args[i] {
|
||||
case "--id":
|
||||
i++
|
||||
id = args[i]
|
||||
case "--pipeline-id":
|
||||
i++
|
||||
pipelineID = args[i]
|
||||
case "--relation-id":
|
||||
i++
|
||||
relationID = args[i]
|
||||
case "--status", "-s":
|
||||
i++
|
||||
status = args[i]
|
||||
case "--started-at":
|
||||
i++
|
||||
startedAtStr = args[i]
|
||||
case "--ended-at":
|
||||
i++
|
||||
endedAtStr = args[i]
|
||||
case "--records-in":
|
||||
i++
|
||||
v := parseInt64(args[i])
|
||||
recordsIn = &v
|
||||
case "--records-out":
|
||||
i++
|
||||
v := parseInt64(args[i])
|
||||
recordsOut = &v
|
||||
case "--error":
|
||||
i++
|
||||
errorMsg = args[i]
|
||||
case "--metrics":
|
||||
i++
|
||||
metricsStr = args[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if pipelineID == "" || status == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: --pipeline-id and --status are required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("exec_%d", timeNow().UnixNano())
|
||||
}
|
||||
|
||||
var startedAt time.Time
|
||||
if startedAtStr != "" {
|
||||
var err error
|
||||
startedAt, err = time.Parse(time.RFC3339, startedAtStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid started-at: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
startedAt = timeNow()
|
||||
}
|
||||
|
||||
var endedAt *time.Time
|
||||
if endedAtStr != "" {
|
||||
t, err := time.Parse(time.RFC3339, endedAtStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid ended-at: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
endedAt = &t
|
||||
}
|
||||
|
||||
var metrics map[string]any
|
||||
if metricsStr != "" {
|
||||
if err := json.Unmarshal([]byte(metricsStr), &metrics); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid metrics JSON: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
e := &ops.Execution{
|
||||
ID: id,
|
||||
PipelineID: pipelineID,
|
||||
RelationID: relationID,
|
||||
Status: ops.ExecutionStatus(status),
|
||||
StartedAt: startedAt,
|
||||
EndedAt: endedAt,
|
||||
RecordsIn: recordsIn,
|
||||
RecordsOut: recordsOut,
|
||||
Error: errorMsg,
|
||||
Metrics: metrics,
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
if err := ops.InsertExecutionSafe(db, e); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Recorded execution: %s\n", e.ID)
|
||||
}
|
||||
|
||||
func cmdOpsExecutionList(args []string) {
|
||||
var pipelineID, relationID, status string
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
switch args[i] {
|
||||
case "--pipeline-id":
|
||||
i++
|
||||
pipelineID = args[i]
|
||||
case "--relation-id":
|
||||
i++
|
||||
relationID = args[i]
|
||||
case "-s":
|
||||
i++
|
||||
status = args[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
execs, err := db.ListExecutions(pipelineID, relationID, ops.ExecutionStatus(status))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(execs) == 0 {
|
||||
fmt.Println("No executions.")
|
||||
return
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "ID\tPIPELINE\tSTATUS\tDURATION_MS\tRECORDS_IN\tRECORDS_OUT")
|
||||
for _, e := range execs {
|
||||
dur := "-"
|
||||
if e.DurationMs != nil {
|
||||
dur = fmt.Sprintf("%d", *e.DurationMs)
|
||||
}
|
||||
rin := "-"
|
||||
if e.RecordsIn != nil {
|
||||
rin = fmt.Sprintf("%d", *e.RecordsIn)
|
||||
}
|
||||
rout := "-"
|
||||
if e.RecordsOut != nil {
|
||||
rout = fmt.Sprintf("%d", *e.RecordsOut)
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", e.ID, e.PipelineID, e.Status, dur, rin, rout)
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func cmdOpsExecutionShow(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: fn ops execution show <id>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
e, err := db.GetExecution(args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if e == nil {
|
||||
fmt.Fprintf(os.Stderr, "execution %q not found\n", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("ID: %s\n", e.ID)
|
||||
fmt.Printf("Pipeline: %s\n", e.PipelineID)
|
||||
if e.RelationID != "" {
|
||||
fmt.Printf("Relation: %s\n", e.RelationID)
|
||||
}
|
||||
fmt.Printf("Status: %s\n", e.Status)
|
||||
fmt.Printf("Started at: %s\n", e.StartedAt.Format(time.RFC3339))
|
||||
if e.EndedAt != nil {
|
||||
fmt.Printf("Ended at: %s\n", e.EndedAt.Format(time.RFC3339))
|
||||
}
|
||||
if e.DurationMs != nil {
|
||||
fmt.Printf("Duration: %d ms\n", *e.DurationMs)
|
||||
}
|
||||
if e.RecordsIn != nil {
|
||||
fmt.Printf("Records in: %d\n", *e.RecordsIn)
|
||||
}
|
||||
if e.RecordsOut != nil {
|
||||
fmt.Printf("Records out: %d\n", *e.RecordsOut)
|
||||
}
|
||||
if e.Error != "" {
|
||||
fmt.Printf("Error: %s\n", e.Error)
|
||||
}
|
||||
if len(e.Metrics) > 0 {
|
||||
m, _ := json.MarshalIndent(e.Metrics, " ", " ")
|
||||
fmt.Printf("Metrics: %s\n", string(m))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Assertion subcommands ---
|
||||
|
||||
func cmdOpsAssertion(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: fn ops assertion <add|list|show|delete|eval|result>")
|
||||
os.Exit(1)
|
||||
}
|
||||
switch args[0] {
|
||||
case "add":
|
||||
cmdOpsAssertionAdd(args[1:])
|
||||
case "list":
|
||||
cmdOpsAssertionList(args[1:])
|
||||
case "show":
|
||||
cmdOpsAssertionShow(args[1:])
|
||||
case "delete":
|
||||
cmdOpsAssertionDelete(args[1:])
|
||||
case "eval":
|
||||
cmdOpsAssertionEval(args[1:])
|
||||
case "result":
|
||||
cmdOpsAssertionResult(args[1:])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown assertion command: %s\n", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdOpsAssertionAdd(args []string) {
|
||||
var id, entityID, name, kind, rule, severity, description string
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
switch args[i] {
|
||||
case "--id":
|
||||
i++
|
||||
id = args[i]
|
||||
case "--entity-id":
|
||||
i++
|
||||
entityID = args[i]
|
||||
case "--name":
|
||||
i++
|
||||
name = args[i]
|
||||
case "--kind":
|
||||
i++
|
||||
kind = args[i]
|
||||
case "--rule":
|
||||
i++
|
||||
rule = args[i]
|
||||
case "--severity":
|
||||
i++
|
||||
severity = args[i]
|
||||
case "--description":
|
||||
i++
|
||||
description = args[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if entityID == "" || name == "" || kind == "" || rule == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: --entity-id, --name, --kind, and --rule are required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("assert_%s_%d", name, timeNow().UnixNano())
|
||||
}
|
||||
if severity == "" {
|
||||
severity = "warning"
|
||||
}
|
||||
|
||||
a := &ops.Assertion{
|
||||
ID: id,
|
||||
EntityID: entityID,
|
||||
Name: name,
|
||||
Kind: kind,
|
||||
Rule: rule,
|
||||
Severity: ops.Severity(severity),
|
||||
Description: description,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
if err := ops.InsertAssertionSafe(db, a); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Created assertion: %s\n", a.ID)
|
||||
}
|
||||
|
||||
func cmdOpsAssertionList(args []string) {
|
||||
var entityID string
|
||||
var activeFilter *bool
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
switch args[i] {
|
||||
case "--entity-id":
|
||||
i++
|
||||
entityID = args[i]
|
||||
case "--active":
|
||||
v := true
|
||||
activeFilter = &v
|
||||
case "--inactive":
|
||||
v := false
|
||||
activeFilter = &v
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
assertions, err := db.ListAssertions(entityID, activeFilter)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(assertions) == 0 {
|
||||
fmt.Println("No assertions.")
|
||||
return
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "ID\tENTITY\tNAME\tKIND\tSEVERITY\tACTIVE")
|
||||
for _, a := range assertions {
|
||||
active := "yes"
|
||||
if !a.Active {
|
||||
active = "no"
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", a.ID, a.EntityID, a.Name, a.Kind, a.Severity, active)
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func cmdOpsAssertionShow(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: fn ops assertion show <id>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
a, err := db.GetAssertion(args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if a == nil {
|
||||
fmt.Fprintf(os.Stderr, "assertion %q not found\n", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
active := "yes"
|
||||
if !a.Active {
|
||||
active = "no"
|
||||
}
|
||||
|
||||
fmt.Printf("ID: %s\n", a.ID)
|
||||
fmt.Printf("Entity: %s\n", a.EntityID)
|
||||
fmt.Printf("Name: %s\n", a.Name)
|
||||
fmt.Printf("Kind: %s\n", a.Kind)
|
||||
fmt.Printf("Rule: %s\n", a.Rule)
|
||||
fmt.Printf("Severity: %s\n", a.Severity)
|
||||
fmt.Printf("Description: %s\n", a.Description)
|
||||
fmt.Printf("Active: %s\n", active)
|
||||
fmt.Printf("Created: %s\n", a.CreatedAt.Format(time.RFC3339))
|
||||
|
||||
// Show recent results
|
||||
results, err := db.ListAssertionResults(a.ID, "")
|
||||
if err == nil && len(results) > 0 {
|
||||
fmt.Printf("\nRecent results:\n")
|
||||
limit := 5
|
||||
if len(results) < limit {
|
||||
limit = len(results)
|
||||
}
|
||||
for _, r := range results[:limit] {
|
||||
fmt.Printf(" [%s] %s — %s %s\n", r.EvaluatedAt.Format(time.RFC3339), r.Status, r.Message, formatResultValue(r.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cmdOpsAssertionDelete(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: fn ops assertion delete <id>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
if err := db.DeleteAssertion(args[0]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Deleted assertion: %s\n", args[0])
|
||||
}
|
||||
|
||||
func cmdOpsAssertionEval(args []string) {
|
||||
var entityID, executionID string
|
||||
react := false
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
switch args[i] {
|
||||
case "--entity-id":
|
||||
i++
|
||||
entityID = args[i]
|
||||
case "--execution-id":
|
||||
i++
|
||||
executionID = args[i]
|
||||
case "--react":
|
||||
react = true
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if entityID == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: --entity-id is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
results, err := ops.EvalEntityAssertions(db, entityID, executionID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Println("No active assertions for this entity.")
|
||||
return
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "ASSERTION\tSTATUS\tMESSAGE")
|
||||
for _, r := range results {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", r.AssertionID, r.Status, truncate(r.Message, 60))
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
// Summary
|
||||
pass, fail, skip := 0, 0, 0
|
||||
for _, r := range results {
|
||||
switch r.Status {
|
||||
case ops.ResultPass:
|
||||
pass++
|
||||
case ops.ResultFail:
|
||||
fail++
|
||||
case ops.ResultSkip:
|
||||
skip++
|
||||
}
|
||||
}
|
||||
fmt.Printf("\nResults: %d pass, %d fail, %d skip\n", pass, fail, skip)
|
||||
|
||||
// Reactive loop: apply severity rules
|
||||
if react && fail > 0 {
|
||||
var regDB *registry.DB
|
||||
regDB = tryOpenRegistryDB()
|
||||
// regDB can be nil — React handles it (just won't create proposals)
|
||||
|
||||
rr, err := ops.React(db, regDB, results)
|
||||
if regDB != nil {
|
||||
defer regDB.Close()
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error in reactive loop: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(rr.EntityUpdates) > 0 {
|
||||
fmt.Println("\nEntity status changes:")
|
||||
for _, u := range rr.EntityUpdates {
|
||||
fmt.Printf(" %s: %s -> %s (%s)\n", u.EntityID, u.OldStatus, u.NewStatus, u.Reason)
|
||||
}
|
||||
}
|
||||
if len(rr.Proposals) > 0 {
|
||||
fmt.Println("\nProposals created:")
|
||||
for _, pid := range rr.Proposals {
|
||||
fmt.Printf(" %s\n", pid)
|
||||
}
|
||||
}
|
||||
} else if !react && fail > 0 {
|
||||
fmt.Println("\nTip: use --react to apply severity rules (update entity status, create proposals)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Assertion Result subcommands ---
|
||||
|
||||
func cmdOpsAssertionResult(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: fn ops assertion result <add|list>")
|
||||
os.Exit(1)
|
||||
}
|
||||
switch args[0] {
|
||||
case "add":
|
||||
cmdOpsAssertionResultAdd(args[1:])
|
||||
case "list":
|
||||
cmdOpsAssertionResultList(args[1:])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown assertion result command: %s\n", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdOpsAssertionResultAdd(args []string) {
|
||||
var id, assertionID, executionID, status, valueStr, message string
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
switch args[i] {
|
||||
case "--id":
|
||||
i++
|
||||
id = args[i]
|
||||
case "--assertion-id":
|
||||
i++
|
||||
assertionID = args[i]
|
||||
case "--execution-id":
|
||||
i++
|
||||
executionID = args[i]
|
||||
case "--status":
|
||||
i++
|
||||
status = args[i]
|
||||
case "--value":
|
||||
i++
|
||||
valueStr = args[i]
|
||||
case "--message":
|
||||
i++
|
||||
message = args[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if assertionID == "" || status == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: --assertion-id and --status are required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("ar_%d", timeNow().UnixNano())
|
||||
}
|
||||
|
||||
var value map[string]any
|
||||
if valueStr != "" {
|
||||
if err := json.Unmarshal([]byte(valueStr), &value); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid value JSON: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
ar := &ops.AssertionResult{
|
||||
ID: id,
|
||||
AssertionID: assertionID,
|
||||
ExecutionID: executionID,
|
||||
Status: ops.AssertionResultStatus(status),
|
||||
Value: value,
|
||||
Message: message,
|
||||
EvaluatedAt: timeNow(),
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
if err := db.InsertAssertionResult(ar); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Recorded result: %s (%s)\n", ar.ID, ar.Status)
|
||||
}
|
||||
|
||||
func cmdOpsAssertionResultList(args []string) {
|
||||
var assertionID, executionID string
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
switch args[i] {
|
||||
case "--assertion-id":
|
||||
i++
|
||||
assertionID = args[i]
|
||||
case "--execution-id":
|
||||
i++
|
||||
executionID = args[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
db := openOpsDB()
|
||||
defer db.Close()
|
||||
|
||||
results, err := db.ListAssertionResults(assertionID, executionID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Println("No assertion results.")
|
||||
return
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "ID\tASSERTION\tEXECUTION\tSTATUS\tEVALUATED_AT")
|
||||
for _, r := range results {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", r.ID, r.AssertionID, r.ExecutionID, r.Status, r.EvaluatedAt.Format(time.RFC3339))
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
// --- helpers for new commands ---
|
||||
|
||||
func timeNow() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func parseInt64(s string) int64 {
|
||||
var v int64
|
||||
fmt.Sscanf(s, "%d", &v)
|
||||
return v
|
||||
}
|
||||
|
||||
func formatResultValue(v map[string]any) string {
|
||||
if len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
b, _ := json.Marshal(v)
|
||||
s := string(b)
|
||||
if len(s) > 40 {
|
||||
return s[:37] + "..."
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func tryOpenRegistryDB() *registry.DB {
|
||||
// Try FN_REGISTRY_ROOT env var first
|
||||
if envRoot := os.Getenv("FN_REGISTRY_ROOT"); envRoot != "" {
|
||||
|
||||
Reference in New Issue
Block a user