Native topdata migration summary - Replaced the legacy 2dabuilder runtime path with the native topdata builder. - Native build, validate, compare, and convert flows now cover the canonical topdata pipeline. - compare-topdata is now a native self-check; legacy reference-builder runtime usage was removed. - Parts generation follows the native asset-scan contract and uses sow-assets via project asset resolution. - Generated feat families, class-feat injects, masterfeat/successor expansion, and dataset-derived feat generation were brought to parity and regression-covered. - Remaining mirrored datasets were migrated into native-owned canonical data while preserving lock IDs. - normalize-topdata bridge behavior and migration-era ownership markers were removed. - Documentation was rewritten around the native workflow and active contracts were cleaned up. - Final hardening pass removed stale validator assumptions and cleaned ignored/generated artifacts for PR readiness. Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/2 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
238 lines
5.5 KiB
Go
238 lines
5.5 KiB
Go
package topdata
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
)
|
|
|
|
func importLegacyMasterfeats(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
|
legacyDir := filepath.Join(referenceBuilderDir, "data", "feat", "masterfeats")
|
|
if _, err := os.Stat(legacyDir); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return 0, nil
|
|
}
|
|
return 0, err
|
|
}
|
|
|
|
targetDir := filepath.Join(dataDir, "masterfeats")
|
|
targetPath := filepath.Join(targetDir, "base.json")
|
|
legacyPlainPath := filepath.Join(targetDir, "masterfeats.json")
|
|
if _, err := os.Stat(targetPath); err == nil {
|
|
obj, err := loadJSONObject(targetPath)
|
|
if err == nil && countLegacyTLKRefsInValue(obj) == 0 {
|
|
return 0, nil
|
|
}
|
|
}
|
|
|
|
collected, err := importLegacyMasterfeatsDataset(legacyDir)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
referenceIDs, err := collectReferenceLockIDs(filepath.Join(referenceBuilderDir, "data"))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
rows := make([]any, 0, len(collected.Rows))
|
|
for _, row := range collected.Rows {
|
|
copyRow, ok := deepCopyValue(row).(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if legacyTLK != nil {
|
|
if _, _, err := inlineLegacyTLKValue(copyRow, legacyTLK); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
copyRow = rewriteImportedIDRefs(copyRow, referenceIDs)
|
|
rows = append(rows, copyRow)
|
|
}
|
|
|
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
|
return 0, err
|
|
}
|
|
writes := []struct {
|
|
path string
|
|
obj map[string]any
|
|
}{
|
|
{
|
|
path: targetPath,
|
|
obj: map[string]any{
|
|
"output": collected.Dataset.OutputName,
|
|
"columns": stringSliceToAny(collected.Columns),
|
|
"rows": rows,
|
|
},
|
|
},
|
|
{
|
|
path: filepath.Join(targetDir, "lock.json"),
|
|
obj: anyMapInt(collected.LockData),
|
|
},
|
|
}
|
|
for _, write := range writes {
|
|
raw, err := json.MarshalIndent(write.obj, "", " ")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
raw = append(raw, '\n')
|
|
if err := os.WriteFile(write.path, raw, 0o644); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
updatedFiles := len(writes)
|
|
if err := os.Remove(legacyPlainPath); err == nil {
|
|
updatedFiles++
|
|
} else if err != nil && !os.IsNotExist(err) {
|
|
return 0, err
|
|
}
|
|
return updatedFiles, nil
|
|
}
|
|
|
|
func importLegacyMasterfeatsDataset(legacyDir string) (nativeCollectedDataset, error) {
|
|
dataset := nativeDataset{
|
|
Name: "masterfeats",
|
|
BasePath: filepath.Join(legacyDir, "masterfeats.json"),
|
|
LockPath: filepath.Join(legacyDir, "lock.json"),
|
|
Spec: specForDataset("masterfeats"),
|
|
}
|
|
tableData, err := loadJSONObject(dataset.BasePath)
|
|
if err != nil {
|
|
return nativeCollectedDataset{}, err
|
|
}
|
|
columns, err := parseColumns(tableData, dataset.Name)
|
|
if err != nil {
|
|
return nativeCollectedDataset{}, err
|
|
}
|
|
rawRows, ok := tableData["rows"].([]any)
|
|
if !ok {
|
|
return nativeCollectedDataset{}, nil
|
|
}
|
|
lockData, err := loadLockfile(dataset.LockPath)
|
|
if err != nil {
|
|
return nativeCollectedDataset{}, err
|
|
}
|
|
usedIDs := map[int]struct{}{}
|
|
for _, rowID := range lockData {
|
|
usedIDs[rowID] = struct{}{}
|
|
}
|
|
|
|
rows := make([]map[string]any, 0, len(rawRows))
|
|
explicitID := make([]bool, 0, len(rawRows))
|
|
for index, raw := range rawRows {
|
|
rowObj, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
rowID := index
|
|
if value, ok := rowObj["id"]; ok {
|
|
parsed, err := asInt(value)
|
|
if err != nil {
|
|
return nativeCollectedDataset{}, err
|
|
}
|
|
rowID = parsed
|
|
}
|
|
row := map[string]any{"id": rowID}
|
|
for _, column := range columns {
|
|
row[column] = nullValue
|
|
}
|
|
for key, value := range rowObj {
|
|
switch key {
|
|
case "id":
|
|
case "key":
|
|
if keyText, ok := value.(string); ok && keyText != "" {
|
|
row["key"] = keyText
|
|
}
|
|
default:
|
|
columnName, ok := canonicalColumn(columns, key)
|
|
if !ok {
|
|
continue
|
|
}
|
|
row[columnName] = value
|
|
}
|
|
}
|
|
rows = append(rows, row)
|
|
_, hasExplicitID := rowObj["id"]
|
|
explicitID = append(explicitID, hasExplicitID)
|
|
if hasExplicitID {
|
|
usedIDs[rowID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
nextID := nextAvailableID(usedIDs)
|
|
lockModified := false
|
|
for index, row := range rows {
|
|
key, hasKey := row["key"].(string)
|
|
if hasKey && key != "" {
|
|
if lockedID, ok := lockData[key]; ok {
|
|
row["id"] = lockedID
|
|
continue
|
|
}
|
|
if explicitID[index] {
|
|
lockData[key] = row["id"].(int)
|
|
} else {
|
|
row["id"] = nextID
|
|
lockData[key] = nextID
|
|
usedIDs[nextID] = struct{}{}
|
|
nextID = nextAvailableID(usedIDs)
|
|
}
|
|
lockModified = true
|
|
continue
|
|
}
|
|
if !explicitID[index] {
|
|
row["id"] = index
|
|
}
|
|
}
|
|
if lockModified {
|
|
_ = lockModified
|
|
}
|
|
|
|
return nativeCollectedDataset{
|
|
Dataset: nativeDataset{
|
|
Name: dataset.Name,
|
|
OutputName: "masterfeats.2da",
|
|
Columns: columns,
|
|
Spec: dataset.Spec,
|
|
},
|
|
Columns: columns,
|
|
Rows: rows,
|
|
LockData: lockData,
|
|
}, nil
|
|
}
|
|
|
|
func rewriteImportedIDRefs(value any, ids map[string]int) map[string]any {
|
|
row, ok := rewriteImportedIDRefsValue(value, ids).(map[string]any)
|
|
if !ok {
|
|
return map[string]any{}
|
|
}
|
|
return row
|
|
}
|
|
|
|
func rewriteImportedIDRefsValue(value any, ids map[string]int) any {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
if len(typed) == 1 {
|
|
if rawID, ok := typed["id"]; ok {
|
|
if key, ok := rawID.(string); ok {
|
|
if resolved, ok := ids[key]; ok {
|
|
return strconv.Itoa(resolved)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out := make(map[string]any, len(typed))
|
|
for key, child := range typed {
|
|
out[key] = rewriteImportedIDRefsValue(child, ids)
|
|
}
|
|
return out
|
|
case []any:
|
|
out := make([]any, 0, len(typed))
|
|
for _, child := range typed {
|
|
out = append(out, rewriteImportedIDRefsValue(child, ids))
|
|
}
|
|
return out
|
|
default:
|
|
return value
|
|
}
|
|
}
|