Skip to content

Commit ed1976c

Browse files
Merge pull request #726 from gastownhall/fix/infix-on-julian-probe-complete
Remove formula and order filename infixes on track1 (supersedes #723)
2 parents 0d73bdf + 98f634f commit ed1976c

91 files changed

Lines changed: 500 additions & 241 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/gc/builtin_prompts_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestMaterializeBuiltinFormulas(t *testing.T) {
6565
if err := materializeBuiltinFormulas(dir); err != nil {
6666
t.Fatalf("materializeBuiltinFormulas: %v", err)
6767
}
68-
if _, err := os.Stat(filepath.Join(dir, citylayout.FormulasRoot, "pancakes.formula.toml")); !os.IsNotExist(err) {
68+
if _, err := os.Stat(filepath.Join(dir, citylayout.FormulasRoot, "pancakes.toml")); !os.IsNotExist(err) {
6969
t.Fatalf("materializeBuiltinFormulas should not write city-local formula seeds on start")
7070
}
7171
}
@@ -78,7 +78,7 @@ func TestMaterializeBuiltinFormulasOverwrites(t *testing.T) {
7878
}
7979

8080
// Write stale content.
81-
stale := filepath.Join(formulasDir, "pancakes.formula.toml")
81+
stale := filepath.Join(formulasDir, "pancakes.toml")
8282
if err := os.WriteFile(stale, []byte("stale"), 0o644); err != nil {
8383
t.Fatal(err)
8484
}

cmd/gc/cmd_formula.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,23 @@ configured via packs and formulas_dir settings.`,
3939
return nil
4040
}
4141

42-
// Scan search paths for .formula.toml files, deduplicating by name
43-
// (last path wins, matching formula layer resolution order).
42+
// Scan search paths for canonical and legacy formula TOML files,
43+
// deduplicating by name (last path wins, matching formula layer
44+
// resolution order).
4445
winners := make(map[string]bool)
4546
for _, dir := range paths {
4647
entries, err := os.ReadDir(dir)
4748
if err != nil {
4849
continue
4950
}
5051
for _, e := range entries {
51-
if e.IsDir() || !strings.HasSuffix(e.Name(), ".formula.toml") {
52+
if e.IsDir() {
53+
continue
54+
}
55+
name, ok := formula.TrimTOMLFilename(e.Name())
56+
if !ok {
5257
continue
5358
}
54-
name := strings.TrimSuffix(e.Name(), ".formula.toml")
5559
winners[name] = true
5660
}
5761
}

cmd/gc/cmd_order.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func newOrderCmd(stdout, stderr io.Writer) *cobra.Command {
2424
Short: "Manage orders (scheduled and event-driven dispatch)",
2525
Long: `Manage orders — scheduled or event-driven dispatch of formulas and scripts.
2626
27-
Orders live in orders/NAME/order.toml files. Each order pairs a gate
27+
Orders live in flat orders/<name>.toml files. Each order pairs a gate
2828
condition (cooldown, cron, condition, event, or manual) with an action
2929
(a formula or an exec script). The controller evaluates gates on each
3030
tick and dispatches work when a gate opens.`,
@@ -54,7 +54,7 @@ func newOrderListCmd(stdout, stderr io.Writer) *cobra.Command {
5454
Short: "List available orders",
5555
Long: `List all available orders with their gate type, schedule, and target.
5656
57-
Scans orders/ directories for order.toml files defining gate conditions,
57+
Scans orders/ directories for flat .toml files defining gate conditions,
5858
scheduling parameters, and target pools.`,
5959
Args: cobra.NoArgs,
6060
RunE: func(_ *cobra.Command, _ []string) error {

cmd/gc/cmd_order_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ schedule = "*/5 * * * *"
322322
}
323323
})
324324

325-
if !strings.Contains(logs, "move to orders/health-check.order.toml") {
325+
if !strings.Contains(logs, "move to orders/health-check.toml") {
326326
t.Fatalf("logs = %q, want move warning", logs)
327327
}
328328
}

cmd/gc/cmd_supervisor_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ func TestPrepareCityForSupervisorEnsuresInitArtifacts(t *testing.T) {
398398
t.Fatal(err)
399399
}
400400

401-
initFormula := filepath.Join(cityPath, citylayout.FormulasRoot, "mol-do-work.formula.toml")
401+
initFormula := filepath.Join(cityPath, citylayout.FormulasRoot, "mol-do-work.toml")
402402
if _, err := os.Stat(initFormula); !os.IsNotExist(err) {
403403
t.Fatalf("init formula should not exist before supervisor prep, err=%v", err)
404404
}

cmd/gc/embed_builtin_packs.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/gastownhall/gascity/examples/gastown/packs/gastown"
1414
"github.com/gastownhall/gascity/examples/gastown/packs/maintenance"
1515
"github.com/gastownhall/gascity/internal/citylayout"
16+
"github.com/gastownhall/gascity/internal/orders"
1617
)
1718

1819
// builtinPack pairs an embedded FS with the subdirectory name used under .gc/system/packs/.
@@ -22,7 +23,6 @@ type builtinPack struct {
2223
}
2324

2425
const (
25-
embeddedOrderSuffix = ".order.toml"
2626
legacyOrderConfigFile = "order.toml"
2727
)
2828

@@ -151,7 +151,7 @@ func materializeFS(embedded fs.FS, root, dstDir string) error {
151151
}
152152

153153
// pruneLegacyEmbeddedOrders removes deprecated order directory layouts when the
154-
// embedded pack already provides the flat orders/<name>.order.toml form.
154+
// embedded pack already provides the flat orders/<name>.toml form.
155155
func pruneLegacyEmbeddedOrders(embedded fs.FS, dstDir string) error {
156156
entries, err := fs.ReadDir(embedded, "orders")
157157
if err != nil {
@@ -162,10 +162,10 @@ func pruneLegacyEmbeddedOrders(embedded fs.FS, dstDir string) error {
162162
continue
163163
}
164164
name := entry.Name()
165-
if !strings.HasSuffix(name, embeddedOrderSuffix) {
165+
orderName, ok := orders.TrimFlatOrderFilename(name)
166+
if !ok {
166167
continue
167168
}
168-
orderName := strings.TrimSuffix(name, embeddedOrderSuffix)
169169
for _, legacyPath := range []string{
170170
filepath.Join(dstDir, "orders", orderName, legacyOrderConfigFile),
171171
filepath.Join(dstDir, "formulas", "orders", orderName, legacyOrderConfigFile),

cmd/gc/embed_builtin_packs_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ func TestMaterializeBuiltinPacks(t *testing.T) {
8181

8282
// Verify embedded order files are materialized alongside formulas.
8383
for _, order := range []string{
84-
filepath.Join(dir, citylayout.SystemPacksRoot, "maintenance", "orders", "gate-sweep.order.toml"),
85-
filepath.Join(dir, citylayout.SystemPacksRoot, "maintenance", "orders", "mol-dog-jsonl.order.toml"),
86-
filepath.Join(dir, citylayout.SystemPacksRoot, "maintenance", "orders", "mol-dog-reaper.order.toml"),
87-
filepath.Join(dir, citylayout.SystemPacksRoot, "dolt", "orders", "dolt-health.order.toml"),
88-
filepath.Join(dir, citylayout.SystemPacksRoot, "gastown", "orders", "digest-generate.order.toml"),
84+
filepath.Join(dir, citylayout.SystemPacksRoot, "maintenance", "orders", "gate-sweep.toml"),
85+
filepath.Join(dir, citylayout.SystemPacksRoot, "maintenance", "orders", "mol-dog-jsonl.toml"),
86+
filepath.Join(dir, citylayout.SystemPacksRoot, "maintenance", "orders", "mol-dog-reaper.toml"),
87+
filepath.Join(dir, citylayout.SystemPacksRoot, "dolt", "orders", "dolt-health.toml"),
88+
filepath.Join(dir, citylayout.SystemPacksRoot, "gastown", "orders", "digest-generate.toml"),
8989
} {
9090
if _, err := os.Stat(order); err != nil {
9191
t.Errorf("embedded order missing: %v", err)
@@ -181,9 +181,9 @@ func TestMaterializeBuiltinPacks_PrunesLegacyOrderDirs(t *testing.T) {
181181
}
182182

183183
for _, path := range []string{
184-
filepath.Join(dir, citylayout.SystemPacksRoot, "maintenance", "orders", "gate-sweep.order.toml"),
185-
filepath.Join(dir, citylayout.SystemPacksRoot, "dolt", "orders", "dolt-health.order.toml"),
186-
filepath.Join(dir, citylayout.SystemPacksRoot, "gastown", "orders", "digest-generate.order.toml"),
184+
filepath.Join(dir, citylayout.SystemPacksRoot, "maintenance", "orders", "gate-sweep.toml"),
185+
filepath.Join(dir, citylayout.SystemPacksRoot, "dolt", "orders", "dolt-health.toml"),
186+
filepath.Join(dir, citylayout.SystemPacksRoot, "gastown", "orders", "digest-generate.toml"),
187187
} {
188188
if _, err := os.Stat(path); err != nil {
189189
t.Fatalf("flat order missing after materialization: %v", err)

cmd/gc/formula_resolve.go

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,90 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7-
"strings"
7+
8+
"github.com/gastownhall/gascity/internal/formula"
89
)
910

10-
// ResolveFormulas computes per-filename winners from layered formula
11-
// directories and creates symlinks in targetDir/.beads/formulas/.
11+
// ResolveFormulas computes per-formula-name winners from layered formula
12+
// directories and creates canonical .toml symlinks in targetDir/.beads/formulas/.
1213
//
13-
// Layers are ordered lowest→highest priority. For each *.formula.toml file
14-
// found across all layers, the highest-priority layer wins. Winners are
15-
// symlinked into targetDir/.beads/formulas/ so bd finds them natively.
14+
// Layers are ordered lowest→highest priority. For each formula name (derived
15+
// from either canonical or legacy filename form), the highest-priority layer
16+
// wins. Winners are symlinked into targetDir/.beads/formulas/<name>.toml so
17+
// bd finds them natively using the canonical filename, regardless of the
18+
// source file's on-disk name.
1619
//
1720
// Idempotent: correct symlinks are left alone, stale ones are updated,
18-
// and symlinks for formulas no longer in any layer are removed. Real files
21+
// and symlinks for formulas no longer in any layer are removed (including
22+
// any stray legacy-suffixed symlinks from earlier runs). Real files
1923
// (non-symlinks) in the target directory are never overwritten.
2024
func ResolveFormulas(targetDir string, layers []string) error {
2125
if len(layers) == 0 {
2226
return nil
2327
}
2428

25-
// Build winner map: filename → absolute source path.
26-
// Later layers overwrite earlier ones (higher priority).
29+
// Build winner map keyed by formula NAME (not filename). Later layers
30+
// overwrite earlier ones (higher priority). Within a single layer, the
31+
// canonical .toml form wins over the legacy .formula.toml form so a
32+
// partially-migrated layer does not shadow its own canonical file.
2733
winners := make(map[string]string)
2834
for _, layerDir := range layers {
2935
entries, err := os.ReadDir(layerDir)
3036
if err != nil {
3137
continue // Layer dir doesn't exist — skip (not an error).
3238
}
39+
// Resolve within-layer winners first so canonical beats legacy
40+
// sibling regardless of ReadDir order, then merge into the
41+
// cross-layer winners map (overwriting lower layers).
42+
layerPick := make(map[string]string)
43+
layerLegacy := make(map[string]bool)
3344
for _, e := range entries {
34-
if e.IsDir() || !strings.HasSuffix(e.Name(), ".formula.toml") {
45+
if e.IsDir() {
46+
continue
47+
}
48+
name, ok := formula.TrimTOMLFilename(e.Name())
49+
if !ok {
3550
continue
3651
}
52+
legacy := e.Name() == name+formula.LegacyTOMLExt
53+
if _, exists := layerPick[name]; exists && legacy && !layerLegacy[name] {
54+
continue // Canonical already picked in this layer — skip legacy sibling.
55+
}
3756
abs, err := filepath.Abs(filepath.Join(layerDir, e.Name()))
3857
if err != nil {
3958
continue
4059
}
41-
winners[e.Name()] = abs
60+
layerPick[name] = abs
61+
layerLegacy[name] = legacy
4262
}
63+
for name, abs := range layerPick {
64+
winners[name] = abs
65+
}
66+
}
67+
68+
// Build the set of canonical filenames we will emit. cleanStaleFormulaSymlinks
69+
// uses this to garbage-collect any legacy-suffixed symlinks from prior runs.
70+
canonicalNames := make(map[string]string, len(winners))
71+
for name, src := range winners {
72+
canonicalNames[name+formula.CanonicalTOMLExt] = src
4373
}
4474

4575
symlinkDir := filepath.Join(targetDir, ".beads", "formulas")
4676

4777
if len(winners) == 0 {
48-
return cleanStaleFormulaSymlinks(symlinkDir, winners)
78+
return cleanStaleFormulaSymlinks(symlinkDir, canonicalNames)
4979
}
5080

5181
// Ensure target symlink directory exists.
5282
if err := os.MkdirAll(symlinkDir, 0o755); err != nil {
5383
return fmt.Errorf("creating formula symlink dir: %w", err)
5484
}
5585

56-
// Create/update symlinks for winners.
57-
for name, srcPath := range winners {
58-
linkPath := filepath.Join(symlinkDir, name)
86+
// Create/update canonical symlinks for winners. The link is always named
87+
// <formula-name>.toml regardless of whether the winning source file on
88+
// disk uses the canonical or legacy extension.
89+
for linkName, srcPath := range canonicalNames {
90+
linkPath := filepath.Join(symlinkDir, linkName)
5991

6092
// Check if a real file (non-symlink) exists — don't overwrite.
6193
fi, err := os.Lstat(linkPath)
@@ -74,11 +106,11 @@ func ResolveFormulas(targetDir string, layers []string) error {
74106
}
75107

76108
if err := os.Symlink(srcPath, linkPath); err != nil {
77-
return fmt.Errorf("creating formula symlink %q → %q: %w", name, srcPath, err)
109+
return fmt.Errorf("creating formula symlink %q → %q: %w", linkName, srcPath, err)
78110
}
79111
}
80112

81-
return cleanStaleFormulaSymlinks(symlinkDir, winners)
113+
return cleanStaleFormulaSymlinks(symlinkDir, canonicalNames)
82114
}
83115

84116
// cleanStaleFormulaSymlinks removes symlinks in symlinkDir that are not in
@@ -91,7 +123,7 @@ func cleanStaleFormulaSymlinks(symlinkDir string, winners map[string]string) err
91123
return nil // Can't read — nothing to clean up.
92124
}
93125
for _, e := range entries {
94-
if e.IsDir() || !strings.HasSuffix(e.Name(), ".formula.toml") {
126+
if e.IsDir() || !formula.IsTOMLFilename(e.Name()) {
95127
continue
96128
}
97129
linkPath := filepath.Join(symlinkDir, e.Name())

0 commit comments

Comments
 (0)