From 997f2b50bf36b8c4337f2122fbcdf00b23a5c71d Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:12:03 +0100 Subject: [PATCH 1/9] fix(params): restore the projection to the canonical cue@v0.16.1 output The previous change regenerated this file with `charly candy params`, which is NOT the instrument this repo is graded on. The org's reusable candy-validate gate and this repo family's CI run plain `cue exp gengotypes` at cue@v0.16.1; the charly wrapper applies a yaml retag and reflows doc comments, so its output differs from the canonical one on the SAME schema. Measured with the canonical instrument against main after that change: cue@v0.16.1 gengotypes vs the committed file -> 14 diff lines (FAIL) Restoring the file to exactly what cue@v0.16.1 emits, so the committed projection matches the gate that grades it. Nothing else changes. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (analysed on a live system) --- candy/plugin-omarchy/params/cue_types_gen.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/candy/plugin-omarchy/params/cue_types_gen.go b/candy/plugin-omarchy/params/cue_types_gen.go index 8aa9b58..10a7947 100644 --- a/candy/plugin-omarchy/params/cue_types_gen.go +++ b/candy/plugin-omarchy/params/cue_types_gen.go @@ -8,25 +8,25 @@ type OmarchyInput struct { // omarchy-shell IPC (the SAME surface the menu and hotkeys use); the // cli-read methods run the omarchy-* bin commands (the acceptance // surfaces the beds assert). - Method string `yaml:"method,omitempty" json:"method"` + Method string `json:"method"` // args — the omarchy CLI args (method: cli) or the weather-location // args (method: weather-location). Required for those two methods. - Args string `yaml:"args,omitempty" json:"args,omitempty"` + Args string `json:"args,omitempty"` // plugin — the omarchy. id for shell-summon / shell-hide. - Plugin string `yaml:"plugin,omitempty" json:"plugin,omitempty"` + Plugin string `json:"plugin,omitempty"` // payload — the JSON payload for shell-summon (optional). - Payload string `yaml:"payload,omitempty" json:"payload,omitempty"` + Payload string `json:"payload,omitempty"` // title / text — the notification title/body for shell-notifications-send. - Title string `yaml:"title,omitempty" json:"title,omitempty"` + Title string `json:"title,omitempty"` - Text string `yaml:"text,omitempty" json:"text,omitempty"` + Text string `json:"text,omitempty"` // expect_non_zero: assert the command FAILED (any non-zero exit) — the // CLI-rejects-X class. Mirrors plugin-command's expect_non_zero; mutually // exclusive with the step-level exit_status matcher (exact code). - ExpectNonZero bool `yaml:"expect_non_zero,omitempty" json:"expect_non_zero"` + ExpectNonZero bool `json:"expect_non_zero"` } From cb99d305ea885b6c051f92f03936b10404c4dab1 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:17:12 +0100 Subject: [PATCH 2/9] chore(pr): re-freeze so the validator reads the corrected body Body-only round: the decode-path proof (JSON never YAML), the filed issue, and the untrimmed module gate are now in the PR body. A REST re-run replays the original event payload and re-reads the stale body, so a new head is the way to make the gate read what GitHub serves. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed) From d6d4de53a652fcf6f646b7780f1c6cbcb973ffea Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:21:48 +0100 Subject: [PATCH 3/9] chore(pr): re-freeze for the executed-decode evidence round Body now carries executed proof instead of static reading: the same JSON decode run against BOTH projections of the schema, with identical results, plus literal grep output. A REST re-run replays the original payload, so a new head is what makes the gate read the served body. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed) From e03a414b66e4ea35561bc7e2bc2bd00393423611 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:25:06 +0100 Subject: [PATCH 4/9] test(params): pin the projection's JSON decode, the only path these structs travel The tag question is a runtime question, so it gets runtime coverage instead of a prose claim. The new test builds a wire map keyed by the struct's json tags, decodes it through json.Unmarshal -- the call every plugin provider makes -- and fails if any json-tagged field comes back empty. That is the failure a generated projection can actually have: a tag drifting from its wire key silently drops configuration. Measured alongside it: the same decode against origin/main's yaml-tagged projection of the same schema populates the identical field set, so the yaml tags are inert on this path. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (analysed on a live system) --- candy/plugin-omarchy/params/decode_test.go | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 candy/plugin-omarchy/params/decode_test.go diff --git a/candy/plugin-omarchy/params/decode_test.go b/candy/plugin-omarchy/params/decode_test.go new file mode 100644 index 0000000..9d3e880 --- /dev/null +++ b/candy/plugin-omarchy/params/decode_test.go @@ -0,0 +1,83 @@ +package params + +// decode_test.go — the ONE runtime path these generated structs participate in, pinned. +// +// charly hands a plugin its parameters as JSON (`spec.Op`'s params_json) and the plugin decodes +// that JSON into this projection; the sdk's kit.DecodeInput re-marshals to JSON before it +// unmarshals too. NO YAML decoder ever sees these types — that is why the projection's `yaml:` +// tags are inert, and why this test pins the JSON side rather than asserting the claim in prose. +// +// It fails if a json tag drifts away from the wire key: the field it belongs to would silently +// stop receiving its configured value, which is the failure mode a generated projection can have. + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" +) + +func TestProjectionJSONDecodePopulatesTaggedFields(t *testing.T) { + rt := reflect.TypeOf(OmarchyInput{}) + + wire := map[string]any{} + fillable := map[int]string{} // field index -> json key the probe can populate + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + if f.PkgPath != "" { + continue // unexported + } + key := strings.Split(f.Tag.Get("json"), ",")[0] + if key == "" || key == "-" { + continue + } + var v any + switch f.Type.Kind() { + case reflect.String: + v = "probe" + case reflect.Bool: + v = true + case reflect.Int, reflect.Int64: + v = 7 + case reflect.Float64: + v = 1.5 + case reflect.Slice: + v = []any{"probe"} + case reflect.Map: + v = map[string]any{"k": "v"} + default: + // structs/pointers/interfaces are not filled by a flat wire map; skip them rather than + // assert something this probe cannot exercise. + continue + } + wire[key] = v + fillable[i] = key + } + if len(fillable) == 0 { + t.Fatal("no json-tagged field the probe can populate — the test would prove nothing") + } + + b, err := json.Marshal(wire) + if err != nil { + t.Fatalf("marshal the wire map: %v", err) + } + var got OmarchyInput + // THE PATH: json -> struct, exactly `json.Unmarshal(req.GetParamsJson(), &op)`. + if err := json.Unmarshal(b, &got); err != nil { + t.Fatalf("decode the wire map into the projection: %v", err) + } + + rv := reflect.ValueOf(got) + missing := []string{} + for i, key := range fillable { + if rv.Field(i).IsZero() { + missing = append(missing, fmt.Sprintf("%s (field %s)", key, rt.Field(i).Name)) + } + } + if len(missing) > 0 { + t.Fatalf("%d of %d json-tagged fields were NOT populated by the decode: %v", + len(missing), len(fillable), missing) + } + t.Logf("decoded %d/%d json-tagged fields from the wire map", len(fillable), len(fillable)) +} From fbdbc191bcf13c968f2622791d160e44004e669f Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:27:35 +0100 Subject: [PATCH 5/9] test(params): anchor the wire keys to the schema, not to the projection's own tags The first version of this test derived each expected wire key from the struct tag it was checking, which made it vacuous: a drifted tag takes its derived expectation with it, so the decode still lines up and the test still passes. Correcting a block that named exactly that. The expected keys now come from candy//schema/*.cue -- the schema that GENERATES this file -- hardcoded on purpose. A tag that drifts, empties or disappears leaves a schema key with no field carrying it and the test fails. Demonstrated: mutating json:"method" to json:"methd" turns it red with the schema-drift message, and restoring turns it green. --- candy/plugin-omarchy/params/decode_test.go | 88 ++++++++++++++-------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/candy/plugin-omarchy/params/decode_test.go b/candy/plugin-omarchy/params/decode_test.go index 9d3e880..112b624 100644 --- a/candy/plugin-omarchy/params/decode_test.go +++ b/candy/plugin-omarchy/params/decode_test.go @@ -1,14 +1,18 @@ package params -// decode_test.go — the ONE runtime path these generated structs participate in, pinned. +// decode_test.go — the ONE runtime path these generated structs participate in, pinned against +// the SOURCE OF TRUTH. // // charly hands a plugin its parameters as JSON (`spec.Op`'s params_json) and the plugin decodes // that JSON into this projection; the sdk's kit.DecodeInput re-marshals to JSON before it -// unmarshals too. NO YAML decoder ever sees these types — that is why the projection's `yaml:` +// unmarshals too. No YAML decoder ever sees these types — which is why the projection's `yaml:` // tags are inert, and why this test pins the JSON side rather than asserting the claim in prose. // -// It fails if a json tag drifts away from the wire key: the field it belongs to would silently -// stop receiving its configured value, which is the failure mode a generated projection can have. +// The expected wire keys below are copied from `candy/plugin-omarchy/schema/*.cue` — the schema that +// GENERATES this file. They are hardcoded on purpose: deriving them from the struct's own `json` +// tags (the obvious thing to do) makes the test vacuous, because a tag that drifts takes its +// derived expectation with it and the decode still lines up. Anchored to the schema instead, a +// drifted, emptied or removed tag leaves the field unpopulated and fails here. import ( "encoding/json" @@ -18,66 +22,90 @@ import ( "testing" ) -func TestProjectionJSONDecodePopulatesTaggedFields(t *testing.T) { - rt := reflect.TypeOf(OmarchyInput{}) +// schemaKeys is the wire-key set declared by candy/plugin-omarchy/schema/*.cue. +var schemaKeys = map[string]bool{ + "args": true, + "expect_non_zero": true, + "method": true, + "payload": true, + "plugin": true, + "text": true, + "title": true, +} +func TestProjectionMatchesSchemaWireKeys(t *testing.T) { + rt := reflect.TypeOf(OmarchyInput{}) + ptr := reflect.New(rt) wire := map[string]any{} - fillable := map[int]string{} // field index -> json key the probe can populate + + exported := 0 for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) if f.PkgPath != "" { continue // unexported } + exported++ key := strings.Split(f.Tag.Get("json"), ",")[0] if key == "" || key == "-" { - continue + t.Fatalf("field %s carries no usable json tag (%q) — the schema's key cannot reach it", f.Name, key) + } + if !schemaKeys[key] { + t.Fatalf("field %s is tagged json:%q, which the schema candy/plugin-omarchy/schema does NOT declare — the projection has drifted from its source of truth", f.Name, key) } - var v any switch f.Type.Kind() { case reflect.String: - v = "probe" + wire[key] = "probe" case reflect.Bool: - v = true + wire[key] = true case reflect.Int, reflect.Int64: - v = 7 + wire[key] = 7 case reflect.Float64: - v = 1.5 + wire[key] = 1.5 case reflect.Slice: - v = []any{"probe"} + wire[key] = []any{"probe"} case reflect.Map: - v = map[string]any{"k": "v"} + wire[key] = map[string]any{"k": "v"} default: - // structs/pointers/interfaces are not filled by a flat wire map; skip them rather than - // assert something this probe cannot exercise. + // nested structs/pointers/interfaces are not filled by a flat wire map continue } - wire[key] = v - fillable[i] = key } - if len(fillable) == 0 { - t.Fatal("no json-tagged field the probe can populate — the test would prove nothing") + if exported == 0 { + t.Fatal("the projection has no exported fields — the test would prove nothing") } b, err := json.Marshal(wire) if err != nil { t.Fatalf("marshal the wire map: %v", err) } - var got OmarchyInput // THE PATH: json -> struct, exactly `json.Unmarshal(req.GetParamsJson(), &op)`. - if err := json.Unmarshal(b, &got); err != nil { + if err := json.Unmarshal(b, ptr.Interface()); err != nil { t.Fatalf("decode the wire map into the projection: %v", err) } - rv := reflect.ValueOf(got) + elem := ptr.Elem() missing := []string{} - for i, key := range fillable { - if rv.Field(i).IsZero() { - missing = append(missing, fmt.Sprintf("%s (field %s)", key, rt.Field(i).Name)) + for key := range wire { + found := false + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + if f.PkgPath != "" { + continue + } + if strings.Split(f.Tag.Get("json"), ",")[0] != key { + continue + } + found = true + if elem.Field(i).IsZero() { + missing = append(missing, fmt.Sprintf("%s (field %s)", key, f.Name)) + } + } + if !found { + missing = append(missing, key+" (no field carries it)") } } if len(missing) > 0 { - t.Fatalf("%d of %d json-tagged fields were NOT populated by the decode: %v", - len(missing), len(fillable), missing) + t.Fatalf("%d wire key(s) were not delivered into the projection: %v", len(missing), missing) } - t.Logf("decoded %d/%d json-tagged fields from the wire map", len(fillable), len(fillable)) + t.Logf("verified %d exported field(s) against the schema's wire keys and decoded %d of them", exported, len(wire)) } From 6adcac0073300806fbc665b4fb3829678d9e9e22 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:33:13 +0100 Subject: [PATCH 6/9] test(params): check the projection against its schema in BOTH directions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous version iterated the struct's own tags to build the wire map, so its schema-to-struct arm was unreachable: every key in the map necessarily had a field. That left a dropped schema key undetectable, which is exactly the drift the test claims to catch. Both directions now run against the #Input definition in candy//schema/*.cue, hardcoded on purpose: struct -> schema catches a drifted/emptied/removed tag, and schema -> struct catches a key the projection dropped. Two executed negative controls prove it — a mutated tag and a deleted field each turn it red with a precise message, and restoring turns it green. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (analysed on a live system) --- candy/plugin-omarchy/params/decode_test.go | 81 ++++++++++++---------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/candy/plugin-omarchy/params/decode_test.go b/candy/plugin-omarchy/params/decode_test.go index 112b624..26c45c5 100644 --- a/candy/plugin-omarchy/params/decode_test.go +++ b/candy/plugin-omarchy/params/decode_test.go @@ -1,18 +1,17 @@ package params -// decode_test.go — the ONE runtime path these generated structs participate in, pinned against -// the SOURCE OF TRUTH. +// decode_test.go — pins this generated projection against its SOURCE OF TRUTH, in both directions. // // charly hands a plugin its parameters as JSON (`spec.Op`'s params_json) and the plugin decodes // that JSON into this projection; the sdk's kit.DecodeInput re-marshals to JSON before it // unmarshals too. No YAML decoder ever sees these types — which is why the projection's `yaml:` -// tags are inert, and why this test pins the JSON side rather than asserting the claim in prose. +// tags are inert, and why this test pins the JSON side rather than asserting that claim in prose. // -// The expected wire keys below are copied from `candy/plugin-omarchy/schema/*.cue` — the schema that -// GENERATES this file. They are hardcoded on purpose: deriving them from the struct's own `json` -// tags (the obvious thing to do) makes the test vacuous, because a tag that drifts takes its -// derived expectation with it and the decode still lines up. Anchored to the schema instead, a -// drifted, emptied or removed tag leaves the field unpopulated and fails here. +// The expected keys below are copied from the `#OmarchyInput` definition in +// `candy/plugin-omarchy/schema/*.cue` — the schema that GENERATES this file. They are hardcoded on +// purpose: deriving them from the struct's own `json` tags (the obvious thing) makes the test +// vacuous, because a tag that drifts takes its derived expectation with it and the decode still +// lines up. import ( "encoding/json" @@ -22,23 +21,26 @@ import ( "testing" ) -// schemaKeys is the wire-key set declared by candy/plugin-omarchy/schema/*.cue. -var schemaKeys = map[string]bool{ - "args": true, - "expect_non_zero": true, - "method": true, - "payload": true, - "plugin": true, - "text": true, - "title": true, +// schemaKeys is the field set of #OmarchyInput, taken from candy/plugin-omarchy/schema. +var schemaKeys = []string{ + "args", + "expect_non_zero", + "method", + "payload", + "plugin", + "text", + "title", } -func TestProjectionMatchesSchemaWireKeys(t *testing.T) { +func TestProjectionMatchesItsSchema(t *testing.T) { rt := reflect.TypeOf(OmarchyInput{}) ptr := reflect.New(rt) - wire := map[string]any{} + // Direction 1: STRUCT -> SCHEMA. Every exported field must carry a json key the schema + // declares; a drifted, emptied or removed tag is caught here. + wire := map[string]any{} exported := 0 + tagOf := map[string]string{} // json key -> Go field name for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) if f.PkgPath != "" { @@ -47,11 +49,12 @@ func TestProjectionMatchesSchemaWireKeys(t *testing.T) { exported++ key := strings.Split(f.Tag.Get("json"), ",")[0] if key == "" || key == "-" { - t.Fatalf("field %s carries no usable json tag (%q) — the schema's key cannot reach it", f.Name, key) + t.Fatalf("field %s carries no usable json tag (%q) — no schema key can reach it", f.Name, key) } - if !schemaKeys[key] { - t.Fatalf("field %s is tagged json:%q, which the schema candy/plugin-omarchy/schema does NOT declare — the projection has drifted from its source of truth", f.Name, key) + if !contains(schemaKeys, key) { + t.Fatalf("field %s is tagged json:%q, which #OmarchyInput in candy/plugin-omarchy/schema does NOT declare — the projection has drifted from its source of truth", f.Name, key) } + tagOf[key] = f.Name switch f.Type.Kind() { case reflect.String: wire[key] = "probe" @@ -74,6 +77,15 @@ func TestProjectionMatchesSchemaWireKeys(t *testing.T) { t.Fatal("the projection has no exported fields — the test would prove nothing") } + // Direction 2: SCHEMA -> STRUCT. Every key the schema declares must be carried by a field; + // a key the projection dropped is caught here, and it is checked against schemaKeys directly + // rather than against anything derived from the tags. + for _, key := range schemaKeys { + if _, ok := tagOf[key]; !ok { + t.Fatalf("schema key %q is declared by #OmarchyInput but NO exported field carries it — the projection is missing part of its schema", key) + } + } + b, err := json.Marshal(wire) if err != nil { t.Fatalf("marshal the wire map: %v", err) @@ -82,30 +94,27 @@ func TestProjectionMatchesSchemaWireKeys(t *testing.T) { if err := json.Unmarshal(b, ptr.Interface()); err != nil { t.Fatalf("decode the wire map into the projection: %v", err) } - elem := ptr.Elem() missing := []string{} for key := range wire { - found := false for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) - if f.PkgPath != "" { - continue - } - if strings.Split(f.Tag.Get("json"), ",")[0] != key { - continue - } - found = true - if elem.Field(i).IsZero() { + if f.PkgPath == "" && strings.Split(f.Tag.Get("json"), ",")[0] == key && elem.Field(i).IsZero() { missing = append(missing, fmt.Sprintf("%s (field %s)", key, f.Name)) } } - if !found { - missing = append(missing, key+" (no field carries it)") - } } if len(missing) > 0 { t.Fatalf("%d wire key(s) were not delivered into the projection: %v", len(missing), missing) } - t.Logf("verified %d exported field(s) against the schema's wire keys and decoded %d of them", exported, len(wire)) + t.Logf("both directions hold: %d exported field(s), %d schema key(s), %d decoded", exported, len(schemaKeys), len(wire)) +} + +func contains(hay []string, needle string) bool { + for _, h := range hay { + if h == needle { + return true + } + } + return false } From 89d7d4246f2547ec8538fb34c2e304dc4958a6e9 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:42:54 +0100 Subject: [PATCH 7/9] chore(pr): re-freeze for the verbatim-evidence round Body corrected: the predecessor number was wrong (self-referential in two PRs), and the three go test captures are now raw output pasted as-is, with the per-repo controls driven by a field each repo actually has. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed) From b2b4193877a6d609229f8fa7481fdf6abe0dc787 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Mon, 2 Nov 2026 00:02:30 +0100 Subject: [PATCH 8/9] chore(pr): re-freeze for the measurement-reconciliation round The body now pastes git's own diff --stat against origin/main and names the plain-diff count as the same pair aligned differently, instead of quoting one number the PR page contradicts. The round RCA names the missed control for the one-directional test: the negative control exercised only the arm I had already reasoned about, so the arm that could not fire was never asked to fire. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed) From 6a50e713609f561e9f5425ab688a8c9e79d502e8 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Mon, 2 Nov 2026 00:07:28 +0100 Subject: [PATCH 9/9] chore(pr): re-freeze for the per-repo measurement round The body inherited a sibling repo's alignment note; this repo's two views agree exactly at this file's own numbers, and the round RCA now carries them instead of another PR's. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed)