// Command task-channel-authority-mutator-closure.go is the per-mutator/callsite
// closure guardrail for the canonical Task->board Channel TOCTOU fence. It
// inventories every Task mutation entry that creates or changes durable Task
// state and the exact fence call each reaches, then verifies the production
// source against that committed inventory using Go AST (line-ordered, so a
// fence that appears AFTER a side-effecting write is caught).
//
// Categories:
//
//   - fenced_entry: the mutation entry itself locks the canonical Task row and
//     the actor's active membership (or the board Channel for create) BEFORE
//     any durable write. The committed fence call must appear in the function
//     body strictly before its first durable write statement.
//   - delegating_entry: a public ForUser/ForAgent wrapper with no direct durable
//     write that delegates to a committed fenced_entry/fenced_helper (e.g.
//     CreateForUser -> createTasksInTx). Must call the declared delegate.
//   - fenced_helper: writes durable Task state but is only reachable from a
//     fenced/delegating entry and performs NO authorization of its own (must
//     not call the ordinary membership-read authorizers).
//
// Invariants (all fail closed):
//
//   - every committed entry exists at its exact file::function;
//   - every fenced_entry calls its declared fence before its first durable write
//     (fence-after-side-effects fails);
//   - every delegating_entry calls its declared delegate;
//   - every production TxStore method that writes durable Task state (direct
//     durable-write SQL or a call to a committed write-bearing function) is
//     committed — a NEW mutator without a fence fails;
//   - every fenced_helper must not call the ordinary membership-read
//     authorizers (loadAuthorizedUserTaskChannelAccess / ensureAgentCanAccess-
//     TaskChannel / resolveAccessibleAgentBoard) as its only gate — a sibling
//     relying on an ordinary membership read fails;
//   - stale committed entries (no longer present) and duplicate/misclassified
//     entries fail.
package main

import (
	"flag"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
)

// durableWriteTokens are the table-level durable Task state writes that must be
// under the fence. Only actual INSERT/UPDATE/DELETE statements count (a SELECT
// that merely joins a durable table for reading is NOT a mutation). The set
// covers every Task-associated durable surface: the Task/checklist/review/
// artifact tables, task-scope execution/participants/watches, and the read-state/
// notification/activity/unread/projection surfaces (scope_memberships, scope_
// notification_settings, conversation_member_states, scope_activity_events).
var durableWriteTokens = []string{
	"INSERT INTO tasks", "UPDATE tasks", "DELETE FROM tasks",
	"INSERT INTO task_scope_participant_metadata", "UPDATE task_scope_participant_metadata", "DELETE FROM task_scope_participant_metadata",
	"INSERT INTO task_checklist_items", "UPDATE task_checklist_items", "DELETE FROM task_checklist_items",
	"INSERT INTO task_review_requests", "UPDATE task_review_requests", "DELETE FROM task_review_requests",
	"INSERT INTO task_scope_execution", "UPDATE task_scope_execution", "DELETE FROM task_scope_execution",
	"INSERT INTO task_artifacts", "UPDATE task_artifacts", "DELETE FROM task_artifacts",
	"INSERT INTO task_timeline_events", "UPDATE task_timeline_events", "DELETE FROM task_timeline_events",
	"INSERT INTO scope_memberships", "UPDATE scope_memberships", "DELETE FROM scope_memberships",
	"INSERT INTO scope_notification_settings", "UPDATE scope_notification_settings", "DELETE FROM scope_notification_settings",
	"INSERT INTO conversation_member_states", "UPDATE conversation_member_states", "DELETE FROM conversation_member_states",
	"INSERT INTO scope_activity_events", "UPDATE scope_activity_events", "DELETE FROM scope_activity_events",
}

// fenceTokens are the centralized fence helpers that authorize the write.
var fenceTokens = []string{
	"lockTaskBoardChannelAuthority",
	"lockHumanTaskBoardChannelAuthority",
	"lockActiveAgentBoardChannelAuthority",
	"lockActiveHumanBoardChannelAuthority",
	"lockTaskScopeCollaboratorAuthority",
	"lockExistingTaskChannel",
	"lockChannelAuthority",
	"lockBoardChannelForCreate",
}

// ordinaryReadAuthorizers are the ordinary (non-locking) membership reads that
// must never be a mutation helper's ONLY gate.
var ordinaryReadAuthorizers = []string{
	"loadAuthorizedUserTaskChannelAccess",
	"ensureAgentCanAccessTaskChannel",
	"resolveAccessibleAgentBoard",
	"authorizeAccessibleAgentChannelID",
	"ensureMemberCanReadTaskCollaboratorScope",
}

const (
	mutatorFencedEntry  = "fenced_entry"
	mutatorDelegating   = "delegating_entry"
	mutatorFencedHelper = "fenced_helper"
)

type mutatorClosureEntry struct {
	File           string
	Function       string
	Classification string
	// Fence is the declared fence (fenced_entry) or delegate (delegating_entry)
	// this entry must reach; empty for fenced_helper.
	Fence string
	// Recv disambiguates the receiver type when the same function name exists on
	// multiple receiver types in one file (for example Store vs TxStore). An
	// empty Recv means a package-level function or an unambiguous single match.
	Recv string
}

func committedMutatorClosure() map[string]mutatorClosureEntry {
	out := map[string]mutatorClosureEntry{}
	add := func(rel, fn, class, fence string, recvs ...string) {
		recv := ""
		if len(recvs) > 0 {
			recv = recvs[0]
		}
		out[rel+"::"+fn] = mutatorClosureEntry{File: rel, Function: fn, Classification: class, Fence: fence, Recv: recv}
	}
	// fenced_entry: the mutation entry itself locks task + membership before write.
	add("internal/store/taskboard/agent_tasks.go", "ClaimForAgent", mutatorFencedEntry, "lockActiveAgentBoardChannelAuthority")
	add("internal/store/taskboard/agent_tasks.go", "UnclaimForAgent", mutatorFencedEntry, "lockActiveAgentBoardChannelAuthority")
	add("internal/store/taskboard/agent_tasks.go", "DeleteForAgent", mutatorFencedEntry, "lockActiveAgentBoardChannelAuthority")
	add("internal/store/taskboard/collaborators.go", "AddTaskCollaborator", mutatorFencedEntry, "lockTaskScopeCollaboratorAuthority")
	add("internal/store/taskboard/collaborator_request_port.go", "ApplyApprovedCollaboratorRequest", mutatorDelegating, "grantTaskCollaboratorInTx")
	add("internal/store/taskboard/collaborators.go", "RemoveTaskCollaborator", mutatorFencedEntry, "lockTaskScopeCollaboratorAuthority")
	add("internal/store/taskboard/collaborators.go", "LeaveTaskCollaborator", mutatorFencedEntry, "lockTaskScopeCollaboratorAuthority")
	add("internal/store/taskboard/create.go", "createTasksInTx", mutatorFencedEntry, "lockBoardChannelForCreate")
	add("internal/store/taskboard/subtask_create.go", "createSubtasksInTx", mutatorFencedEntry, "lockChannelAuthority")
	add("internal/store/taskboard/detail_artifacts_checkpoints.go", "addTaskCheckpointForUser", mutatorFencedEntry, "lockActiveHumanBoardChannelAuthority")
	add("internal/store/taskboard/detail_artifacts_checkpoints.go", "addTaskCheckpointForAgent", mutatorFencedEntry, "lockTaskBoardChannelAuthority")
	add("internal/store/taskboard/detail_mutation.go", "BlockTaskForUser", mutatorFencedEntry, "lockActiveHumanBoardChannelAuthority")
	add("internal/store/taskboard/detail_mutation.go", "BlockTaskForAgent", mutatorFencedEntry, "lockTaskBoardChannelAuthority")
	add("internal/store/taskboard/detail_mutation.go", "UnblockTaskForUser", mutatorFencedEntry, "lockActiveHumanBoardChannelAuthority")
	add("internal/store/taskboard/detail_mutation.go", "UnblockTaskForAgent", mutatorFencedEntry, "lockTaskBoardChannelAuthority")
	add("internal/store/taskboard/detail_mutation.go", "updateTaskStatusInTx", mutatorFencedEntry, "lockExistingTaskChannel")
	add("internal/store/taskboard/detail_mutation.go", "updateTaskInTx", mutatorFencedEntry, "lockChannelAuthority")
	add("internal/store/taskboard/detail_read.go", "resolveReadableUserTask", mutatorFencedEntry, "lockHumanTaskBoardChannelAuthority")
	add("internal/store/taskboard/detail_read.go", "resolveReadableAgentTask", mutatorFencedEntry, "lockTaskBoardChannelAuthority")
	add("internal/store/taskboard/detail_review.go", "requestTaskReviewForUser", mutatorFencedEntry, "lockActiveHumanBoardChannelAuthority")
	add("internal/store/taskboard/detail_review.go", "requestTaskReviewForAgent", mutatorFencedEntry, "lockTaskBoardChannelAuthority")
	add("internal/store/taskboard/detail_review.go", "resolveTaskReviewForUser", mutatorFencedEntry, "lockActiveHumanBoardChannelAuthority")
	add("internal/store/taskboard/detail_review.go", "resolveTaskReviewForAgent", mutatorFencedEntry, "lockTaskBoardChannelAuthority")
	add("internal/store/taskboard/detail_review.go", "reassignTaskReviewForUser", mutatorFencedEntry, "lockActiveHumanBoardChannelAuthority")
	add("internal/store/taskboard/detail_review.go", "reassignTaskReviewForAgent", mutatorFencedEntry, "lockTaskBoardChannelAuthority")
	add("internal/store/taskboard/detail_review.go", "markTaskReviewNotReviewableForUser", mutatorFencedEntry, "lockActiveHumanBoardChannelAuthority")
	add("internal/store/taskboard/detail_review.go", "markTaskReviewNotReviewableForAgent", mutatorFencedEntry, "lockTaskBoardChannelAuthority")
	add("internal/store/taskboard/task_scope_write_authority.go", "authorizeUserTaskScopeDiscussionWrite", mutatorFencedEntry, "lockHumanTaskBoardChannelAuthority")
	add("internal/store/taskboard/task_scope_write_authority.go", "authorizeAgentTaskScopeDiscussionWrite", mutatorFencedEntry, "lockTaskBoardChannelAuthority")
	add("internal/store/taskboard/user_tasks.go", "claimForUserInTx", mutatorFencedEntry, "lockActiveHumanBoardChannelAuthority")
	add("internal/store/taskboard/user_tasks.go", "UnclaimForUser", mutatorFencedEntry, "lockActiveHumanBoardChannelAuthority")
	add("internal/store/taskboard/user_tasks.go", "DeleteForUser", mutatorFencedEntry, "lockHumanTaskBoardChannelAuthority")
	add("internal/store/taskboard/read_state.go", "MarkTaskRead", mutatorFencedEntry, "lockExistingTaskChannel")

	// delegating_entry: public ForUser/ForAgent wrappers that delegate to a
	// committed fenced core and carry no durable write of their own. Fence may
	// name multiple acceptable delegates (comma-separated); any one must be called.
	add("internal/store/taskboard/agent_tasks.go", "CreateForAgent", mutatorDelegating, "createTasksInTx")
	add("internal/store/taskboard/user_tasks.go", "CreateForUser", mutatorDelegating, "createTasksInTx")
	add("internal/store/taskboard/user_tasks.go", "ClaimForUser", mutatorDelegating, "claimForUserInTx")
	add("internal/store/taskboard/detail_mutation.go", "updateTaskForUser", mutatorDelegating, "updateTaskInTx")
	add("internal/store/taskboard/detail_mutation.go", "updateTaskForAgent", mutatorDelegating, "updateTaskInTx")
	add("internal/store/taskboard/detail_mutation.go", "updateTaskStatusForAgent", mutatorDelegating, "updateTaskStatusInTx")
	add("internal/store/taskboard/detail_mutation.go", "updateTaskStatusForUser", mutatorDelegating, "updateTaskStatusInTx")
	add("internal/store/taskboard/subtask_create.go", "CreateSubtasks", mutatorDelegating, "createSubtasksRequestForUser,createSubtasksRequestForAgent")
	add("internal/store/taskboard/subtask_create.go", "createSubtasksRequestForUser", mutatorDelegating, "createSubtasksForUser")
	add("internal/store/taskboard/subtask_create.go", "createSubtasksRequestForAgent", mutatorDelegating, "createAgentSubtasks,createSubtasksForAgent")
	add("internal/store/taskboard/subtask_create.go", "createAgentSubtasks", mutatorDelegating, "createSubtasksForAgentTaskScope,createSubtasksForAgent")
	add("internal/store/taskboard/subtask_create.go", "createSubtasksForUser", mutatorDelegating, "createSubtasksInTx")
	add("internal/store/taskboard/subtask_create.go", "createSubtasksForAgent", mutatorDelegating, "createSubtasksInTx")
	add("internal/store/taskboard/subtask_create.go", "createSubtasksForAgentTaskScope", mutatorDelegating, "createSubtasksInTx")
	add("internal/store/taskboard/task_scope_write_authority.go", "AuthorizeUserTaskScopeDiscussionWrite", mutatorDelegating, "authorizeUserTaskScopeDiscussionWrite")
	add("internal/store/taskboard/task_scope_write_authority.go", "AuthorizeAgentTaskScopeDiscussionWrite", mutatorDelegating, "authorizeAgentTaskScopeDiscussionWrite")
	// Public dispatchers (Store/TxStore entry points) that route to the
	// ForUser/ForAgent fenced cores.
	add("internal/store/taskboard/client_service.go", "CreateTasks", mutatorDelegating, "CreateForAgent,CreateForUser")
	add("internal/store/taskboard/client_service.go", "ClaimTasks", mutatorDelegating, "ClaimForAgent,ClaimForUser")
	add("internal/store/taskboard/client_service.go", "UnclaimTask", mutatorDelegating, "UnclaimForAgent,UnclaimForUser")
	add("internal/store/taskboard/client_service.go", "DeleteTask", mutatorDelegating, "DeleteForAgent,DeleteForUser")
	add("internal/store/taskboard/detail_artifacts_checkpoints.go", "AddTaskCheckpoint", mutatorDelegating, "addTaskCheckpointForAgent,addTaskCheckpointForUser")
	add("internal/store/taskboard/detail_mutation.go", "UpdateTask", mutatorDelegating, "updateTaskForAgent,updateTaskForUser")
	add("internal/store/taskboard/detail_mutation.go", "UpdateTaskStatus", mutatorDelegating, "updateTaskStatusForAgent,updateTaskStatusForUser")
	add("internal/store/taskboard/detail_mutation.go", "BlockTask", mutatorDelegating, "BlockTaskForAgent,BlockTaskForUser")
	add("internal/store/taskboard/detail_mutation.go", "UnblockTask", mutatorDelegating, "UnblockTaskForAgent,UnblockTaskForUser")
	add("internal/store/taskboard/detail_review.go", "RequestTaskReview", mutatorDelegating, "requestTaskReviewForAgent,requestTaskReviewForUser")
	add("internal/store/taskboard/detail_review.go", "RequestTaskChanges", mutatorDelegating, "resolveTaskReviewForAgent,resolveTaskReviewForUser")
	add("internal/store/taskboard/detail_review.go", "ApproveTaskReview", mutatorDelegating, "resolveTaskReviewForAgent,resolveTaskReviewForUser")
	add("internal/store/taskboard/detail_review.go", "ReassignTaskReview", mutatorDelegating, "reassignTaskReviewForAgent,reassignTaskReviewForUser")
	add("internal/store/taskboard/detail_review.go", "MarkTaskReviewNotReviewable", mutatorDelegating, "markTaskReviewNotReviewableForAgent,markTaskReviewNotReviewableForUser")

	// fenced_helper: writes durable Task state but only reachable from a fenced
	// entry; must not perform its own membership authorization.
	add("internal/store/taskboard/detail_artifacts_checkpoints.go", "addTaskCheckpointInTx", mutatorFencedHelper, "")
	add("internal/store/taskboard/detail_mutation.go", "blockTaskInTx", mutatorFencedHelper, "")
	add("internal/store/taskboard/detail_mutation.go", "unblockTaskInTx", mutatorFencedHelper, "")
	add("internal/store/taskboard/detail_review.go", "requestTaskReviewInTx", mutatorFencedHelper, "")
	add("internal/store/taskboard/detail_review.go", "reassignTaskReviewInTx", mutatorFencedHelper, "")
	add("internal/store/taskboard/detail_review.go", "resolveTaskReviewInTx", mutatorFencedHelper, "")
	add("internal/store/taskboard/detail_review.go", "markTaskReviewNotReviewableInTx", mutatorFencedHelper, "")
	add("internal/store/taskboard/task_status_update.go", "applyTaskStatusUpdate", mutatorFencedHelper, "")
	add("internal/store/taskboard/service.go", "insertTask", mutatorFencedHelper, "")
	add("internal/store/taskboard/user_tasks.go", "deleteLockedTask", mutatorFencedHelper, "")
	add("internal/store/taskboard/collaborator_scope_execution.go", "ensureTaskScopeExecution", mutatorFencedHelper, "")
	add("internal/store/taskboard/collaborator_grant.go", "grantTaskCollaboratorInTx", mutatorFencedHelper, "")
	add("internal/store/taskboard/review_rollup.go", "rollUpChildReviewsForParentTask", mutatorFencedHelper, "", "TxStore")
	return out
}

type mutatorChecker struct {
	root      string
	fset      *token.FileSet
	failed    bool
	fails     []string
	closure   map[string]mutatorClosureEntry
	fileFuncs map[string]map[string]*ast.FuncDecl // rel -> fn -> decl
	// scoped restricts check() to the pre-seeded fileFuncs/closure so the
	// mutation-based self-tests exercise one dimension without walking the whole
	// repository.
	scoped bool
	// extraFuncs are synthetic functions merged into fileFuncs after the walk
	// (used by self-tests to inject a mutator that does not exist on disk).
	extraFuncs map[string]map[string]*ast.FuncDecl
}

func newMutatorChecker(root string) *mutatorChecker {
	return &mutatorChecker{
		root:      root,
		fset:      token.NewFileSet(),
		closure:   committedMutatorClosure(),
		fileFuncs: map[string]map[string]*ast.FuncDecl{},
	}
}

func (c *mutatorChecker) fail(format string, args ...any) {
	c.failed = true
	c.fails = append(c.fails, fmt.Sprintf(format, args...))
}

func (c *mutatorChecker) walkFiles() {
	roots := []string{"internal", "cmd", "desktop"}
	for _, relativeRoot := range roots {
		walkRoot := filepath.Join(c.root, relativeRoot)
		if _, err := os.Stat(walkRoot); err != nil {
			continue
		}
		err := filepath.WalkDir(walkRoot, func(path string, entry os.DirEntry, walkErr error) error {
			if walkErr != nil {
				return walkErr
			}
			if entry.IsDir() {
				switch entry.Name() {
				case ".git", "node_modules", "vendor":
					return filepath.SkipDir
				}
				return nil
			}
			if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
				return nil
			}
			parsed, err := parser.ParseFile(c.fset, path, nil, parser.SkipObjectResolution)
			if err != nil {
				return fmt.Errorf("parse %s: %w", path, err)
			}
			rel, err := filepath.Rel(c.root, path)
			if err != nil {
				return err
			}
			rel = filepath.ToSlash(rel)
			if c.fileFuncs[rel] == nil {
				c.fileFuncs[rel] = map[string]*ast.FuncDecl{}
			}
			for _, decl := range parsed.Decls {
				fd, ok := decl.(*ast.FuncDecl)
				if !ok {
					continue
				}
				c.fileFuncs[rel][fd.Name.Name] = fd
			}
			return nil
		})
		if err != nil {
			c.fail("walk production Go under %s: %v", relativeRoot, err)
		}
	}
}

// funcHasCallToken reports whether fn's body contains an identifier/selector call
// whose last segment equals needle.
func funcHasCallToken(fn *ast.FuncDecl, needle string) bool {
	found := false
	if fn.Body == nil {
		return false
	}
	ast.Inspect(fn.Body, func(n ast.Node) bool {
		if found {
			return false
		}
		call, ok := n.(*ast.CallExpr)
		if !ok {
			return true
		}
		switch fun := call.Fun.(type) {
		case *ast.Ident:
			if fun.Name == needle {
				found = true
			}
		case *ast.SelectorExpr:
			if fun.Sel.Name == needle {
				found = true
			}
		}
		return !found
	})
	return found
}

// funcContainsToken reports whether fn's body references the token anywhere
// (identifier or selector segment).
func funcContainsToken(fn *ast.FuncDecl, needle string) bool {
	found := false
	if fn.Body == nil {
		return false
	}
	ast.Inspect(fn.Body, func(n ast.Node) bool {
		if found {
			return false
		}
		switch node := n.(type) {
		case *ast.Ident:
			if node.Name == needle {
				found = true
			}
		case *ast.SelectorExpr:
			if node.Sel.Name == needle {
				found = true
			}
		}
		return !found
	})
	return found
}

// firstDurableWritePos returns the token.Pos of the first statement in fn that
// contains a durable write token, or 0 if none. token.Pos offsets are comparable
// within the same parsed file regardless of which FileSet produced them, so the
// synthetic self-test functions (parsed in a private FileSet) order correctly
// against the same function's fence.
func (c *mutatorChecker) firstDurableWritePos(fn *ast.FuncDecl) token.Pos {
	first := token.Pos(0)
	if fn.Body == nil {
		return 0
	}
	ast.Inspect(fn.Body, func(n ast.Node) bool {
		if first != 0 {
			return false
		}
		var text string
		switch node := n.(type) {
		case *ast.BasicLit:
			if node.Kind == token.STRING {
				text = strings.ToUpper(node.Value)
			}
		case *ast.Ident:
			text = strings.ToUpper(node.Name)
		case *ast.SelectorExpr:
			text = strings.ToUpper(node.Sel.Name)
		}
		if text == "" {
			return true
		}
		for _, tok := range durableWriteTokens {
			if strings.Contains(text, strings.ToUpper(tok)) {
				first = n.Pos()
				return false
			}
		}
		return true
	})
	return first
}

// earliestFencePos returns the token.Pos of the earliest call to the declared
// fence in fn, or 0 if the fence is not called.
func (c *mutatorChecker) earliestFencePos(fn *ast.FuncDecl, fence string) token.Pos {
	earliest := token.Pos(0)
	if fn.Body == nil {
		return 0
	}
	ast.Inspect(fn.Body, func(n ast.Node) bool {
		if earliest != 0 {
			return false
		}
		call, ok := n.(*ast.CallExpr)
		if !ok {
			return true
		}
		name := ""
		switch fun := call.Fun.(type) {
		case *ast.Ident:
			name = fun.Name
		case *ast.SelectorExpr:
			name = fun.Sel.Name
		}
		if name == fence {
			earliest = n.Pos()
		}
		return earliest == 0
	})
	return earliest
}

// callNamesInFunc returns the set of function names directly called by fn
// (selector segment or bare ident), for write-bearing closure detection.
func callNamesInFunc(fn *ast.FuncDecl) map[string]bool {
	out := map[string]bool{}
	if fn.Body == nil {
		return out
	}
	ast.Inspect(fn.Body, func(n ast.Node) bool {
		call, ok := n.(*ast.CallExpr)
		if !ok {
			return true
		}
		switch fun := call.Fun.(type) {
		case *ast.Ident:
			out[fun.Name] = true
		case *ast.SelectorExpr:
			out[fun.Sel.Name] = true
		}
		return true
	})
	return out
}

// check runs the full closure verification.
func (c *mutatorChecker) check() {
	if !c.scoped {
		c.walkFiles()
	}
	for rel, funcs := range c.extraFuncs {
		if c.fileFuncs[rel] == nil {
			c.fileFuncs[rel] = map[string]*ast.FuncDecl{}
		}
		for name, fn := range funcs {
			c.fileFuncs[rel][name] = fn
		}
	}

	// Compute the set of committed cores that transitively write durable Task
	// state. A committed fenced_helper always writes; a committed fenced_entry
	// writes if its body directly writes or calls a write-bearing committed
	// function. Read-only fenced entries (e.g. resolveReadableUserTask, which
	// only locks + reads) are not write cores, so read delegators that call them
	// are not required in the mutation closure.
	writeCore := map[string]bool{}
	changed := true
	for changed {
		changed = false
		for _, entry := range c.closure {
			switch entry.Classification {
			case mutatorFencedHelper:
				if !writeCore[entry.Function] {
					writeCore[entry.Function] = true
					changed = true
				}
			case mutatorFencedEntry:
				if writeCore[entry.Function] {
					continue
				}
				fn := c.fileFuncs[entry.File][entry.Function]
				if fn == nil {
					continue
				}
				writes := c.firstDurableWritePos(fn) > 0
				if !writes {
					for call := range callNamesInFunc(fn) {
						if writeCore[call] {
							writes = true
							break
						}
					}
				}
				if writes {
					writeCore[entry.Function] = true
					changed = true
				}
			}
		}
	}

	// Every committed entry must exist and reach its declared fence/delegate.
	for key, entry := range c.closure {
		fn := c.fileFuncs[entry.File][entry.Function]
		if fn == nil {
			c.fail("stale committed mutator closure entry %s: function not found in production", key)
			continue
		}
		switch entry.Classification {
		case mutatorFencedEntry:
			if entry.Fence == "" {
				c.fail("%s fenced_entry has no declared fence", key)
				continue
			}
			fencePos := c.earliestFencePos(fn, entry.Fence)
			if fencePos == 0 {
				c.fail("%s (%s) must call its declared fence %q before any durable write", key, entry.Classification, entry.Fence)
				continue
			}
			firstWrite := c.firstDurableWritePos(fn)
			if firstWrite > 0 && fencePos > firstWrite {
				c.fail("%s (%s) must call its declared fence %q BEFORE its durable write (fence at line %d after write at line %d)", key, entry.Classification, entry.Fence, c.fset.Position(fencePos).Line, c.fset.Position(firstWrite).Line)
			}
		case mutatorDelegating:
			if entry.Fence == "" {
				c.fail("%s delegating_entry has no declared delegate", key)
				continue
			}
			matched := false
			for _, delegate := range strings.Split(entry.Fence, ",") {
				if funcHasCallToken(fn, strings.TrimSpace(delegate)) {
					matched = true
					break
				}
			}
			if !matched {
				c.fail("%s delegating_entry must call one of its declared delegates (%q)", key, entry.Fence)
			}
			// A delegating entry must not carry its own durable write.
			if c.firstDurableWritePos(fn) > 0 {
				c.fail("%s delegating_entry must not carry its own durable write; delegate to %q", key, entry.Fence)
			}
		case mutatorFencedHelper:
			// A fenced_helper must not rely on an ordinary membership read as its
			// only gate (it must be reached only from a fenced entry).
			for _, auth := range ordinaryReadAuthorizers {
				if funcContainsToken(fn, auth) {
					c.fail("%s fenced_helper must not authorize via ordinary membership read %q; it is reached only from a fenced entry", key, auth)
				}
			}
		default:
			c.fail("%s has unknown classification %q", key, entry.Classification)
		}
	}

	// No NEW Task mutator may appear: every TxStore method in
	// internal/store/taskboard that (a) directly writes durable Task state OR
	// (b) directly calls a committed write-bearing core (fenced_entry that
	// transitively writes, or a fenced_helper) must itself be committed in the
	// closure. Anything else is a mutator that bypasses the reviewed fence.
	if c.scoped {
		return
	}
	for rel, funcs := range c.fileFuncs {
		if !strings.HasPrefix(rel, "internal/store/taskboard/") {
			continue
		}
		for fnName, fn := range funcs {
			if !isTxStoreMethod(fn) {
				continue
			}
			key := rel + "::" + fnName
			if _, committed := c.closure[key]; committed {
				continue
			}
			if c.firstDurableWritePos(fn) > 0 {
				c.fail("uncommitted Task mutation %s writes durable Task state directly but has no committed fence closure; add it to committedMutatorClosure() with the exact fence it reaches", key)
				continue
			}
			for call := range callNamesInFunc(fn) {
				if writeCore[call] {
					c.fail("uncommitted Task mutation %s directly calls committed write-bearing core %q without its own committed closure; add it to committedMutatorClosure() as a fenced/delegating entry", key, call)
					break
				}
			}
		}
	}
}

func isTxStoreMethod(fn *ast.FuncDecl) bool {
	if fn.Recv == nil || len(fn.Recv.List) != 1 {
		return false
	}
	recvType := ""
	switch t := fn.Recv.List[0].Type.(type) {
	case *ast.Ident:
		recvType = t.Name
	case *ast.StarExpr:
		if id, ok := t.X.(*ast.Ident); ok {
			recvType = id.Name
		}
	case *ast.IndexExpr:
		if id, ok := t.X.(*ast.Ident); ok {
			recvType = id.Name
		}
	}
	return recvType == "TxStore"
}

func runMutatorClosure(argv []string) int {
	fs := flag.NewFlagSet("task-channel-authority-mutator-closure", flag.ExitOnError)
	root := fs.String("root", ".", "repository root")
	if err := fs.Parse(argv); err != nil {
		fmt.Fprintln(os.Stderr, err)
		return 2
	}
	c := newMutatorChecker(*root)
	c.check()
	if c.failed {
		fmt.Fprintf(os.Stderr, "task-channel-authority mutator closure guardrail failed with %d issue(s):\n", len(c.fails))
		for _, f := range c.fails {
			fmt.Fprintf(os.Stderr, "  - %s\n", f)
		}
		return 1
	}
	fmt.Println("task-channel-authority mutator closure guardrail passed.")
	return 0
}
