Phase 5: scaffold Crucible (sow-tools) — dispatcher + builder shims, container, PR-first CI, fail-closed (D11, D19).
This commit is contained in:
@@ -1,345 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type familyIdentity struct {
|
||||
Parent string
|
||||
Child string
|
||||
}
|
||||
|
||||
type familyExpansionSource struct {
|
||||
Dataset string
|
||||
Column string
|
||||
Predicate string
|
||||
}
|
||||
|
||||
type familyExpansionTitleStyle struct {
|
||||
ChildCase string
|
||||
ChildParenthetical string
|
||||
}
|
||||
|
||||
type familyExpansionSpec struct {
|
||||
Family string
|
||||
FamilyKey string
|
||||
Template string
|
||||
ChildSource familyExpansionSource
|
||||
NamePrefix string
|
||||
LabelPrefix string
|
||||
ConstantPrefix string
|
||||
TemplateFields []string
|
||||
DefaultFields map[string]any
|
||||
ApplyAfterModules bool
|
||||
ChildRefField string
|
||||
IdentitySource string
|
||||
AllowExistingOnly bool
|
||||
AutoPrereqFields map[string]string
|
||||
LegacyFamilyKeys []string
|
||||
TitleStyle familyExpansionTitleStyle
|
||||
}
|
||||
|
||||
func splitFamilyExpansionIdentity(text string) familyIdentity {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return familyIdentity{}
|
||||
}
|
||||
if idx := strings.Index(text, "_"); idx > 0 && idx < len(text)-1 {
|
||||
return familyIdentity{
|
||||
Parent: text[:idx],
|
||||
Child: text[idx+1:],
|
||||
}
|
||||
}
|
||||
return familyIdentity{Parent: text}
|
||||
}
|
||||
|
||||
func parseFamilyExpansionSource(raw any) (familyExpansionSource, error) {
|
||||
if raw == nil {
|
||||
return familyExpansionSource{}, fmt.Errorf("child_source is required")
|
||||
}
|
||||
obj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return familyExpansionSource{}, fmt.Errorf("child_source must be an object")
|
||||
}
|
||||
dataset, ok := obj["dataset"].(string)
|
||||
if !ok || strings.TrimSpace(dataset) == "" {
|
||||
return familyExpansionSource{}, fmt.Errorf("child_source.dataset must be a non-empty string")
|
||||
}
|
||||
source := familyExpansionSource{
|
||||
Dataset: strings.TrimSpace(dataset),
|
||||
}
|
||||
if column, ok := obj["column"].(string); ok && strings.TrimSpace(column) != "" {
|
||||
source.Column = strings.TrimSpace(column)
|
||||
}
|
||||
if predicate, ok := obj["predicate"].(string); ok && strings.TrimSpace(predicate) != "" {
|
||||
source.Predicate = strings.TrimSpace(predicate)
|
||||
}
|
||||
return source, nil
|
||||
}
|
||||
|
||||
func parseFamilyExpansionSpec(path string, obj map[string]any) (familyExpansionSpec, error) {
|
||||
family, ok := obj["family"].(string)
|
||||
if !ok || strings.TrimSpace(family) == "" {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: family must be a non-empty string", path)
|
||||
}
|
||||
familyKey, ok := obj["family_key"].(string)
|
||||
if !ok || strings.TrimSpace(familyKey) == "" {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: family_key must be a non-empty string", path)
|
||||
}
|
||||
template, ok := obj["template"].(string)
|
||||
if !ok || strings.TrimSpace(template) == "" {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: template must be a non-empty string", path)
|
||||
}
|
||||
source, err := parseFamilyExpansionSource(obj["child_source"])
|
||||
if err != nil {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||
}
|
||||
namePrefix, _ := optionalTrimmedString(obj, "name_prefix")
|
||||
labelPrefix, _ := optionalTrimmedString(obj, "label_prefix")
|
||||
constantPrefix, _ := optionalTrimmedString(obj, "constant_prefix")
|
||||
templateFields, err := parseOptionalStringArray(obj["template_fields"], "template_fields")
|
||||
if err != nil {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||
}
|
||||
defaultFields, err := parseOptionalObject(obj["default_fields"], "default_fields")
|
||||
if err != nil {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||
}
|
||||
applyAfterModules, err := parseOptionalBoolField(obj["apply_after_modules"], "apply_after_modules")
|
||||
if err != nil {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||
}
|
||||
childRefField, _ := optionalTrimmedString(obj, "child_ref_field")
|
||||
identitySource, _ := optionalTrimmedString(obj, "identity_source")
|
||||
switch identitySource {
|
||||
case "", "child_source_value":
|
||||
default:
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: identity_source must be empty or child_source_value", path)
|
||||
}
|
||||
autoPrereqFields, err := parseOptionalStringMap(obj["auto_prereq_fields"], "auto_prereq_fields")
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
titleStyle, err := parseFamilyExpansionTitleStyle(obj["title_style"])
|
||||
if err != nil {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||
}
|
||||
allowExistingOnly, err := parseOptionalBoolField(obj["allow_existing_only"], "allow_existing_only")
|
||||
if err != nil {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||
}
|
||||
return familyExpansionSpec{
|
||||
Family: strings.TrimSpace(family),
|
||||
FamilyKey: strings.TrimSpace(familyKey),
|
||||
Template: strings.TrimSpace(template),
|
||||
ChildSource: source,
|
||||
NamePrefix: namePrefix,
|
||||
LabelPrefix: labelPrefix,
|
||||
ConstantPrefix: constantPrefix,
|
||||
TemplateFields: templateFields,
|
||||
DefaultFields: defaultFields,
|
||||
ApplyAfterModules: applyAfterModules,
|
||||
ChildRefField: childRefField,
|
||||
IdentitySource: identitySource,
|
||||
AllowExistingOnly: allowExistingOnly,
|
||||
AutoPrereqFields: autoPrereqFields,
|
||||
LegacyFamilyKeys: legacyFamilyKeys,
|
||||
TitleStyle: titleStyle,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseFamilyExpansionTitleStyle(raw any) (familyExpansionTitleStyle, error) {
|
||||
style := familyExpansionTitleStyle{
|
||||
ChildCase: "preserve",
|
||||
ChildParenthetical: "preserve",
|
||||
}
|
||||
if raw == nil {
|
||||
return style, nil
|
||||
}
|
||||
obj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return familyExpansionTitleStyle{}, fmt.Errorf("title_style must be an object")
|
||||
}
|
||||
if childCase, ok := obj["child_case"].(string); ok && strings.TrimSpace(childCase) != "" {
|
||||
style.ChildCase = strings.TrimSpace(childCase)
|
||||
}
|
||||
switch style.ChildCase {
|
||||
case "preserve", "lower":
|
||||
default:
|
||||
return familyExpansionTitleStyle{}, fmt.Errorf("title_style.child_case must be preserve or lower")
|
||||
}
|
||||
if childParenthetical, ok := obj["child_parenthetical"].(string); ok && strings.TrimSpace(childParenthetical) != "" {
|
||||
style.ChildParenthetical = strings.TrimSpace(childParenthetical)
|
||||
}
|
||||
switch style.ChildParenthetical {
|
||||
case "preserve", "comma":
|
||||
default:
|
||||
return familyExpansionTitleStyle{}, fmt.Errorf("title_style.child_parenthetical must be preserve or comma")
|
||||
}
|
||||
return style, 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 {
|
||||
_, hasFamilyKey := obj["family_key"]
|
||||
_, hasTemplate := obj["template"]
|
||||
_, hasChildSource := obj["child_source"]
|
||||
_, hasNamePrefix := obj["name_prefix"]
|
||||
_, hasLabelPrefix := obj["label_prefix"]
|
||||
_, hasConstantPrefix := obj["constant_prefix"]
|
||||
_, hasTemplateFields := obj["template_fields"]
|
||||
_, hasDefaultFields := obj["default_fields"]
|
||||
_, hasApplyAfterModules := obj["apply_after_modules"]
|
||||
_, hasChildRefField := obj["child_ref_field"]
|
||||
_, hasIdentitySource := obj["identity_source"]
|
||||
_, hasAllowExistingOnly := obj["allow_existing_only"]
|
||||
_, hasAutoPrereqFields := obj["auto_prereq_fields"]
|
||||
_, hasLegacyFamilyKeys := obj["legacy_family_keys"]
|
||||
_, hasTitleStyle := obj["title_style"]
|
||||
return hasFamilyKey || hasTemplate || hasChildSource || hasNamePrefix || hasLabelPrefix ||
|
||||
hasConstantPrefix || hasTemplateFields || hasDefaultFields || hasApplyAfterModules || hasChildRefField ||
|
||||
hasIdentitySource || hasAllowExistingOnly || hasAutoPrereqFields || hasLegacyFamilyKeys || hasTitleStyle
|
||||
}
|
||||
|
||||
func optionalTrimmedString(obj map[string]any, field string) (string, bool) {
|
||||
text, ok := obj[field].(string)
|
||||
if !ok || strings.TrimSpace(text) == "" {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSpace(text), true
|
||||
}
|
||||
|
||||
func parseOptionalStringArray(raw any, field string) ([]string, error) {
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
items, ok := raw.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s must be an array of strings", field)
|
||||
}
|
||||
out := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
text, ok := item.(string)
|
||||
if !ok || strings.TrimSpace(text) == "" {
|
||||
return nil, fmt.Errorf("%s must contain only non-empty strings", field)
|
||||
}
|
||||
out = append(out, strings.TrimSpace(text))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseOptionalObject(raw any, field string) (map[string]any, error) {
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
obj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s must be an object", field)
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func parseOptionalStringMap(raw any, field string) (map[string]string, error) {
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
obj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s must be an object of strings", field)
|
||||
}
|
||||
out := make(map[string]string, len(obj))
|
||||
for key, value := range obj {
|
||||
text, ok := value.(string)
|
||||
if !ok || strings.TrimSpace(text) == "" {
|
||||
return nil, fmt.Errorf("%s must contain only non-empty string values", field)
|
||||
}
|
||||
out[key] = strings.TrimSpace(text)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseOptionalBoolField(raw any, field string) (bool, error) {
|
||||
if raw == nil {
|
||||
return false, nil
|
||||
}
|
||||
value, ok := raw.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("%s must be a boolean", field)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func familyMetadata(parent, child, source, template string) map[string]any {
|
||||
meta := map[string]any{
|
||||
"parent": strings.TrimSpace(parent),
|
||||
}
|
||||
if strings.TrimSpace(child) != "" {
|
||||
meta["child"] = strings.TrimSpace(child)
|
||||
}
|
||||
if strings.TrimSpace(source) != "" {
|
||||
meta["source"] = strings.TrimSpace(source)
|
||||
}
|
||||
if strings.TrimSpace(template) != "" {
|
||||
meta["template"] = strings.TrimSpace(template)
|
||||
}
|
||||
return map[string]any{"family": meta}
|
||||
}
|
||||
|
||||
func parseFamilyMetadata(raw any) (map[string]any, error) {
|
||||
obj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("must be an object")
|
||||
}
|
||||
parent, ok := obj["parent"].(string)
|
||||
if !ok || strings.TrimSpace(parent) == "" {
|
||||
return nil, fmt.Errorf("parent must be a non-empty string")
|
||||
}
|
||||
meta := map[string]any{
|
||||
"parent": strings.TrimSpace(parent),
|
||||
}
|
||||
if child, ok := obj["child"].(string); ok && strings.TrimSpace(child) != "" {
|
||||
meta["child"] = strings.TrimSpace(child)
|
||||
}
|
||||
if source, ok := obj["source"].(string); ok && strings.TrimSpace(source) != "" {
|
||||
meta["source"] = strings.TrimSpace(source)
|
||||
}
|
||||
if template, ok := obj["template"].(string); ok && strings.TrimSpace(template) != "" {
|
||||
meta["template"] = strings.TrimSpace(template)
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func childTokenFromExpandedKey(key, parent string) string {
|
||||
key = strings.TrimSpace(key)
|
||||
key = strings.TrimPrefix(key, "feat:")
|
||||
if strings.HasPrefix(key, parent+"_") {
|
||||
return strings.TrimPrefix(key, parent+"_")
|
||||
}
|
||||
if strings.HasPrefix(key, parent) {
|
||||
return strings.TrimPrefix(key, parent)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user