Phase 5: scaffold Crucible (sow-tools) — dispatcher + builder shims, container, PR-first CI, fail-closed (D11, D19).
This commit is contained in:
@@ -1,407 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type wikiVisibilityDefinitions struct {
|
||||
ReferenceSets map[string]wikiVisibilityReferenceSet `json:"reference_sets" yaml:"reference_sets"`
|
||||
Datasets map[string]wikiVisibilityDataset `json:"datasets" yaml:"datasets"`
|
||||
}
|
||||
|
||||
type wikiVisibilityDataset struct {
|
||||
Eligibility wikiVisibilityPredicate `json:"eligibility" yaml:"eligibility"`
|
||||
Include []wikiVisibilityRule `json:"include" yaml:"include"`
|
||||
Exclude []wikiVisibilityRule `json:"exclude" yaml:"exclude"`
|
||||
}
|
||||
|
||||
type wikiVisibilityPredicate struct {
|
||||
When string `json:"when" yaml:"when"`
|
||||
}
|
||||
|
||||
type wikiVisibilityRule struct {
|
||||
When string `json:"when" yaml:"when"`
|
||||
ReferenceSet string `json:"reference_set" yaml:"reference_set"`
|
||||
}
|
||||
|
||||
type wikiVisibilityReferenceSet struct {
|
||||
TargetDataset string `json:"target_dataset" yaml:"target_dataset"`
|
||||
Sources []wikiVisibilityReferenceSource `json:"sources" yaml:"sources"`
|
||||
}
|
||||
|
||||
type wikiVisibilityReferenceSource struct {
|
||||
Dataset string `json:"dataset" yaml:"dataset"`
|
||||
TableField string `json:"table_field" yaml:"table_field"`
|
||||
TableKind string `json:"table_kind" yaml:"table_kind"`
|
||||
RowRefField string `json:"row_ref_field" yaml:"row_ref_field"`
|
||||
ExpandMasterFeat bool `json:"expand_masterfeat" yaml:"expand_masterfeat"`
|
||||
}
|
||||
|
||||
func loadWikiVisibilityDefinitions(path string) (*wikiVisibilityDefinitions, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read wiki visibility definitions %s: %w", path, err)
|
||||
}
|
||||
policy, err := parseWikiVisibilityDefinitions(raw, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
func parseWikiVisibilityDefinitions(raw []byte, path string) (wikiVisibilityDefinitions, error) {
|
||||
var policy wikiVisibilityDefinitions
|
||||
if err := yaml.Unmarshal(raw, &policy); err != nil {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("parse wiki visibility definitions %s: %w", path, err)
|
||||
}
|
||||
if policy.ReferenceSets == nil {
|
||||
policy.ReferenceSets = map[string]wikiVisibilityReferenceSet{}
|
||||
}
|
||||
if policy.Datasets == nil {
|
||||
policy.Datasets = map[string]wikiVisibilityDataset{}
|
||||
}
|
||||
for dataset, definition := range policy.Datasets {
|
||||
if !isWikiVisibilityDataset(dataset) {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: unknown dataset %q", path, dataset)
|
||||
}
|
||||
if strings.TrimSpace(definition.Eligibility.When) != "" {
|
||||
if err := validateWikiVisibilityExpression(definition.Eligibility.When); err != nil {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s eligibility: %w", path, dataset, err)
|
||||
}
|
||||
}
|
||||
for _, rules := range [][]wikiVisibilityRule{definition.Include, definition.Exclude} {
|
||||
for _, rule := range rules {
|
||||
if strings.TrimSpace(rule.When) == "" && strings.TrimSpace(rule.ReferenceSet) == "" {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s rule requires when or reference_set", path, dataset)
|
||||
}
|
||||
if strings.TrimSpace(rule.When) != "" {
|
||||
if err := validateWikiVisibilityExpression(rule.When); err != nil {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s rule: %w", path, dataset, err)
|
||||
}
|
||||
}
|
||||
if set := strings.TrimSpace(rule.ReferenceSet); set != "" {
|
||||
referenceSet, ok := policy.ReferenceSets[set]
|
||||
if !ok {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s references unknown reference_set %q", path, dataset, set)
|
||||
}
|
||||
if referenceSet.TargetDataset != dataset {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s cannot use reference_set %q targeting %s", path, dataset, set, referenceSet.TargetDataset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for name, referenceSet := range policy.ReferenceSets {
|
||||
if !isWikiVisibilityDataset(referenceSet.TargetDataset) {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s has unknown target_dataset %q", path, name, referenceSet.TargetDataset)
|
||||
}
|
||||
if len(referenceSet.Sources) == 0 {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s requires sources", path, name)
|
||||
}
|
||||
for _, source := range referenceSet.Sources {
|
||||
if !isWikiVisibilityDataset(source.Dataset) {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s has unknown source dataset %q", path, name, source.Dataset)
|
||||
}
|
||||
if strings.TrimSpace(source.TableField) == "" || strings.TrimSpace(source.RowRefField) == "" {
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s source %s requires table_field and row_ref_field", path, name, source.Dataset)
|
||||
}
|
||||
switch source.TableKind {
|
||||
case "class_feats", "class_bonus_feats", "race_feats":
|
||||
default:
|
||||
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s source %s has unsupported table_kind %q", path, name, source.Dataset, source.TableKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func isWikiVisibilityDataset(name string) bool {
|
||||
switch name {
|
||||
case "baseitems", "classes", "feat", "racialtypes", "skills", "spells":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validateWikiVisibilityExpression(expression string) error {
|
||||
parser := wikiExprParser{
|
||||
tokens: tokenizeWikiExpr(wikiExpressionBody(expression)),
|
||||
row: map[string]any{},
|
||||
allowMissingFields: true,
|
||||
}
|
||||
if _, err := parser.parseComparison(); err != nil {
|
||||
return err
|
||||
}
|
||||
if parser.peek().kind != wikiExprEOF {
|
||||
return fmt.Errorf("unexpected token %q", parser.peek().text)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wikiExpressionBody(expression string) string {
|
||||
expression = strings.TrimSpace(expression)
|
||||
if strings.HasPrefix(expression, "{{") && strings.HasSuffix(expression, "}}") {
|
||||
return strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(expression, "{{"), "}}"))
|
||||
}
|
||||
return expression
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) loadWikiVisibility(path string) error {
|
||||
policy, err := loadWikiVisibilityDefinitions(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.visibilityPolicy = policy
|
||||
if policy == nil {
|
||||
ctx.visibleWikiKeys = nil
|
||||
return nil
|
||||
}
|
||||
ctx.visibleWikiKeys, err = ctx.buildWikiVisibilityIndex(policy)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) buildWikiVisibilityIndex(policy *wikiVisibilityDefinitions) (map[string]bool, error) {
|
||||
visible := map[string]bool{}
|
||||
for _, dataset := range []string{"classes", "racialtypes", "skills", "spells", "baseitems", "feat"} {
|
||||
definition, ok := policy.Datasets[dataset]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rows := ctx.wikiRowsForDataset(dataset)
|
||||
for _, key := range sortedKeys(rows) {
|
||||
row := rows[key]
|
||||
include, err := ctx.evaluateWikiVisibilityWithoutSets(dataset, key, row, definition)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
visible[key] = include
|
||||
}
|
||||
}
|
||||
referenceSets := ctx.collectWikiVisibilityReferenceSets(policy, visible)
|
||||
previousVisible := ctx.visibleWikiKeys
|
||||
ctx.visibleWikiKeys = visible
|
||||
defer func() {
|
||||
ctx.visibleWikiKeys = previousVisible
|
||||
}()
|
||||
for pass := 0; pass <= len(visible); pass++ {
|
||||
changed := false
|
||||
for _, dataset := range []string{"classes", "racialtypes", "skills", "spells", "baseitems", "feat"} {
|
||||
definition, ok := policy.Datasets[dataset]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rows := ctx.wikiRowsForDataset(dataset)
|
||||
for _, key := range sortedKeys(rows) {
|
||||
include, err := ctx.evaluateWikiVisibility(dataset, key, rows[key], definition, referenceSets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if visible[key] != include {
|
||||
changed = true
|
||||
}
|
||||
visible[key] = include
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return visible, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("wiki visibility index did not converge")
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) evaluateWikiVisibilityWithoutSets(dataset, key string, row map[string]any, definition wikiVisibilityDataset) (bool, error) {
|
||||
return ctx.evaluateWikiVisibility(dataset, key, row, definition, map[string]map[string]struct{}{})
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) evaluateWikiVisibility(dataset, key string, row map[string]any, definition wikiVisibilityDataset, referenceSets map[string]map[string]struct{}) (bool, error) {
|
||||
if row == nil || !ctx.wikiRowCanRender(dataset, row) {
|
||||
return false, nil
|
||||
}
|
||||
if expression := strings.TrimSpace(definition.Eligibility.When); expression != "" {
|
||||
eligible, err := ctx.evalWikiVisibilityPredicate(expression, row)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("evaluate wiki visibility dataset %s eligibility for %s: %w", dataset, key, err)
|
||||
}
|
||||
if !eligible {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
included := len(definition.Include) == 0
|
||||
for _, rule := range definition.Include {
|
||||
match, err := ctx.matchWikiVisibilityRule(rule, key, row, referenceSets)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("evaluate wiki visibility dataset %s include for %s: %w", dataset, key, err)
|
||||
}
|
||||
included = included || match
|
||||
}
|
||||
excluded := false
|
||||
for _, rule := range definition.Exclude {
|
||||
match, err := ctx.matchWikiVisibilityRule(rule, key, row, referenceSets)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("evaluate wiki visibility dataset %s exclude for %s: %w", dataset, key, err)
|
||||
}
|
||||
excluded = excluded || match
|
||||
}
|
||||
switch ctx.wikiGenerateOverride(row) {
|
||||
case "0":
|
||||
return false, nil
|
||||
case "1":
|
||||
return true, nil
|
||||
default:
|
||||
return included && !excluded, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) matchWikiVisibilityRule(rule wikiVisibilityRule, key string, row map[string]any, referenceSets map[string]map[string]struct{}) (bool, error) {
|
||||
match := false
|
||||
if expression := strings.TrimSpace(rule.When); expression != "" {
|
||||
value, err := ctx.evalWikiVisibilityPredicate(expression, row)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
match = match || value
|
||||
}
|
||||
if set := strings.TrimSpace(rule.ReferenceSet); set != "" {
|
||||
_, ok := referenceSets[set][key]
|
||||
match = match || ok
|
||||
}
|
||||
return match, nil
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) evalWikiVisibilityPredicate(expression string, row map[string]any) (bool, error) {
|
||||
parser := wikiExprParser{
|
||||
tokens: tokenizeWikiExpr(wikiExpressionBody(expression)),
|
||||
row: row,
|
||||
ctx: ctx,
|
||||
allowMissingFields: true,
|
||||
}
|
||||
value, err := parser.parseComparison()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if parser.peek().kind != wikiExprEOF {
|
||||
return false, fmt.Errorf("unexpected token %q", parser.peek().text)
|
||||
}
|
||||
return truthyWikiTableValue(value), nil
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) collectWikiVisibilityReferenceSets(policy *wikiVisibilityDefinitions, visible map[string]bool) map[string]map[string]struct{} {
|
||||
out := map[string]map[string]struct{}{}
|
||||
names := make([]string, 0, len(policy.ReferenceSets))
|
||||
for name := range policy.ReferenceSets {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
keys := map[string]struct{}{}
|
||||
for _, source := range policy.ReferenceSets[name].Sources {
|
||||
for key, row := range ctx.wikiRowsForDataset(source.Dataset) {
|
||||
if !visible[key] {
|
||||
continue
|
||||
}
|
||||
table := ctx.tableForValue(fieldValue(row, source.TableField), ctx.wikiVisibilityTables(source.TableKind))
|
||||
if table == nil {
|
||||
continue
|
||||
}
|
||||
for _, tableRow := range table.Rows {
|
||||
refKey := resolveReferenceKey(fieldValue(tableRow, source.RowRefField), ctx.wikiIDMapForDataset(policy.ReferenceSets[name].TargetDataset))
|
||||
if refKey == "" {
|
||||
refKey = resolveReferenceKey(fieldValue(tableRow, source.RowRefField), nil)
|
||||
}
|
||||
if refKey == "" {
|
||||
continue
|
||||
}
|
||||
if source.ExpandMasterFeat && strings.HasPrefix(refKey, "masterfeats:") {
|
||||
for _, featKey := range ctx.masterFeatGroupKeys(refKey) {
|
||||
keys[featKey] = struct{}{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
keys[refKey] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
out[name] = keys
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) wikiRowsForDataset(dataset string) map[string]map[string]any {
|
||||
switch dataset {
|
||||
case "baseitems":
|
||||
return ctx.baseitemRows
|
||||
case "classes":
|
||||
return ctx.classRows
|
||||
case "feat":
|
||||
return ctx.featRows
|
||||
case "racialtypes":
|
||||
return ctx.raceRows
|
||||
case "skills":
|
||||
return ctx.skillRows
|
||||
case "spells":
|
||||
return ctx.spellRows
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) wikiIDMapForDataset(dataset string) map[int]string {
|
||||
switch dataset {
|
||||
case "classes":
|
||||
return ctx.classIDToKey
|
||||
case "feat":
|
||||
return ctx.featIDToKey
|
||||
case "racialtypes":
|
||||
return ctx.raceIDToKey
|
||||
case "skills":
|
||||
return ctx.skillIDToKey
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) wikiVisibilityTables(kind string) map[string]wikiTable {
|
||||
switch kind {
|
||||
case "class_feats":
|
||||
return ctx.classFeatTables
|
||||
case "class_bonus_feats":
|
||||
return ctx.classBonusTables
|
||||
case "race_feats":
|
||||
return ctx.raceFeatTables
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) masterFeatGroupKeys(group string) []string {
|
||||
keys := []string{}
|
||||
for key, row := range ctx.featRows {
|
||||
if masterfeatGroupKey(fieldValue(row, "MASTERFEAT"), ctx.masterfeatRows) == group {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) canReferenceWikiKey(key string) bool {
|
||||
if key == "" || ctx.visibilityPolicy == nil {
|
||||
return true
|
||||
}
|
||||
visible, configured := ctx.visibleWikiKeys[key]
|
||||
if configured {
|
||||
return visible
|
||||
}
|
||||
dataset := strings.SplitN(key, ":", 2)[0]
|
||||
_, configured = ctx.visibilityPolicy.Datasets[dataset]
|
||||
return !configured
|
||||
}
|
||||
Reference in New Issue
Block a user