From 23ef63ca7b9765f3f36d26b65845e612126df4e9 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Thu, 21 May 2026 16:04:18 +0200 Subject: [PATCH] Prerequisite reachability filters **Scope Audited** - Module wiki visibility config and docs. - Toolkit wiki expression engine and visibility tests. - Feat prerequisite rendering/reference behavior. **Changes Made** - Added `hidden_ref(Field, "dataset")` and `all_hidden_refs(..., "dataset")` helpers in [wiki_tables.go](/home/vicky/Projects/nwnee-shadowsoverwestgate/toolkit/internal/topdata/wiki_tables.go:914). - Added feat visibility exclusions in [visibility.yaml](/home/vicky/Projects/nwnee-shadowsoverwestgate/module/topdata/wiki/visibility.yaml:44) for: - `PREREQFEAT1` - `PREREQFEAT2` - `REQSKILL` - `REQSKILL2` - `OrReqFeat0..OrReqFeat4` - Documented the helpers in [README.md](/home/vicky/Projects/nwnee-shadowsoverwestgate/module/topdata/wiki/README.md:53). - Added coverage in [wiki_native_test.go](/home/vicky/Projects/nwnee-shadowsoverwestgate/toolkit/internal/topdata/wiki_native_test.go:646). **Configuration/Schema Impact** - No schema file change. `visibility.yaml` can now use the two new expression helpers. **Compatibility Impact** - Existing visibility expressions are unchanged. - Missing or `****` prerequisite fields are ignored. - Present references that resolve to hidden or missing wiki rows are treated as unreachable. **Tests/Validation** - Confirmed the new test failed before implementation on unknown helper. - Ran `go test ./internal/topdata -run TestWikiVisibility`. - Ran `go test ./internal/topdata`. - Rebuilt toolkit with `./build-tool.sh`. - Ran module wiki build with local toolkit: `SOW_TOOLS_DEV_BINARY=../toolkit/tools/sow-toolkit ./build-wiki.sh` Result: `wiki pages: 771`. --- internal/topdata/wiki_native_test.go | 84 ++++++++++++++++++++++++++++ internal/topdata/wiki_tables.go | 50 +++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/internal/topdata/wiki_native_test.go b/internal/topdata/wiki_native_test.go index 7379111..77e4635 100644 --- a/internal/topdata/wiki_native_test.go +++ b/internal/topdata/wiki_native_test.go @@ -643,6 +643,90 @@ datasets: } } +func TestWikiVisibilityCanExcludeUnreachableFeatPrerequisites(t *testing.T) { + policy, err := parseWikiVisibilityDefinitions([]byte(` +datasets: + feat: + include: + - when: "{{true}}" + exclude: + - when: "{{hidden_ref(PREREQFEAT1, \"feat\")}}" + - when: "{{hidden_ref(REQSKILL, \"skills\")}}" + - when: "{{all_hidden_refs(OrReqFeat0, OrReqFeat1, OrReqFeat2, OrReqFeat3, OrReqFeat4, \"feat\")}}" +`), "visibility.yaml") + if err != nil { + t.Fatalf("parse visibility policy: %v", err) + } + ctx := &wikiContext{ + visibilityPolicy: &policy, + visibleWikiKeys: map[string]bool{ + "feat:visible": true, + "feat:hidden": false, + "skills:visible": true, + "skills:hidden": false, + }, + featIDToKey: map[int]string{1: "feat:visible", 2: "feat:hidden"}, + skillIDToKey: map[int]string{1: "skills:visible", 2: "skills:hidden"}, + } + definition := policy.Datasets["feat"] + featRow := func(values map[string]any) map[string]any { + row := map[string]any{"FEAT": "Test Feat", "DESCRIPTION": "Description"} + for key, value := range values { + row[key] = value + } + return row + } + for name, tt := range map[string]struct { + row map[string]any + want bool + }{ + "visible required feat": { + row: featRow(map[string]any{"PREREQFEAT1": 1}), + want: true, + }, + "hidden required feat": { + row: featRow(map[string]any{"PREREQFEAT1": 2}), + want: false, + }, + "missing required feat is unreachable": { + row: featRow(map[string]any{"PREREQFEAT1": 999}), + want: false, + }, + "visible required skill": { + row: featRow(map[string]any{"REQSKILL": "skills:visible"}), + want: true, + }, + "hidden required skill": { + row: featRow(map[string]any{"REQSKILL": "skills:hidden"}), + want: false, + }, + "null prerequisite fields are ignored": { + row: featRow(map[string]any{"PREREQFEAT1": "****", "REQSKILL": "****"}), + want: true, + }, + "or group with visible option remains reachable": { + row: featRow(map[string]any{"OrReqFeat0": 2, "OrReqFeat1": 1}), + want: true, + }, + "or group with only hidden options is unreachable": { + row: featRow(map[string]any{"OrReqFeat0": 2, "OrReqFeat1": "feat:missing"}), + want: false, + }, + "empty or group is ignored": { + row: featRow(map[string]any{"OrReqFeat0": "****", "OrReqFeat1": "****"}), + want: true, + }, + } { + got, err := ctx.evaluateWikiVisibility("feat", "feat:"+name, tt.row, definition, nil) + if err != nil { + t.Fatalf("%s visibility evaluation failed: %v", name, err) + } + if got != tt.want { + t.Fatalf("%s expected visibility %v, got %v", name, tt.want, got) + } + } +} + func TestWikiVisibilityMetadataOverrideCannotBypassEligibility(t *testing.T) { policy, err := parseWikiVisibilityDefinitions([]byte(` datasets: diff --git a/internal/topdata/wiki_tables.go b/internal/topdata/wiki_tables.go index e1d58a9..a812ffc 100644 --- a/internal/topdata/wiki_tables.go +++ b/internal/topdata/wiki_tables.go @@ -911,11 +911,61 @@ func (p *wikiExprParser) callHelper(name string, args []any) (any, error) { } } return false, nil + case "hidden_ref": + if len(args) != 2 { + return nil, fmt.Errorf("hidden_ref expects 2 arguments") + } + hidden, err := p.hiddenWikiReference(args[0], stringifyWikiTableValue(args[1])) + if err != nil { + return nil, err + } + return hidden, nil + case "all_hidden_refs": + if len(args) < 2 { + return nil, fmt.Errorf("all_hidden_refs expects at least 2 arguments") + } + targetDataset := stringifyWikiTableValue(args[len(args)-1]) + checked := false + for _, value := range args[:len(args)-1] { + if !presentWikiValue(value) { + continue + } + checked = true + hidden, err := p.hiddenWikiReference(value, targetDataset) + if err != nil { + return nil, err + } + if !hidden { + return false, nil + } + } + return checked, nil default: return nil, fmt.Errorf("unknown helper %q", name) } } +func (p *wikiExprParser) hiddenWikiReference(value any, targetDataset string) (bool, error) { + targetDataset = strings.TrimSpace(targetDataset) + if !isWikiVisibilityDataset(targetDataset) { + return false, fmt.Errorf("unknown reference dataset %q", targetDataset) + } + if !presentWikiValue(value) { + return false, nil + } + if p.ctx == nil { + return false, nil + } + key := resolveReferenceKey(value, p.ctx.wikiIDMapForDataset(targetDataset)) + if key == "" { + return true, nil + } + if !strings.HasPrefix(key, targetDataset+":") { + return true, nil + } + return !p.ctx.canReferenceWikiKey(key), nil +} + func presentWikiValue(value any) bool { if isNullLike(value) { return false