fix(workgroup): the natural sample excludes experiments by construction

The protocol said the sample is ordinary use and leaves out benchmark and
fixture runs. Nothing enforced it. The only thing keeping the twelve designed
turns out was that they happened before the freeze, which says nothing at all
about the next scripted run — and the session store already holds 481 project
directories from past automated ones, every future sibling of which would have
walked straight into a sample that calls itself natural.

A driven run lives in a scratch directory. So a workspace under a temp root is
not ordinary use, and its sessions never enter the sample. The check asks the
host for the slug it writes such a path as, rather than looking for words in a
directory name: the encoding is the host's own and reading it back is a fact,
while matching "scratch" or "tmp" would be a guess about how someone names
things.

Seven of 488 project directories survive it, and they are the ones a person
actually works in. The count of what was turned away is printed with the rest,
so a sample that stays empty says why.
This commit is contained in:
esengine committed 2026-09-07 22:12:01 -07:00
1 parent c2ffdf6d5e
commit 491016f8c9
4 files changed
+104 -16

No files matched your search

+5 -1
View File
@@ -25,7 +25,11 @@ diff.
A turn with nothing to fold is evidence about coverage, and dropping it would
answer a different question and flatter the feature.
- **Out**: turns written for this experiment, benchmark and fixture runs, and
anything shaped to call more tools than the work needed.
anything shaped to call more tools than the work needed. Enforced, not
merely stated: a workspace under a temp root is where a driven run lives, so
its sessions never enter the sample. Before that check the only thing keeping
a scripted run out was that it happened before the freeze, which says nothing
about the next one.
- **No verdict before the sample is whole.** Under 20 the tool prints the count
and stops.
+37
View File
@@ -8,6 +8,7 @@ import (
"testing"
"time"
"reasonix/internal/config"
"reasonix/internal/event"
"reasonix/internal/eventwire"
"reasonix/internal/workgroup"
@@ -132,3 +133,39 @@ func TestOnlyNamedTurnsInACompleteLogAreEligible(t *testing.T) {
t.Fatal("a log whose witness says truncated was read as complete")
}
}
// The protocol says the natural sample excludes experiments. Saying it is not
// enough: before this, the only thing keeping a scripted run out was that it
// happened before the freeze, which says nothing about the next one. A
// workspace under a temp root is where a driven run lives, and the check reads
// the host's own slug encoding rather than guessing at directory names.
func TestWorkspacesUnderATempRootAreNotOrdinaryUse(t *testing.T) {
slugs := scratchSlugs()
if len(slugs) == 0 {
t.Fatal("no temp-root slugs were derived, so nothing would ever be excluded")
}
for _, tc := range []struct {
name string
dir string
scratch bool
}{
{"a driven run's workspace", config.WorkspaceSlug(filepath.Join(os.TempDir(), "assay", "ws")), true},
{"the system temp root itself", config.WorkspaceSlug(filepath.Join("/private/tmp", "run")), true},
{"a person's own project", config.WorkspaceSlug(filepath.Join(home(t), "projects", "thing")), false},
} {
t.Run(tc.name, func(t *testing.T) {
if got := underScratch(tc.dir, slugs); got != tc.scratch {
t.Fatalf("underScratch(%q) = %v, want %v", tc.dir, got, tc.scratch)
}
})
}
}
func home(t *testing.T) string {
t.Helper()
dir, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory to build a non-temp path from")
}
return dir
}
+62 -15
View File
@@ -29,8 +29,9 @@ type turn struct {
func main() {
root := flag.String("root", "", "session root to scan (default: the configured one)")
flag.Parse()
dirs := scanRoots(*root)
dirs, scratch := scanRoots(*root)
turns, skipped := collect(dirs)
skipped.scratchWorkspaces = scratch
sort.Slice(turns, func(i, j int) bool {
if !turns[i].at.Equal(turns[j].at) {
return turns[i].at.Before(turns[j].at)
@@ -40,47 +41,91 @@ func main() {
report(turns, skipped)
}
func scanRoots(root string) []string {
func scanRoots(root string) ([]string, int) {
if strings.TrimSpace(root) != "" {
return []string{root}
return []string{root}, 0
}
projects, scratch := projectSessionDirs()
seen := map[string]bool{}
var out []string
for _, d := range append([]string{config.SessionDir()}, projectSessionDirs()...) {
for _, d := range append([]string{config.SessionDir()}, projects...) {
if d != "" && !seen[d] {
seen[d] = true
out = append(out, d)
}
}
return out
return out, scratch
}
// projectSessionDirs finds the per-project session directories a Studio window
// writes into, which is where ordinary use lands.
func projectSessionDirs() []string {
// writes into. Workspaces under a temp root are left out: a run driven from a
// scratch directory is an experiment, and the protocol excludes those by
// construction rather than by hoping none happen after the freeze.
func projectSessionDirs() ([]string, int) {
home := config.ReasonixHomeDir()
if home == "" {
return nil
return nil, 0
}
entries, err := os.ReadDir(filepath.Join(home, "projects"))
if err != nil {
return nil
return nil, 0
}
scratch := scratchSlugs()
var out []string
skipped := 0
for _, e := range entries {
if e.IsDir() {
out = append(out, filepath.Join(home, "projects", e.Name(), "sessions"))
if !e.IsDir() {
continue
}
if underScratch(e.Name(), scratch) {
skipped++
continue
}
out = append(out, filepath.Join(home, "projects", e.Name(), "sessions"))
}
return out, skipped
}
// scratchSlugs is what a session directory's name starts with when its
// workspace lives under a temp root. Built with the host's own slug function,
// so this reads the encoding the host wrote rather than guessing at names.
func scratchSlugs() []string {
var out []string
for _, root := range []string{os.TempDir(), "/tmp", "/private/tmp"} {
for _, p := range []string{root, resolved(root)} {
if abs, err := filepath.Abs(p); err == nil && abs != "" && abs != string(filepath.Separator) {
if slug := config.WorkspaceSlug(abs); slug != "" {
out = append(out, slug)
}
}
}
}
return out
}
func resolved(path string) string {
if p, err := filepath.EvalSymlinks(path); err == nil {
return p
}
return path
}
func underScratch(name string, slugs []string) bool {
for _, s := range slugs {
if strings.HasPrefix(name, s) {
return true
}
}
return false
}
// skipReasons counts what the protocol turned away, so a small sample can say
// why rather than look like nothing happened.
type skipReasons struct {
beforeFreeze int
truncatedLogs int
unnamedTurns int
beforeFreeze int
truncatedLogs int
unnamedTurns int
scratchWorkspaces int
}
func collect(dirs []string) ([]turn, skipReasons) {
@@ -183,8 +228,10 @@ func report(turns []turn, skipped skipReasons) {
fmt.Printf(" protocol frozen at %s; sample is the first %d eligible authored turns after it\n",
frozenAt, sampleSize)
fmt.Printf(" eligible so far: %d\n", len(turns))
fmt.Printf(" turned away: %d session(s) before the freeze, %d truncated, %d unnamed turns\n",
fmt.Printf(" turned away: %d session(s) before the freeze, %d truncated, %d unnamed turns,\n",
skipped.beforeFreeze, skipped.truncatedLogs, skipped.unnamedTurns)
fmt.Printf(" %d workspace(s) under a temp root — experiments, not ordinary use\n",
skipped.scratchWorkspaces)
if len(turns) < sampleSize {
fmt.Printf("\nNo result yet. %d more eligible authored turn(s) are owed.\n", sampleSize-len(turns))
fmt.Printf("Reading a partial sample is the sequential decision this protocol exists to prevent,\n")
BIN
View File
Binary file not shown.