// Package credentialform refuses what the catalogue's tree holds about credentials and routes that no manifest judge // sees: a module that provides and keeps a `bound` state the provisioner does not read, and a route received as a file. // It judges no offer's credential form: that rule (an own secret, an issuer, or `form` pair or none; the lead, // 2026-10-29) is part of the catalogue's rules, which the checker's judge runs on every change (CatalogueProblems, // novox/hq ADR 0578 ยง2); this package called it itself until 2026-11. // // It refuses a route received as a file too (issue 774, research 072 row 34, the operator on 2026-10-31: "there is only // 1 path, the other is simply deprecated/invalid"): a route is a provision, served from the seat mesh-provisioner's // plans alone, and a manifest's `receives.route` is the deprecated path. A module still moving off it is listed in // stillReceivingRoutes with why; every refusal names them all, and a listed module that no longer receives one is // refused too, so the list only shrinks. package credentialform import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" ) // Problems is every module of the manifests under root/modules that keeps a `bound` state the provisioner does not read, // and every route received as a file, one line each. func Problems(root string) ([]string, error) { files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) if err != nil { return nil, err } sort.Strings(files) var out []string var receiving []string reads := map[string]bool{} var binders []string for _, f := range files { raw, err := os.ReadFile(f) if err != nil { return nil, err } m, err := catalogue.Decode(raw) if err != nil { return nil, fmt.Errorf("%s: %w", f, err) } if _, received := m.Receives["route"]; received { receiving = append(receiving, m.Module) } else if _, listed := stillReceivingRoutes[m.Module]; listed { out = append(out, fmt.Sprintf("%s no longer receives route; take it off stillReceivingRoutes", m.Module)) } var kept struct { Reads []string `json:"reads"` State []json.RawMessage `json:"state"` } _ = json.Unmarshal(raw, &kept) // reads may hold objects in another module; only the provisioner's are read if m.Module == "mesh-provisioner" { for _, r := range kept.Reads { reads[r] = true } } if len(m.Provides) > 0 && keepsBound(kept.State) { binders = append(binders, m.Module) } } for _, module := range binders { if !reads[module+".bound"] { out = append(out, fmt.Sprintf("%s provides and keeps a `bound` state, and mesh-provisioner's manifest does not read %s.bound", module, module)) } } for _, module := range receiving { if _, listed := stillReceivingRoutes[module]; !listed { out = append(out, fmt.Sprintf("%s receives route as a file: a route is a provision, served from the seat "+ "mesh-provisioner's plans alone (issue 774); still on the deprecated path: %s", module, strings.Join(receiving, ", "))) } } return out, nil } // stillReceivingRoutes is every module still receiving its routes as a file, and why it has not moved yet. var stillReceivingRoutes = map[string]string{ "route-adapter": "migration scaffolding writing into a predecessor's proxy (novox/hq ADR 0104), deleted when that " + "migration ends; it reads the received file its TypeScript adapter turns into the predecessor's route files", } // keepsBound says whether a manifest's `state` names `bound`, as a plain name or an object with a name. func keepsBound(state []json.RawMessage) bool { for _, s := range state { var name string var obj struct { Name string `json:"name"` } if json.Unmarshal(s, &name) == nil && name == "bound" || json.Unmarshal(s, &obj) == nil && obj.Name == "bound" { return true } } return false }