chore: avance acumulado de sesiones previas (reorg dev/issues + ajustes)

Reorganizacion de dev/issues en subcarpetas (completed/, cpp/, gamedev/,
kanban/, trading/, imagegen/, matrix/) y cambios acumulados en cmd/fn/pyrunner,
.claude/commands y settings. Trabajo de otro LLM/sesion, commiteado a peticion
del usuario para desbloquear el working tree. Excluido logs/ardour_mcp_server.log (ruido).
This commit is contained in:
2026-06-30 14:43:51 +02:00
parent 5501507588
commit a3f75d61ec
125 changed files with 421 additions and 203 deletions
+50 -34
View File
@@ -18,6 +18,7 @@ type pyParam struct {
Default string // empty if required
IsKwargs bool // **kwargs
IsRegistry bool // type is a registry type (needs factory)
KwOnly bool // declared after a bare "*" or "*args" — must be passed by keyword
}
// pyFactory links a registry type to the function that creates it.
@@ -45,12 +46,21 @@ func parsePySignature(sig string) []pyParam {
// Split by comma, respecting nested brackets
parts := splitParams(raw)
var params []pyParam
kwOnly := false
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" || part == "self" || part == "cls" {
continue
}
// A bare "*" (PEP 3102) or "*args" var-positional marks the start of
// keyword-only params. Neither maps cleanly to positional CLI args, so
// skip the marker itself and flag every following param as keyword-only.
if part == "*" || (strings.HasPrefix(part, "*") && !strings.HasPrefix(part, "**")) {
kwOnly = true
continue
}
p := parseSingleParam(part)
p.KwOnly = kwOnly
params = append(params, p)
}
return params
@@ -189,11 +199,19 @@ func generatePyRunner(fn *registry.Function, db *registry.DB, registryRoot strin
// Classify params
var factoryImports []string // import lines for factories
var factorySetup []string // code to create factory objects
var argLines []string // code to parse CLI args
var callArgs []string // arguments to pass to the function
var bodyLines []string // code that fills _call_args / _call_kwargs
cliArgIdx := 0
// emitCall appends one param to _call_args (positional) or _call_kwargs
// (keyword-only). indent prefixes the line (for params read inside an `if`).
emitCall := func(p pyParam, indent string) string {
if p.KwOnly {
return fmt.Sprintf("%s_call_kwargs[%q] = %s", indent, p.Name, p.Name)
}
return fmt.Sprintf("%s_call_args.append(%s)", indent, p.Name)
}
for _, p := range params {
if p.IsKwargs {
// Skip **kwargs for now — can't auto-resolve from CLI
@@ -235,27 +253,35 @@ func generatePyRunner(fn *registry.Function, db *registry.DB, registryRoot strin
fmt.Sprintf("%s = %s(%s)", p.Name, factory.FuncName,
strings.Join(factoryArgs, ", ")))
callArgs = append(callArgs, p.Name)
// Factory objects are always present (required).
bodyLines = append(bodyLines, emitCall(p, ""))
} else {
// Primitive type — from CLI args
// Primitive type — from CLI args.
if p.Default != "" {
// Optional param with default
argLines = append(argLines,
fmt.Sprintf("%s = _args[%d] if len(_args) > %d else %s",
p.Name, cliArgIdx, cliArgIdx, convertDefault(p.Type, p.Default)))
argLines = append(argLines,
convertArg(p.Name, p.Type, true))
// Optional: only pass when the CLI arg is present. When absent we
// DON'T replicate the signature default (it may reference a module
// constant that doesn't exist in this runner) — we simply omit the
// argument so the function applies its own native default.
bodyLines = append(bodyLines,
fmt.Sprintf("if len(_args) > %d:", cliArgIdx))
bodyLines = append(bodyLines,
fmt.Sprintf(" %s = _args[%d]", p.Name, cliArgIdx))
if conv := convertArg(p.Name, p.Type, true); conv != "" {
bodyLines = append(bodyLines, " "+conv)
}
bodyLines = append(bodyLines, emitCall(p, " "))
} else {
// Required param
argLines = append(argLines,
// Required param.
bodyLines = append(bodyLines,
fmt.Sprintf("if len(_args) <= %d: sys.exit('error: missing required arg: %s (%s)')",
cliArgIdx, p.Name, p.Type))
argLines = append(argLines,
bodyLines = append(bodyLines,
fmt.Sprintf("%s = _args[%d]", p.Name, cliArgIdx))
argLines = append(argLines,
convertArg(p.Name, p.Type, false))
if conv := convertArg(p.Name, p.Type, false); conv != "" {
bodyLines = append(bodyLines, conv)
}
bodyLines = append(bodyLines, emitCall(p, ""))
}
callArgs = append(callArgs, p.Name)
cliArgIdx++
}
}
@@ -289,18 +315,18 @@ func generatePyRunner(fn *registry.Function, db *registry.DB, registryRoot strin
sb.WriteString("\n")
}
// Arg parsing
if len(argLines) > 0 {
sb.WriteString("# --- parse CLI args ---\n")
for _, line := range argLines {
sb.WriteString(line + "\n")
}
sb.WriteString("\n")
// Arg parsing — build the positional/keyword argument collections.
sb.WriteString("# --- parse CLI args ---\n")
sb.WriteString("_call_args = []\n")
sb.WriteString("_call_kwargs = {}\n")
for _, line := range bodyLines {
sb.WriteString(line + "\n")
}
sb.WriteString("\n")
// Call
sb.WriteString("# --- execute ---\n")
sb.WriteString(fmt.Sprintf("_result = %s(%s)\n", fn.Name, strings.Join(callArgs, ", ")))
sb.WriteString(fmt.Sprintf("_result = %s(*_call_args, **_call_kwargs)\n", fn.Name))
sb.WriteString("\n")
// Output
@@ -365,16 +391,6 @@ func convertArg(name, typ string, _ bool) string {
}
}
// convertDefault ensures the default value is valid Python for the given type.
func convertDefault(_, def string) string {
// Most defaults from the signature are already valid Python
// Just handle the None case for Optional types
if def == "None" || def == "" {
return "None"
}
return def
}
// pythonList creates a Python list literal from strings: ["a", "b", "c"]
func pythonList(items []string) string {
quoted := make([]string, len(items))
+141
View File
@@ -0,0 +1,141 @@
package main
import (
"os"
"os/exec"
"strings"
"testing"
"fn-registry/registry"
)
// Signature with a bare "*" (PEP 3102) separating positional from keyword-only
// params. This is the shape that used to make fn run emit "* = _args[3]".
const kwOnlySig = "def add_event_dav(summary: str, start: str, end: str = '', *, location: str = '', all_day: bool = False) -> dict"
func TestParsePySignatureBareStarKeywordOnly(t *testing.T) {
params := parsePySignature(kwOnlySig)
// The bare "*" marker must never surface as a real parameter.
for _, p := range params {
if p.Name == "*" {
t.Fatalf("bare '*' leaked as a param: %+v", params)
}
}
want := map[string]bool{ // name -> expected KwOnly
"summary": false,
"start": false,
"end": false,
"location": true,
"all_day": true,
}
if len(params) != len(want) {
t.Fatalf("got %d params, want %d: %+v", len(params), len(want), params)
}
for _, p := range params {
kw, ok := want[p.Name]
if !ok {
t.Errorf("unexpected param %q", p.Name)
continue
}
if p.KwOnly != kw {
t.Errorf("param %q KwOnly=%v, want %v", p.Name, p.KwOnly, kw)
}
}
}
func TestGeneratePyRunnerKeywordOnlyValid(t *testing.T) {
fn := &registry.Function{
Name: "add_event_dav",
Lang: "py",
FilePath: "python/functions/pipelines/add_event_dav.py",
Signature: kwOnlySig,
}
// All params are primitive, so no factory lookup happens and db is unused.
script, err := generatePyRunner(fn, nil, "")
if err != nil {
t.Fatalf("generatePyRunner: %v", err)
}
if strings.Contains(script, "* = _args") {
t.Fatalf("runner emitted invalid syntax '* = _args':\n%s", script)
}
// The signature default DEFAULT_BASE_URL (a module constant) must NOT be
// replicated into the runner — that NameErrors at runtime.
if strings.Contains(script, "DEFAULT_BASE_URL") {
t.Errorf("runner replicated non-literal default DEFAULT_BASE_URL:\n%s", script)
}
// Required positionals are appended; keyword-only optionals go to kwargs.
for _, want := range []string{
"_call_args.append(summary)",
"_call_args.append(start)",
`_call_kwargs["location"] = location`,
`_call_kwargs["all_day"] = all_day`,
"_result = add_event_dav(*_call_args, **_call_kwargs)",
} {
if !strings.Contains(script, want) {
t.Errorf("missing %q in generated runner:\n%s", want, script)
}
}
// The generated runner must itself be valid Python (compile, don't run).
mustCompilePython(t, script)
}
// mustCompilePython checks the script parses as valid Python via py_compile.
func mustCompilePython(t *testing.T, script string) {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "runner_*.py")
if err != nil {
t.Fatalf("temp file: %v", err)
}
if _, err := f.WriteString(script); err != nil {
t.Fatalf("write: %v", err)
}
f.Close()
py := pythonBinForTest()
out, err := exec.Command(py, "-m", "py_compile", f.Name()).CombinedOutput()
if err != nil {
t.Fatalf("generated runner is not valid Python (%s): %v\n%s", py, err, out)
}
}
// pythonBinForTest prefers the project venv, falling back to python3 on PATH.
func pythonBinForTest() string {
for _, c := range []string{"../../python/.venv/bin/python3", "python3"} {
if c == "python3" {
return c
}
if _, err := os.Stat(c); err == nil {
return c
}
}
return "python3"
}
// A "*args" var-positional marker must behave like the bare "*": skipped, and
// everything after it treated as keyword-only.
func TestParsePySignatureVarargsKeywordOnly(t *testing.T) {
sig := "def f(a: str, *args, b: int = 0) -> dict"
params := parsePySignature(sig)
for _, p := range params {
if strings.HasPrefix(p.Name, "*") {
t.Fatalf("'*args' marker leaked as a param: %+v", params)
}
}
if len(params) != 2 {
t.Fatalf("got %d params, want 2: %+v", len(params), params)
}
got := map[string]bool{}
for _, p := range params {
got[p.Name] = p.KwOnly
}
if got["a"] != false || got["b"] != true {
t.Errorf("KwOnly mismatch: a=%v (want false), b=%v (want true)", got["a"], got["b"])
}
}