Closing gaps and cleaning house
This commit is contained in:
@@ -45,6 +45,7 @@ Canonical generated families declare:
|
|||||||
"name_prefix": "Weapon Focus",
|
"name_prefix": "Weapon Focus",
|
||||||
"label_prefix": "FEAT_WEAPON_FOCUS",
|
"label_prefix": "FEAT_WEAPON_FOCUS",
|
||||||
"constant_prefix": "FEAT_WEAPON_FOCUS",
|
"constant_prefix": "FEAT_WEAPON_FOCUS",
|
||||||
|
"legacy_family_keys": ["epic_weapon_focus"],
|
||||||
"default_fields": {
|
"default_fields": {
|
||||||
"DESCRIPTION": {
|
"DESCRIPTION": {
|
||||||
"tlk": {
|
"tlk": {
|
||||||
@@ -86,6 +87,8 @@ Rules:
|
|||||||
`REQSKILL`
|
`REQSKILL`
|
||||||
- `identity_source: "child_source_value"` tells the builder to reuse IDs/keys from the
|
- `identity_source: "child_source_value"` tells the builder to reuse IDs/keys from the
|
||||||
source column when that column already carries feat identity
|
source column when that column already carries feat identity
|
||||||
|
- `legacy_family_keys` declares previous generated-family prefixes that should donate
|
||||||
|
existing locked row IDs to the current family when a family is renamed
|
||||||
- `auto_prereq_fields` binds prerequisite fields to other authored families by
|
- `auto_prereq_fields` binds prerequisite fields to other authored families by
|
||||||
`family_key`, without code changes
|
`family_key`, without code changes
|
||||||
- `allow_existing_only` limits expansion to already-authored child rows when a family is
|
- `allow_existing_only` limits expansion to already-authored child rows when a family is
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ Weapon family shape:
|
|||||||
"name_prefix": "Weapon Focus",
|
"name_prefix": "Weapon Focus",
|
||||||
"label_prefix": "FEAT_WEAPON_FOCUS",
|
"label_prefix": "FEAT_WEAPON_FOCUS",
|
||||||
"constant_prefix": "FEAT_WEAPON_FOCUS",
|
"constant_prefix": "FEAT_WEAPON_FOCUS",
|
||||||
|
"legacy_family_keys": ["epic_weapon_focus"],
|
||||||
"default_fields": {
|
"default_fields": {
|
||||||
"DESCRIPTION": {
|
"DESCRIPTION": {
|
||||||
"tlk": {
|
"tlk": {
|
||||||
@@ -123,6 +124,10 @@ Rules:
|
|||||||
- `apply_after_modules: true` makes the generated family the final authoritative shared
|
- `apply_after_modules: true` makes the generated family the final authoritative shared
|
||||||
layer for legacy explicitly authored child rows, so shared baselines can live in the
|
layer for legacy explicitly authored child rows, so shared baselines can live in the
|
||||||
generated family file instead of `masterfeats` overrides
|
generated family file instead of `masterfeats` overrides
|
||||||
|
- `legacy_family_keys` declares old generated-family prefixes whose existing row IDs
|
||||||
|
should be inherited by the current family; this is how renamed families such as
|
||||||
|
`greater_skill_focus` retaining vanilla `epic_skill_focus` rows preserve hardcoded
|
||||||
|
engine behavior without hardcoding the rename in toolkit logic
|
||||||
- family-expansion files must declare `template`, `family_key`, and `child_source.dataset`
|
- family-expansion files must declare `template`, `family_key`, and `child_source.dataset`
|
||||||
- `child_source.column` is used when expansion is gated by a non-null source field
|
- `child_source.column` is used when expansion is gated by a non-null source field
|
||||||
- `child_source.predicate: "accessible"` currently means `HideFromLevelUp != 1`
|
- `child_source.predicate: "accessible"` currently means `HideFromLevelUp != 1`
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type familyExpansionSpec struct {
|
|||||||
IdentitySource string
|
IdentitySource string
|
||||||
AllowExistingOnly bool
|
AllowExistingOnly bool
|
||||||
AutoPrereqFields map[string]string
|
AutoPrereqFields map[string]string
|
||||||
|
LegacyFamilyKeys []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitFamilyExpansionIdentity(text string) familyIdentity {
|
func splitFamilyExpansionIdentity(text string) familyIdentity {
|
||||||
@@ -114,6 +115,13 @@ func parseFamilyExpansionSpec(path string, obj map[string]any) (familyExpansionS
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
legacyFamilyKeys, err := parseOptionalStringArray(obj["legacy_family_keys"], "legacy_family_keys")
|
||||||
|
if err != nil {
|
||||||
|
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
if err := validateLegacyFamilyKeys(familyKey, legacyFamilyKeys); err != nil {
|
||||||
|
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||||
|
}
|
||||||
allowExistingOnly, err := parseOptionalBoolField(obj["allow_existing_only"], "allow_existing_only")
|
allowExistingOnly, err := parseOptionalBoolField(obj["allow_existing_only"], "allow_existing_only")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||||
@@ -133,9 +141,26 @@ func parseFamilyExpansionSpec(path string, obj map[string]any) (familyExpansionS
|
|||||||
IdentitySource: identitySource,
|
IdentitySource: identitySource,
|
||||||
AllowExistingOnly: allowExistingOnly,
|
AllowExistingOnly: allowExistingOnly,
|
||||||
AutoPrereqFields: autoPrereqFields,
|
AutoPrereqFields: autoPrereqFields,
|
||||||
|
LegacyFamilyKeys: legacyFamilyKeys,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateLegacyFamilyKeys(familyKey string, legacyFamilyKeys []string) error {
|
||||||
|
seen := map[string]string{}
|
||||||
|
normalizedFamilyKey := normalizeKeyIdentity(familyKey)
|
||||||
|
for _, legacyKey := range legacyFamilyKeys {
|
||||||
|
normalizedLegacyKey := normalizeKeyIdentity(legacyKey)
|
||||||
|
if normalizedLegacyKey == normalizedFamilyKey {
|
||||||
|
return fmt.Errorf("legacy_family_keys must not include family_key %q", familyKey)
|
||||||
|
}
|
||||||
|
if previous, ok := seen[normalizedLegacyKey]; ok {
|
||||||
|
return fmt.Errorf("legacy_family_keys contains duplicate-equivalent keys %q and %q", previous, legacyKey)
|
||||||
|
}
|
||||||
|
seen[normalizedLegacyKey] = legacyKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func isFamilyExpansionObject(obj map[string]any) bool {
|
func isFamilyExpansionObject(obj map[string]any) bool {
|
||||||
_, hasFamilyKey := obj["family_key"]
|
_, hasFamilyKey := obj["family_key"]
|
||||||
_, hasTemplate := obj["template"]
|
_, hasTemplate := obj["template"]
|
||||||
@@ -150,9 +175,10 @@ func isFamilyExpansionObject(obj map[string]any) bool {
|
|||||||
_, hasIdentitySource := obj["identity_source"]
|
_, hasIdentitySource := obj["identity_source"]
|
||||||
_, hasAllowExistingOnly := obj["allow_existing_only"]
|
_, hasAllowExistingOnly := obj["allow_existing_only"]
|
||||||
_, hasAutoPrereqFields := obj["auto_prereq_fields"]
|
_, hasAutoPrereqFields := obj["auto_prereq_fields"]
|
||||||
|
_, hasLegacyFamilyKeys := obj["legacy_family_keys"]
|
||||||
return hasFamilyKey || hasTemplate || hasChildSource || hasNamePrefix || hasLabelPrefix ||
|
return hasFamilyKey || hasTemplate || hasChildSource || hasNamePrefix || hasLabelPrefix ||
|
||||||
hasConstantPrefix || hasTemplateFields || hasDefaultFields || hasApplyAfterModules || hasChildRefField ||
|
hasConstantPrefix || hasTemplateFields || hasDefaultFields || hasApplyAfterModules || hasChildRefField ||
|
||||||
hasIdentitySource || hasAllowExistingOnly || hasAutoPrereqFields
|
hasIdentitySource || hasAllowExistingOnly || hasAutoPrereqFields || hasLegacyFamilyKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
func optionalTrimmedString(obj map[string]any, field string) (string, bool) {
|
func optionalTrimmedString(obj map[string]any, field string) (string, bool) {
|
||||||
|
|||||||
+159
-14
@@ -747,6 +747,14 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
lockAdded := 0
|
lockAdded := 0
|
||||||
lockPruned := 0
|
lockPruned := 0
|
||||||
retiredKeys := map[string]struct{}{}
|
retiredKeys := map[string]struct{}{}
|
||||||
|
featFamilyAliases := generatedFamilyAliases{}
|
||||||
|
if dataset.Name == "feat" {
|
||||||
|
var err error
|
||||||
|
featFamilyAliases, err = collectGeneratedFamilyAliases(dataset.GeneratedDir)
|
||||||
|
if err != nil {
|
||||||
|
return nativeCollectedDataset{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for key, rowID := range lockData {
|
for key, rowID := range lockData {
|
||||||
if rowID <= baseBoundaryID {
|
if rowID <= baseBoundaryID {
|
||||||
@@ -834,7 +842,7 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
if parsedID == lockedID {
|
if parsedID == lockedID {
|
||||||
return override, nil
|
return override, nil
|
||||||
}
|
}
|
||||||
if dataset.Name == "feat" && generatedCanonicalLockCanMove(rawKey, parsedID, lockData) {
|
if dataset.Name == "feat" && generatedCanonicalLockCanMove(rawKey, parsedID, lockData, featFamilyAliases) {
|
||||||
delete(lockData, rawKey)
|
delete(lockData, rawKey)
|
||||||
lockModified = true
|
lockModified = true
|
||||||
return override, nil
|
return override, nil
|
||||||
@@ -847,7 +855,7 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
}
|
}
|
||||||
for lockedKey, lockedID := range lockData {
|
for lockedKey, lockedID := range lockData {
|
||||||
if lockedKey != key && lockedID == rowID {
|
if lockedKey != key && lockedID == rowID {
|
||||||
if dataset.Name == "feat" && generatedAliasLockForKey(key, lockedKey) {
|
if dataset.Name == "feat" && generatedAliasLockForKey(key, lockedKey, featFamilyAliases) {
|
||||||
delete(lockData, lockedKey)
|
delete(lockData, lockedKey)
|
||||||
lockModified = true
|
lockModified = true
|
||||||
continue
|
continue
|
||||||
@@ -2396,8 +2404,48 @@ func generatedFamilyPrereqKey(ctx *featGeneratedContext, sourceRow map[string]an
|
|||||||
return featKey, true
|
return featKey, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func generatedFamilyLegacyAliases(familyKey string) []string {
|
type generatedFamilyAliases map[string][]string
|
||||||
switch familyKey {
|
|
||||||
|
func collectGeneratedFamilyAliases(generatedDir string) (generatedFamilyAliases, error) {
|
||||||
|
aliases := generatedFamilyAliases{}
|
||||||
|
if generatedDir == "" {
|
||||||
|
return aliases, nil
|
||||||
|
}
|
||||||
|
paths, err := collectModulePaths(generatedDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, path := range paths {
|
||||||
|
obj, err := loadJSONObject(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isFamilyExpansionObject(obj) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
spec, err := parseFamilyExpansionSpec(path, obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(spec.LegacyFamilyKeys) > 0 {
|
||||||
|
aliases[spec.FamilyKey] = spec.LegacyFamilyKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aliases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configuredGeneratedFamilyLegacyAliases(familyKey string, aliases generatedFamilyAliases) []string {
|
||||||
|
normalized := normalizeKeyIdentity(familyKey)
|
||||||
|
for configuredFamilyKey, configured := range aliases {
|
||||||
|
if normalizeKeyIdentity(configuredFamilyKey) == normalized && len(configured) > 0 {
|
||||||
|
return configured
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compatibilityGeneratedFamilyLegacyAliases(familyKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compatibilityGeneratedFamilyLegacyAliases(familyKey string) []string {
|
||||||
|
switch normalizeKeyIdentity(familyKey) {
|
||||||
case "skillfocus":
|
case "skillfocus":
|
||||||
return []string{"skillfocus"}
|
return []string{"skillfocus"}
|
||||||
case "greaterskillfocus":
|
case "greaterskillfocus":
|
||||||
@@ -2413,18 +2461,94 @@ func generatedFamilyLegacyAliases(familyKey string) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generatedAliasLockForKey(canonicalKey, lockedKey string) bool {
|
func knownGeneratedFamilyKeys(aliases generatedFamilyAliases) []string {
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
keys := make([]string, 0, 32)
|
||||||
|
add := func(key string) {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
for _, key := range []string{
|
||||||
|
"greater_weapon_specialization",
|
||||||
|
"epic_weapon_specialization",
|
||||||
|
"greaterweaponspecialization",
|
||||||
|
"epicweaponspecialization",
|
||||||
|
"overwhelming_critical",
|
||||||
|
"epic_overwhelming_critical",
|
||||||
|
"overwhelmingcritical",
|
||||||
|
"epicoverwhelmingcritical",
|
||||||
|
"greater_weapon_focus",
|
||||||
|
"epic_weapon_focus",
|
||||||
|
"greaterweaponfocus",
|
||||||
|
"epicweaponfocus",
|
||||||
|
"greater_skill_focus",
|
||||||
|
"epic_skill_focus",
|
||||||
|
"greaterskillfocus",
|
||||||
|
"epicskillfocus",
|
||||||
|
"weapon_specialization",
|
||||||
|
"weaponspecialization",
|
||||||
|
"improved_critical",
|
||||||
|
"improvedcritical",
|
||||||
|
"weapon_of_choice",
|
||||||
|
"weaponofchoice",
|
||||||
|
"weapon_focus",
|
||||||
|
"weaponfocus",
|
||||||
|
"skill_focus",
|
||||||
|
"skillfocus",
|
||||||
|
} {
|
||||||
|
add(key)
|
||||||
|
}
|
||||||
|
for familyKey, legacyKeys := range aliases {
|
||||||
|
add(familyKey)
|
||||||
|
for _, legacyKey := range legacyKeys {
|
||||||
|
add(legacyKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.SortFunc(keys, func(a, b string) int {
|
||||||
|
if len(a) == len(b) {
|
||||||
|
return strings.Compare(a, b)
|
||||||
|
}
|
||||||
|
return len(b) - len(a)
|
||||||
|
})
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitKnownGeneratedFamilyIdentity(text string, aliases generatedFamilyAliases) familyIdentity {
|
||||||
|
text = strings.TrimPrefix(strings.TrimSpace(text), "feat:")
|
||||||
|
for _, parent := range knownGeneratedFamilyKeys(aliases) {
|
||||||
|
if text == parent {
|
||||||
|
return familyIdentity{Parent: parent}
|
||||||
|
}
|
||||||
|
prefix := parent + "_"
|
||||||
|
if strings.HasPrefix(text, prefix) {
|
||||||
|
return familyIdentity{
|
||||||
|
Parent: parent,
|
||||||
|
Child: strings.TrimPrefix(text, prefix),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return splitFamilyExpansionIdentity(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatedAliasLockForKey(canonicalKey, lockedKey string, aliases generatedFamilyAliases) bool {
|
||||||
canonicalKey = strings.TrimPrefix(strings.TrimSpace(canonicalKey), "feat:")
|
canonicalKey = strings.TrimPrefix(strings.TrimSpace(canonicalKey), "feat:")
|
||||||
lockedKey = strings.TrimPrefix(strings.TrimSpace(lockedKey), "feat:")
|
lockedKey = strings.TrimPrefix(strings.TrimSpace(lockedKey), "feat:")
|
||||||
if canonicalKey == "" || lockedKey == "" {
|
if canonicalKey == "" || lockedKey == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
identity := splitFamilyExpansionIdentity(canonicalKey)
|
identity := splitKnownGeneratedFamilyIdentity(canonicalKey, aliases)
|
||||||
if identity.Parent == "" || identity.Child == "" {
|
if identity.Parent == "" || identity.Child == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
legacyChildren := legacyFamilyChildAliases(identity.Parent, identity.Child)
|
legacyChildren := legacyFamilyChildAliases(identity.Parent, identity.Child)
|
||||||
for _, alias := range generatedFamilyLegacyAliases(identity.Parent) {
|
for _, alias := range configuredGeneratedFamilyLegacyAliases(identity.Parent, aliases) {
|
||||||
prefix := alias + "_"
|
prefix := alias + "_"
|
||||||
if !strings.HasPrefix(lockedKey, prefix) {
|
if !strings.HasPrefix(lockedKey, prefix) {
|
||||||
continue
|
continue
|
||||||
@@ -2439,18 +2563,38 @@ func generatedAliasLockForKey(canonicalKey, lockedKey string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func generatedCanonicalLockCanMove(canonicalKey string, rowID int, lockData map[string]int) bool {
|
func generatedCanonicalLockCanMove(canonicalKey string, rowID int, lockData map[string]int, aliases generatedFamilyAliases) bool {
|
||||||
if rowID <= 0 {
|
if rowID <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for lockedKey, lockedID := range lockData {
|
for lockedKey, lockedID := range lockData {
|
||||||
if lockedID == rowID && generatedAliasLockForKey(canonicalKey, lockedKey) {
|
if lockedID == rowID && generatedAliasLockForKey(canonicalKey, lockedKey, aliases) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *featGeneratedContext) generatedFamilyAliases() generatedFamilyAliases {
|
||||||
|
aliases := generatedFamilyAliases{}
|
||||||
|
for familyKey, spec := range c.familySpecs {
|
||||||
|
if len(spec.LegacyFamilyKeys) > 0 {
|
||||||
|
aliases[familyKey] = spec.LegacyFamilyKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *featGeneratedContext) generatedFamilyLegacyAliases(familyKey string) []string {
|
||||||
|
normalized := normalizeKeyIdentity(familyKey)
|
||||||
|
for specFamilyKey, spec := range c.familySpecs {
|
||||||
|
if normalizeKeyIdentity(specFamilyKey) == normalized && len(spec.LegacyFamilyKeys) > 0 {
|
||||||
|
return spec.LegacyFamilyKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compatibilityGeneratedFamilyLegacyAliases(familyKey)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *featGeneratedContext) generatedFeatKeyForID(familyKey string, rowID int) (string, bool) {
|
func (c *featGeneratedContext) generatedFeatKeyForID(familyKey string, rowID int) (string, bool) {
|
||||||
if rowID <= 0 {
|
if rowID <= 0 {
|
||||||
return "", false
|
return "", false
|
||||||
@@ -2467,7 +2611,7 @@ func (c *featGeneratedContext) generatedFeatKeyForID(familyKey string, rowID int
|
|||||||
return best, true
|
return best, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, alias := range generatedFamilyLegacyAliases(familyKey) {
|
for _, alias := range c.generatedFamilyLegacyAliases(familyKey) {
|
||||||
for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
|
for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
|
||||||
matches := make([]string, 0)
|
matches := make([]string, 0)
|
||||||
prefix := "feat:" + alias + "_"
|
prefix := "feat:" + alias + "_"
|
||||||
@@ -2489,7 +2633,7 @@ func (c *featGeneratedContext) generatedFeatKeyForID(familyKey string, rowID int
|
|||||||
func (c *featGeneratedContext) generatedFeatKeyFromLegacyAlias(familyKey, sourceSlug string) (string, int, bool) {
|
func (c *featGeneratedContext) generatedFeatKeyFromLegacyAlias(familyKey, sourceSlug string) (string, int, bool) {
|
||||||
targets := legacyFamilyChildAliases(familyKey, sourceSlug)
|
targets := legacyFamilyChildAliases(familyKey, sourceSlug)
|
||||||
canonicalKey := c.preferredGeneratedFeatKey(familyKey, sourceSlug)
|
canonicalKey := c.preferredGeneratedFeatKey(familyKey, sourceSlug)
|
||||||
for _, alias := range generatedFamilyLegacyAliases(familyKey) {
|
for _, alias := range c.generatedFamilyLegacyAliases(familyKey) {
|
||||||
for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
|
for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
|
||||||
prefix := "feat:" + alias + "_"
|
prefix := "feat:" + alias + "_"
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
@@ -2510,7 +2654,8 @@ func (c *featGeneratedContext) generatedFeatKeyFromLegacyAlias(familyKey, source
|
|||||||
|
|
||||||
func legacyFamilyChildAliases(familyKey, sourceSlug string) []string {
|
func legacyFamilyChildAliases(familyKey, sourceSlug string) []string {
|
||||||
aliases := []string{}
|
aliases := []string{}
|
||||||
if familyKey != "greaterskillfocus" && familyKey != "skillfocus" {
|
normalizedFamilyKey := normalizeKeyIdentity(familyKey)
|
||||||
|
if normalizedFamilyKey != "greaterskillfocus" && normalizedFamilyKey != "skillfocus" {
|
||||||
return []string{sourceSlug}
|
return []string{sourceSlug}
|
||||||
}
|
}
|
||||||
switch normalizeKeyIdentity(sourceSlug) {
|
switch normalizeKeyIdentity(sourceSlug) {
|
||||||
@@ -2539,12 +2684,12 @@ func legacyFamilyChildAliases(familyKey, sourceSlug string) []string {
|
|||||||
|
|
||||||
func (c *featGeneratedContext) generatedFeatKeyFromCanonicalAlias(canonicalKey string) (string, int, bool) {
|
func (c *featGeneratedContext) generatedFeatKeyFromCanonicalAlias(canonicalKey string) (string, int, bool) {
|
||||||
stripped := strings.TrimPrefix(strings.TrimSpace(canonicalKey), "feat:")
|
stripped := strings.TrimPrefix(strings.TrimSpace(canonicalKey), "feat:")
|
||||||
identity := splitFamilyExpansionIdentity(stripped)
|
identity := splitKnownGeneratedFamilyIdentity(stripped, c.generatedFamilyAliases())
|
||||||
if identity.Parent == "" || identity.Child == "" {
|
if identity.Parent == "" || identity.Child == "" {
|
||||||
return "", 0, false
|
return "", 0, false
|
||||||
}
|
}
|
||||||
legacyChildren := legacyFamilyChildAliases(identity.Parent, identity.Child)
|
legacyChildren := legacyFamilyChildAliases(identity.Parent, identity.Child)
|
||||||
for _, alias := range generatedFamilyLegacyAliases(identity.Parent) {
|
for _, alias := range c.generatedFamilyLegacyAliases(identity.Parent) {
|
||||||
for _, legacyChild := range legacyChildren {
|
for _, legacyChild := range legacyChildren {
|
||||||
if _, rowID, ok := c.featKeyForFamilyChild(alias, legacyChild); ok {
|
if _, rowID, ok := c.featKeyForFamilyChild(alias, legacyChild); ok {
|
||||||
return "feat:" + identity.Parent + "_" + identity.Child, rowID, true
|
return "feat:" + identity.Parent + "_" + identity.Child, rowID, true
|
||||||
|
|||||||
@@ -1469,11 +1469,10 @@ func validateGeneratedFeatFamilies(dataDir string, report *ValidationReport) {
|
|||||||
{FamilyKey: "weaponofchoice", Dataset: "baseitems", Column: "WeaponOfChoiceFeat"},
|
{FamilyKey: "weaponofchoice", Dataset: "baseitems", Column: "WeaponOfChoiceFeat"},
|
||||||
}
|
}
|
||||||
for _, requirement := range required {
|
for _, requirement := range required {
|
||||||
spec, ok := specs[requirement.FamilyKey]
|
spec, path, ok := generatedFamilySpecByNormalizedKey(specs, specPaths, requirement.FamilyKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
path := specPaths[requirement.FamilyKey]
|
|
||||||
if spec.ChildSource.Dataset != requirement.Dataset ||
|
if spec.ChildSource.Dataset != requirement.Dataset ||
|
||||||
spec.ChildSource.Column != requirement.Column ||
|
spec.ChildSource.Column != requirement.Column ||
|
||||||
spec.ChildSource.Predicate != requirement.Predicate {
|
spec.ChildSource.Predicate != requirement.Predicate {
|
||||||
@@ -1489,6 +1488,20 @@ func validateGeneratedFeatFamilies(dataDir string, report *ValidationReport) {
|
|||||||
validateRequiredProductionGeneratedFamilies(featDataset.GeneratedDir, specs, report)
|
validateRequiredProductionGeneratedFamilies(featDataset.GeneratedDir, specs, report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generatedFamilySpecByNormalizedKey(specs map[string]familyExpansionSpec, specPaths map[string]string, familyKey string) (familyExpansionSpec, string, bool) {
|
||||||
|
normalized := normalizeKeyIdentity(familyKey)
|
||||||
|
for candidateKey, spec := range specs {
|
||||||
|
if normalizeKeyIdentity(candidateKey) == normalized {
|
||||||
|
path := ""
|
||||||
|
if specPaths != nil {
|
||||||
|
path = specPaths[candidateKey]
|
||||||
|
}
|
||||||
|
return spec, path, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return familyExpansionSpec{}, "", false
|
||||||
|
}
|
||||||
|
|
||||||
func validateRequiredProductionGeneratedFamilies(path string, specs map[string]familyExpansionSpec, report *ValidationReport) {
|
func validateRequiredProductionGeneratedFamilies(path string, specs map[string]familyExpansionSpec, report *ValidationReport) {
|
||||||
weaponFamilies := []string{
|
weaponFamilies := []string{
|
||||||
"weaponfocus",
|
"weaponfocus",
|
||||||
@@ -1499,11 +1512,11 @@ func validateRequiredProductionGeneratedFamilies(path string, specs map[string]f
|
|||||||
"greaterweaponspecialization",
|
"greaterweaponspecialization",
|
||||||
}
|
}
|
||||||
for _, familyKey := range weaponFamilies {
|
for _, familyKey := range weaponFamilies {
|
||||||
if _, ok := specs[familyKey]; !ok {
|
if _, _, ok := generatedFamilySpecByNormalizedKey(specs, nil, familyKey); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := specs["weaponofchoice"]; !ok {
|
if _, _, ok := generatedFamilySpecByNormalizedKey(specs, nil, "weaponofchoice"); !ok {
|
||||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
Severity: SeverityError,
|
Severity: SeverityError,
|
||||||
Path: path,
|
Path: path,
|
||||||
@@ -1577,6 +1590,7 @@ func validateGeneratedFeatFamilyCompleteness(path string, spec familyExpansionSp
|
|||||||
"identity_source": spec.IdentitySource,
|
"identity_source": spec.IdentitySource,
|
||||||
"allow_existing_only": spec.AllowExistingOnly,
|
"allow_existing_only": spec.AllowExistingOnly,
|
||||||
"auto_prereq_fields": stringMapToAny(spec.AutoPrereqFields),
|
"auto_prereq_fields": stringMapToAny(spec.AutoPrereqFields),
|
||||||
|
"legacy_family_keys": validationStringSliceToAny(spec.LegacyFamilyKeys),
|
||||||
"child_source": map[string]any{
|
"child_source": map[string]any{
|
||||||
"dataset": spec.ChildSource.Dataset,
|
"dataset": spec.ChildSource.Dataset,
|
||||||
"column": spec.ChildSource.Column,
|
"column": spec.ChildSource.Column,
|
||||||
|
|||||||
@@ -2606,6 +2606,132 @@ func TestResolveGeneratedFeatIdentityUsesLegacyAliasForNoSourceFamily(t *testing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveGeneratedFeatIdentityUsesLegacyAliasForUnderscoreNoSourceFamily(t *testing.T) {
|
||||||
|
ctx := &featGeneratedContext{
|
||||||
|
lockData: map[string]int{
|
||||||
|
"feat:greater_skill_focus_appraise": 1316,
|
||||||
|
"feat:epic_skill_focus_appraise": 588,
|
||||||
|
"feat:epic_skill_focus_animal_empathy": 587,
|
||||||
|
},
|
||||||
|
supplementalID: map[string]int{},
|
||||||
|
tlkStateKeys: map[string]struct{}{},
|
||||||
|
familySpecs: map[string]familyExpansionSpec{
|
||||||
|
"greater_skill_focus": {
|
||||||
|
FamilyKey: "greater_skill_focus",
|
||||||
|
LegacyFamilyKeys: []string{"epic_skill_focus"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
spec := familyExpansionSpec{FamilyKey: "greater_skill_focus"}
|
||||||
|
|
||||||
|
got, rowID, hasID, err := ctx.resolveGeneratedFeatIdentity(spec, "appraise", map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGeneratedFeatIdentity: %v", err)
|
||||||
|
}
|
||||||
|
if got != "feat:greater_skill_focus_appraise" || !hasID || rowID != 588 {
|
||||||
|
t.Fatalf("expected underscore greater skill focus to take legacy alias id 588, got key=%q id=%d hasID=%v", got, rowID, hasID)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, rowID, hasID, err = ctx.resolveGeneratedFeatIdentity(spec, "animal_handling", map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGeneratedFeatIdentity renamed skill: %v", err)
|
||||||
|
}
|
||||||
|
if got != "feat:greater_skill_focus_animal_handling" || !hasID || rowID != 587 {
|
||||||
|
t.Fatalf("expected renamed underscore greater skill focus to take legacy alias id 587, got key=%q id=%d hasID=%v", got, rowID, hasID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGeneratedFeatIdentityTranslatesLegacyWeaponFamilySourceIDWithUnderscoreKey(t *testing.T) {
|
||||||
|
ctx := &featGeneratedContext{
|
||||||
|
lockData: map[string]int{
|
||||||
|
"feat:epic_weapon_focus_club": 619,
|
||||||
|
"feat:epic_weapon_specialization_club": 650,
|
||||||
|
"feat:epic_overwhelming_critical_club": 900,
|
||||||
|
},
|
||||||
|
supplementalID: map[string]int{},
|
||||||
|
tlkStateKeys: map[string]struct{}{},
|
||||||
|
existingFeat: map[string]struct{}{
|
||||||
|
"feat:epic_weapon_focus_club": {},
|
||||||
|
"feat:epic_weapon_specialization_club": {},
|
||||||
|
"feat:epic_overwhelming_critical_club": {},
|
||||||
|
},
|
||||||
|
familySpecs: map[string]familyExpansionSpec{
|
||||||
|
"greater_weapon_focus": {
|
||||||
|
FamilyKey: "greater_weapon_focus",
|
||||||
|
LegacyFamilyKeys: []string{"epic_weapon_focus"},
|
||||||
|
},
|
||||||
|
"greater_weapon_specialization": {
|
||||||
|
FamilyKey: "greater_weapon_specialization",
|
||||||
|
LegacyFamilyKeys: []string{"epic_weapon_specialization"},
|
||||||
|
},
|
||||||
|
"overwhelming_critical": {
|
||||||
|
FamilyKey: "overwhelming_critical",
|
||||||
|
LegacyFamilyKeys: []string{"epic_overwhelming_critical"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
family string
|
||||||
|
want string
|
||||||
|
rowID int
|
||||||
|
}{
|
||||||
|
{family: "greater_weapon_focus", want: "feat:greater_weapon_focus_club", rowID: 619},
|
||||||
|
{family: "greater_weapon_specialization", want: "feat:greater_weapon_specialization_club", rowID: 650},
|
||||||
|
{family: "overwhelming_critical", want: "feat:overwhelming_critical_club", rowID: 900},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
got, rowID, hasID, err := ctx.resolveGeneratedFeatIdentityBySource(tc.family, "club", fmt.Sprintf("%d", tc.rowID))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGeneratedFeatIdentityBySource(%s): %v", tc.family, err)
|
||||||
|
}
|
||||||
|
if got != tc.want || !hasID || rowID != tc.rowID {
|
||||||
|
t.Fatalf("expected %s at id %d, got key=%q id=%d hasID=%v", tc.want, tc.rowID, got, rowID, hasID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGeneratedFeatIdentityUsesConfiguredLegacyFamilyKeys(t *testing.T) {
|
||||||
|
ctx := &featGeneratedContext{
|
||||||
|
lockData: map[string]int{
|
||||||
|
"feat:old_weapon_training_test_club": 4000,
|
||||||
|
},
|
||||||
|
supplementalID: map[string]int{},
|
||||||
|
tlkStateKeys: map[string]struct{}{},
|
||||||
|
familySpecs: map[string]familyExpansionSpec{
|
||||||
|
"new_weapon_training": {
|
||||||
|
FamilyKey: "new_weapon_training",
|
||||||
|
LegacyFamilyKeys: []string{"old_weapon_training"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
spec := familyExpansionSpec{FamilyKey: "new_weapon_training"}
|
||||||
|
|
||||||
|
got, rowID, hasID, err := ctx.resolveGeneratedFeatIdentity(spec, "test_club", map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGeneratedFeatIdentity: %v", err)
|
||||||
|
}
|
||||||
|
if got != "feat:new_weapon_training_test_club" || !hasID || rowID != 4000 {
|
||||||
|
t.Fatalf("expected configured legacy family to donate id 4000, got key=%q id=%d hasID=%v", got, rowID, hasID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !generatedAliasLockForKey(
|
||||||
|
"feat:new_weapon_training_test_club",
|
||||||
|
"feat:old_weapon_training_test_club",
|
||||||
|
generatedFamilyAliases{"new_weapon_training": []string{"old_weapon_training"}},
|
||||||
|
) {
|
||||||
|
t.Fatal("expected configured legacy family lock to be movable")
|
||||||
|
}
|
||||||
|
|
||||||
|
got, rowID, hasID, err = ctx.resolveGeneratedFeatIdentity(familyExpansionSpec{FamilyKey: "newweapontraining"}, "test_club", map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGeneratedFeatIdentity compact family key: %v", err)
|
||||||
|
}
|
||||||
|
if got != "feat:newweapontraining_test_club" || !hasID || rowID != 4000 {
|
||||||
|
t.Fatalf("expected normalized configured legacy lookup to donate id 4000, got key=%q id=%d hasID=%v", got, rowID, hasID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildFamilyExpansionMovesCanonicalLockToLegacyAliasID(t *testing.T) {
|
func TestBuildFamilyExpansionMovesCanonicalLockToLegacyAliasID(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
writeFeatGeneratedHarness(t, root, map[string]string{
|
writeFeatGeneratedHarness(t, root, map[string]string{
|
||||||
@@ -2616,6 +2742,7 @@ func TestBuildFamilyExpansionMovesCanonicalLockToLegacyAliasID(t *testing.T) {
|
|||||||
"name_prefix": "Greater Skill Focus",
|
"name_prefix": "Greater Skill Focus",
|
||||||
"label_prefix": "FEAT_GREATER_SKILL_FOCUS",
|
"label_prefix": "FEAT_GREATER_SKILL_FOCUS",
|
||||||
"constant_prefix": "FEAT_GREATER_SKILL_FOCUS",
|
"constant_prefix": "FEAT_GREATER_SKILL_FOCUS",
|
||||||
|
"legacy_family_keys": ["epicskillfocus"],
|
||||||
"child_ref_field": "REQSKILL",
|
"child_ref_field": "REQSKILL",
|
||||||
"child_source": {"dataset":"skills","predicate":"accessible"},
|
"child_source": {"dataset":"skills","predicate":"accessible"},
|
||||||
"overrides": {"skills:concentration":{"ICON":"ife_greater_concentration"}}
|
"overrides": {"skills:concentration":{"ICON":"ife_greater_concentration"}}
|
||||||
@@ -2650,6 +2777,150 @@ func TestBuildFamilyExpansionMovesCanonicalLockToLegacyAliasID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildFamilyExpansionMovesUnderscoreCanonicalLockToUnderscoreLegacyAliasID(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeFeatGeneratedHarness(t, root, map[string]string{
|
||||||
|
"greater_skill_focus.json": `{
|
||||||
|
"family": "greater_skill_focus",
|
||||||
|
"family_key": "greater_skill_focus",
|
||||||
|
"template": "masterfeats:greaterskillfocus",
|
||||||
|
"name_prefix": "Greater Skill Focus",
|
||||||
|
"label_prefix": "FEAT_GREATER_SKILL_FOCUS",
|
||||||
|
"constant_prefix": "FEAT_GREATER_SKILL_FOCUS",
|
||||||
|
"legacy_family_keys": ["epic_skill_focus"],
|
||||||
|
"child_ref_field": "REQSKILL",
|
||||||
|
"child_source": {"dataset":"skills","predicate":"accessible"},
|
||||||
|
"overrides": {"skills:concentration":{"ICON":"ife_greater_concentration"}}
|
||||||
|
}` + "\n",
|
||||||
|
}, map[string]int{
|
||||||
|
"feat:greater_skill_focus_concentration": 1317,
|
||||||
|
"feat:epic_skill_focus_concentration": 589,
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := BuildNative(testProject(root), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildNative: %v", err)
|
||||||
|
}
|
||||||
|
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "feat.2da"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read feat.2da: %v", err)
|
||||||
|
}
|
||||||
|
text := string(got)
|
||||||
|
if !strings.Contains(text, "589\tFEAT_GREATER_SKILL_FOCUS_CONCENTRATION") {
|
||||||
|
t.Fatalf("expected underscore greater skill focus to move to legacy alias id 589, got:\n%s", text)
|
||||||
|
}
|
||||||
|
lockRaw, err := os.ReadFile(filepath.Join(root, "topdata", "data", "feat", "lock.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read feat lock: %v", err)
|
||||||
|
}
|
||||||
|
lockText := string(lockRaw)
|
||||||
|
if !strings.Contains(lockText, `"feat:greater_skill_focus_concentration": 589`) {
|
||||||
|
t.Fatalf("expected canonical underscore key to own legacy row id 589, got:\n%s", lockText)
|
||||||
|
}
|
||||||
|
if strings.Contains(lockText, "epic_skill_focus_concentration") {
|
||||||
|
t.Fatalf("expected stale underscore epic skill focus lock to be pruned, got:\n%s", lockText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildFamilyExpansionRegeneratesLegacyAliasIDWithoutFeatLock(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeFeatGeneratedHarness(t, root, map[string]string{
|
||||||
|
"greater_skill_focus.json": `{
|
||||||
|
"family": "greater_skill_focus",
|
||||||
|
"family_key": "greater_skill_focus",
|
||||||
|
"template": "masterfeats:greaterskillfocus",
|
||||||
|
"name_prefix": "Greater Skill Focus",
|
||||||
|
"label_prefix": "FEAT_GREATER_SKILL_FOCUS",
|
||||||
|
"constant_prefix": "FEAT_GREATER_SKILL_FOCUS",
|
||||||
|
"legacy_family_keys": ["epic_skill_focus"],
|
||||||
|
"child_ref_field": "REQSKILL",
|
||||||
|
"child_source": {"dataset":"skills","predicate":"accessible"},
|
||||||
|
"overrides": {"skills:concentration":{"ICON":"ife_greater_concentration"}}
|
||||||
|
}` + "\n",
|
||||||
|
}, map[string]int{})
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
|
||||||
|
"output": "feat.2da",
|
||||||
|
"compare_reference": false,
|
||||||
|
"columns": [
|
||||||
|
"LABEL","FEAT","DESCRIPTION","ICON","MINATTACKBONUS","MINSTR","MINDEX","MININT","MINWIS","MINCON","MINCHA",
|
||||||
|
"MINSPELLLVL","PREREQFEAT1","PREREQFEAT2","GAINMULTIPLE","EFFECTSSTACK","ALLCLASSESCANUSE","CATEGORY","MAXCR",
|
||||||
|
"SPELLID","SUCCESSOR","CRValue","USESPERDAY","MASTERFEAT","TARGETSELF","OrReqFeat0","OrReqFeat1","OrReqFeat2",
|
||||||
|
"OrReqFeat3","OrReqFeat4","REQSKILL","ReqSkillMinRanks","REQSKILL2","ReqSkillMinRanks2","Constant","TOOLSCATEGORIES",
|
||||||
|
"HostileFeat","MinLevel","MinLevelClass","MaxLevel","MinFortSave","PreReqEpic","ReqAction"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": 589,
|
||||||
|
"key": "feat:epic_skill_focus_concentration",
|
||||||
|
"LABEL": "FEAT_EPIC_SKILL_FOCUS_CONCENTRATION",
|
||||||
|
"REQSKILL": 1,
|
||||||
|
"MASTERFEAT": 5,
|
||||||
|
"Constant": "FEAT_EPIC_SKILL_FOCUS_CONCENTRATION"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
result, err := BuildNative(testProject(root), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildNative: %v", err)
|
||||||
|
}
|
||||||
|
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "feat.2da"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read feat.2da: %v", err)
|
||||||
|
}
|
||||||
|
text := string(got)
|
||||||
|
if !strings.Contains(text, "589\tFEAT_GREATER_SKILL_FOCUS_CONCENTRATION") {
|
||||||
|
t.Fatalf("expected empty lockfile rebuild to preserve legacy row id 589, got:\n%s", text)
|
||||||
|
}
|
||||||
|
lockRaw, err := os.ReadFile(filepath.Join(root, "topdata", "data", "feat", "lock.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read feat lock: %v", err)
|
||||||
|
}
|
||||||
|
lockText := string(lockRaw)
|
||||||
|
if !strings.Contains(lockText, `"feat:greater_skill_focus_concentration": 589`) {
|
||||||
|
t.Fatalf("expected regenerated lock to assign greater key to row 589, got:\n%s", lockText)
|
||||||
|
}
|
||||||
|
if strings.Contains(lockText, "epic_skill_focus_concentration") {
|
||||||
|
t.Fatalf("expected regenerated lock to prune legacy epic key, got:\n%s", lockText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFamilyExpansionRejectsInvalidLegacyFamilyKeys(t *testing.T) {
|
||||||
|
base := map[string]any{
|
||||||
|
"family": "greater_skill_focus",
|
||||||
|
"family_key": "greater_skill_focus",
|
||||||
|
"template": "masterfeats:greaterskillfocus",
|
||||||
|
"child_source": map[string]any{"dataset": "skills", "predicate": "accessible"},
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
keys []any
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "self alias",
|
||||||
|
keys: []any{"greater_skill_focus"},
|
||||||
|
errMsg: "legacy_family_keys must not include family_key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normalized duplicate",
|
||||||
|
keys: []any{"epicskillfocus", "epic_skill_focus"},
|
||||||
|
errMsg: "legacy_family_keys contains duplicate-equivalent keys",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
obj := map[string]any{}
|
||||||
|
for key, value := range base {
|
||||||
|
obj[key] = value
|
||||||
|
}
|
||||||
|
obj["legacy_family_keys"] = tc.keys
|
||||||
|
_, err := parseFamilyExpansionSpec(tc.name+".json", obj)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tc.errMsg) {
|
||||||
|
t.Fatalf("%s: expected %q error, got %v", tc.name, tc.errMsg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPruneRetiredGeneratedFeatTLKEntries(t *testing.T) {
|
func TestPruneRetiredGeneratedFeatTLKEntries(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "generated"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "generated"))
|
||||||
|
|||||||
Reference in New Issue
Block a user