Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"github.com/google/uuid"
)
// MigratorFixture is a minimal offline dump for dry-run without MySQL.
// It is NOT a substitute for validating against production MySQL.
type MigratorFixture struct {
Companies []struct {
ID string `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
} `json:"companies"`
Users []struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Active bool `json:"active"`
} `json:"users"`
AdminUsers []struct {
UserID string `json:"user_id"`
Email string `json:"email"`
} `json:"admin_users"`
Profiles []struct {
CompanyID string `json:"company_id"`
UserID string `json:"user_id"`
Role string `json:"role"`
Status string `json:"status"`
} `json:"profiles"`
XMLFeeds []struct {
ID int64 `json:"id"`
CompanyID string `json:"company_id"`
Name string `json:"name"`
FieldMappings json.RawMessage `json:"field_mappings"`
} `json:"xml_feeds"`
Files []struct {
ID int64 `json:"id"`
CompanyID string `json:"company_id"`
FileName string `json:"file_name"`
} `json:"files"`
RawProducts int `json:"raw_products_count"`
}
func loadFixture(path string) (*MigratorFixture, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var f MigratorFixture
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return &f, nil
}
// runFixtureDryRun remaps fixture rows and writes id-map + validation-style counts
// without connecting to MySQL or Postgres.
func runFixtureDryRun(fixturePath, mapsDir, idMapPath string) {
fx, err := loadFixture(fixturePath)
if err != nil {
log.Fatalf("fixture: %v", err)
}
if err := os.MkdirAll(mapsDir, 0o755); err != nil {
log.Fatalf("maps dir: %v", err)
}
outIDMap := idMapPath
if outIDMap == "" {
outIDMap = filepath.Join(mapsDir, "id-map.json")
}
report := map[string]int{}
companyMap := map[string]string{}
userMap := map[string]string{}
feedMap := map[string]string{}
fileMap := map[string]string{}
for _, c := range fx.Companies {
companyMap[c.ID] = uuid.New().String()
report["companies"]++
}
for _, u := range fx.Users {
userMap[u.ID] = uuid.New().String()
report["users"]++
}
for _, a := range fx.AdminUsers {
if _, ok := userMap[a.UserID]; ok {
report["admin_users"]++
} else {
report["admin_users_unmatched"]++
}
}
for _, p := range fx.Profiles {
if _, okC := companyMap[p.CompanyID]; !okC {
report["memberships_skipped"]++
continue
}
if _, okU := userMap[p.UserID]; !okU {
report["memberships_skipped"]++
continue
}
report["memberships"]++
}
for _, f := range fx.XMLFeeds {
if _, ok := companyMap[f.CompanyID]; !ok {
report["input_feeds_skipped"]++
continue
}
feedMap[fmt.Sprintf("%d", f.ID)] = uuid.New().String()
report["input_feeds"]++
if len(f.FieldMappings) > 0 && string(f.FieldMappings) != "null" && string(f.FieldMappings) != "{}" {
report["feed_mappings"]++
}
}
for _, f := range fx.Files {
if _, ok := companyMap[f.CompanyID]; !ok {
report["files_skipped"]++
continue
}
fileMap[fmt.Sprintf("%d", f.ID)] = uuid.New().String()
report["files"]++
}
report["raw_products"] = fx.RawProducts
idDoc := NewIDMapDocument(userMap, companyMap, true)
idDoc.Source = "fixture"
idDoc.AttachEntityMaps(nil, nil, feedMap, nil, fileMap, report)
if err := WriteIDMap(outIDMap, idDoc); err != nil {
log.Fatalf("id-map: %v", err)
}
writeEntityMapFiles(mapsDir, companyMap, userMap, nil, nil, feedMap, nil, fileMap)
counts := []CountPair{
{Entity: "companies", MySQL: int64(len(fx.Companies)), Note: "fixture"},
{Entity: "users", MySQL: int64(len(fx.Users)), Note: "fixture; password_hash never imported"},
{Entity: "admin_users", MySQL: int64(len(fx.AdminUsers)), Note: "→ is_platform_admin"},
{Entity: "profiles", MySQL: int64(len(fx.Profiles)), Note: "→ memberships"},
{Entity: "xml_feeds", MySQL: int64(len(fx.XMLFeeds)), Note: "→ input_feeds + feed_mappings"},
{Entity: "files", MySQL: int64(len(fx.Files)), Note: "metadata only"},
{Entity: "raw_products", MySQL: int64(fx.RawProducts), Note: "count-only in fixture"},
}
v := ValidationReport{
Mode: "fixture-dry-run",
Counts: counts,
Orphans: []OrphanFinding{{
Check: "skipped_fixture",
Pass: true,
Sample: "orphan checks need live Postgres after a real load",
}},
OK: true,
}
v.OrphanSummary = summarizeOrphans(v.Orphans)
printValidation(v)
_ = writeJSON(filepath.Join(mapsDir, "validation-report.json"), v)
printMigrationReport(report, true)
fmt.Printf("wrote maps under %s (unified: %s)\n", mapsDir, outIDMap)
fmt.Println("BLOCKER: fixture mode is not a substitute for dry-run against production MySQL — set MIGRATE_MYSQL_DSN and re-run before cutover.")
}