Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/contractlint/boot_resident_closure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ var deferredSkillCores = map[string][]string{
"## Status Viewer", "### Captain-Facing State Display", "## Issue Filing",
},
filepath.Join("skills", "fo-write-core", "SKILL.md"): {
"## FO Write Scope", "## ID Styles",
"## Mutation Gate", "## FO Write Scope", "## ID Styles",
},
filepath.Join("skills", "fo-dispatch-recovery", "SKILL.md"): {
"## Degraded Mode", "## Break-Glass Manual Dispatch", "## Context Budget Failure and Dead Ensign Handling",
Expand Down
2 changes: 1 addition & 1 deletion internal/contractlint/fn_consolidation_structure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func TestDeferredLoadPointsFoldFourModulesWithTriggersAndGreetGuard(t *testing.T
for _, lp := range []string{
"first status query", // status-query load-point
"first worker dispatch", // dispatch load-point
"write to main", // write/new-entity load-point
"file-write intent", // write/new-entity load-point
"terminal boundary", // merge load-point
} {
if !strings.Contains(block, lp) {
Expand Down
150 changes: 150 additions & 0 deletions internal/contractlint/fo_write_core_mutation_gate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// ABOUTME: Structural guard for fo-write-core's machine-readable mutation gate.
// ABOUTME: The path fixtures are independent of the skill prose, so drift reds.
package contractlint

import (
"os"
"path/filepath"
"strings"
"testing"
)

const (
foWriteClassifierStart = "<!-- FO-WRITE-CLASSIFIER:START -->"
foWriteClassifierEnd = "<!-- FO-WRITE-CLASSIFIER:END -->"
)

func TestFOWriteCoreMutationGateClassifiesTargets(t *testing.T) {
table := readFOWriteClassifierTable(t)
classifier := parseFOWriteClassifierTable(t, table)

cases := []struct {
path string
want string
}{
{".spacedock-state/task/index.md", "allowed-state"},
{"docs/dev/README.md", "allowed-process"},
{"cmd/spacedock/main.go", "blocked-product"},
{"internal/status/mutate.go", "blocked-product"},
{"internal/status/mutate_test.go", "blocked-product"},
{"skills/fo-write-core/SKILL.md", "blocked-product"},
{"agents/first-officer.md", "blocked-product"},
{"references/legacy.md", "blocked-product"},
{"plugin.json", "blocked-product"},
{".github/workflows/runtime-live-e2e.yml", "blocked-product"},
{"docs/site/reference/command-reference.md", "blocked-product"},
{"docs/specs/state-behavior-extension.md", "blocked-product"},
{"docs/roadmap/0250-fo-behavioral-discipline/index.md", "blocked-product"},
{"fixtures/entity-label-drive/README.md", "blocked-product"},
{"docs/dev/_mods/pr-merge.md", "blocked-product"},
}
for _, tc := range cases {
if got := classifyFOWriteTarget(classifier, tc.path); got != tc.want {
t.Errorf("classify %q = %q, want %q", tc.path, got, tc.want)
}
}
}

func TestFOWriteCoreMutationGateRequiresExactOverride(t *testing.T) {
table := readFOWriteClassifierTable(t)
classifier := parseFOWriteClassifierTable(t, table)

target := "internal/status/mutate.go"
if foWriteOverrideAllows(target, "you may fix the code directly") {
t.Fatalf("broad direct-edit text unexpectedly allowed %q", target)
}
if foWriteMayWrite(classifier, target, "you may fix the code directly") {
t.Fatalf("broad direct-edit text must not allow blocked product target %q", target)
}
if !foWriteMayWrite(classifier, target, "you may directly edit internal/status/mutate.go for this task") {
t.Fatalf("exact target grant must allow %q after blocked-product classification", target)
}
if foWriteMayWrite(classifier, "internal/status/parse.go", "you may directly edit internal/status/mutate.go for this task") {
t.Fatalf("exact grant for mutate.go must not allow a different product path")
}
}

func readFOWriteClassifierTable(t *testing.T) string {
t.Helper()
path := filepath.Join(repoRoot(t), "skills", "fo-write-core", "SKILL.md")
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
body := string(data)
start := strings.Index(body, foWriteClassifierStart)
end := strings.Index(body, foWriteClassifierEnd)
if start < 0 || end < 0 || end <= start {
t.Fatalf("skills/fo-write-core/SKILL.md must carry a machine-readable classifier block bounded by %s and %s", foWriteClassifierStart, foWriteClassifierEnd)
}
return body[start+len(foWriteClassifierStart) : end]
}

func parseFOWriteClassifierTable(t *testing.T, table string) map[string][]string {
t.Helper()
out := map[string][]string{}
for _, line := range strings.Split(table, "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "|") || strings.Contains(line, "---") || strings.HasPrefix(strings.ToLower(line), "| class ") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 4 {
continue
}
class := strings.TrimSpace(parts[1])
if class == "" {
continue
}
var patterns []string
for _, raw := range strings.Split(parts[2], ";") {
pattern := strings.Trim(strings.TrimSpace(raw), "`")
if pattern != "" {
patterns = append(patterns, pattern)
}
}
out[class] = patterns
}
for _, class := range []string{"allowed-state", "allowed-process", "blocked-product", "override"} {
if len(out[class]) == 0 {
t.Fatalf("classifier table missing class %q with at least one pattern/rule; parsed=%v", class, out)
}
}
return out
}

func classifyFOWriteTarget(classifier map[string][]string, target string) string {
for _, class := range []string{"blocked-product", "allowed-state", "allowed-process"} {
for _, pattern := range classifier[class] {
if pathPatternMatches(pattern, target) {
return class
}
}
}
return "blocked-product"
}

func pathPatternMatches(pattern, target string) bool {
pattern = strings.TrimSpace(pattern)
switch {
case strings.HasSuffix(pattern, "/**"):
return strings.HasPrefix(target, strings.TrimSuffix(pattern, "**"))
case strings.HasPrefix(pattern, "**/*"):
return strings.HasSuffix(target, strings.TrimPrefix(pattern, "**/*"))
default:
return target == pattern
}
}

func foWriteMayWrite(classifier map[string][]string, target, grant string) bool {
switch classifyFOWriteTarget(classifier, target) {
case "allowed-state", "allowed-process":
return true
default:
return foWriteOverrideAllows(target, grant)
}
}

func foWriteOverrideAllows(target, grant string) bool {
return strings.Contains(grant, target)
}
Loading
Loading