--- ps1_meta.lua — Orchestrator entry point for the tape-atom metaprogram.
---
--- Dispatches to pass modules under `scripts/passes/`, resolving dependencies topologically (Kahn's algorithm + cycle detection).
---
--- Architecture:
---   - PASSES table: Declarative dep graph (data, not code).
---   - FLAG_HANDLERS table: Maps CLI flags to handlers.
---   - parse_args → build_ctx (resolves unity/direct includes or exact sources) → topo_sort → dispatch_passes.
---   - The first pass in the dep graph is `scan-source` (see `passes/scan_source.lua`).
---     It calls `duffle.scan_source` once per source to produce the fat `SourceScan` payload, which is attached to each `src.scan`. 
---     Every other pass that reads source structure depends on `scan-source` and consumes `src.scan` as a read-only. 
---
-- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup
-- ════════════════════════════════════════════════════════════════════════════

-- Bootstrap: load `duffle_paths.lua` via this script's own path.
-- Use `arg[0]` when this file is the entry script (`arg[0]` ends in "ps1_meta.lua");
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
-- So the dofile's return value is the duffle module.
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil
local _bootstrap_src
if _is_entry_script then
	_bootstrap_src = arg[0]
else
	-- debug.getinfo(1, "S").source returns "@<path>" for the current chunk;
	-- strip the leading "@" so the directory match works in both cases.
	_bootstrap_src = debug.getinfo(1, "S").source:sub(2)
end
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua")

-- ════════════════════════════════════════════════════════════════════════════
-- Constants
-- ════════════════════════════════════════════════════════════════════════════

-- Exit codes (per the --help text and the post-build summary convention).
local EXIT_OK                  = 0
local EXIT_VALIDATION_ERRORS   = 1
local EXIT_INTERNAL_ERROR      = 2

-- Default --out-root value if not provided.
local DEFAULT_OUT_ROOT         = "build/gen"

-- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names).
local ALL_PASSES_SENTINEL      = "__all__"

-- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`. 
-- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag.
local PASS_FLAG_DISPATCH_KEY   = "__pass__"

-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
-- ════════════════════════════════════════════════════════════════════════════

--- @class PassDescriptor
--- @field module string       -- Module name passed to require()
--- @field kind   string       -- "shared" | "header-output" | "validation" | "diagnostic" | "report"
---                            -- Report severity is independent from process exit policy (see PASS_KIND_STOP_ON_ERROR).
--- @field deps   string[]     -- Names of upstream passes
--- @field groups string[]?    -- OPTIONAL build-phase groups this pass is a root of (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only

--- @class SourceFile
--- @field path     string  -- Absolute path to the source file
--- @field text     string  -- Full source text
--- @field dir      string  -- Directory containing the source
--- @field basename string  -- Filename without extension

--- @class PassCtx
--- @field metadata_path      string                 -- Path to word_count.metadata.h
--- @field shared             table                  -- Cross-pass shared state
--- @field shared.corpus      table                  -- Authored-source/project projection
--- @field out_root           string                 -- Output root (e.g. "build/gen")
--- @field project_root       string                 -- PS1 repository root
--- @field flags              table                  -- CLI flags + per-pass stash
--- @field verbose            boolean                -- If true, log diagnostic info

--- @class Finding
--- @field line integer  -- Source line (or 0 for pass-level)
--- @field msg  string   -- Finding message

--- @class PassResult
--- @field outputs  PassOutputEntry[] -- Emitted file paths
--- @field errors   Finding[]         -- Build-stops (per-pass kind policy)
--- @field warnings Finding[]         -- Informational

--- @class ParsedArgs
--- @field requested_set string[]   -- Pass names to run (explicit --all expanded)
--- @field sources       string[]   -- Exact --source values, retained in CLI order
--- @field unity_root    string|nil -- --unity-root value; mutually exclusive with sources
--- @field metadata      string     -- --metadata value
--- @field out_root      string     -- --out-root value (default "build/gen")
--- @field project_root  string     -- PS1 repository root (derived from metadata by default)
--- @field verbose       boolean    -- If true, log diagnostic info

-- ════════════════════════════════════════════════════════════════════════════
-- PASSES Table
-- ════════════════════════════════════════════════════════════════════════════

-- Build-phase groups: Each PASSES row may declare membership in one or more named groups via `groups = { ... }`.
-- The CLI flags --pre-link and --post-link request the *roots* of their group; topo_sort then closes transitive dependencies from those roots,
-- and dispatch_passes runs every pass in the resulting closure without phase-filtering.
--
-- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it, 
-- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source).

local PASSES = {
	["scan-source"] = {
		module = "passes.scan_source",
		kind   = "shared", deps = {},
	},
	["word-counts"] = {
		module = "passes.word_count_eval",
		kind   = "shared", deps = {},
	},
	components = {
		module = "passes.components",
		kind   = "header-output",
		deps   = {"scan-source", "word-counts"},
	},
	auto_reg = {
		module = "passes.auto_reg",
		kind   = "header-output",
		deps   = {"components"},
		groups = { "pre-link" },
	},
	["emission-model"] = {
		module = "passes.emission_model",
		kind   = "validation",
		deps   = {"components"},
	},
	annotation = {
		module = "passes.annotation",
		kind   = "validation",
		deps   = {"scan-source", "word-counts"},
	},
	offsets = {
		module = "passes.offsets",
		kind   = "header-output",
		deps   = {"scan-source", "word-counts", "components", "emission-model"},
		groups = { "pre-link" },
	},
	["static-analysis"] = {
		module = "passes.static_analysis",
		-- "diagnostic" — every `error`/`warning` finding is written to the report file;
		-- The orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR).
		-- Report severity is independent from process exit policy.
		kind   = "diagnostic",
		deps   = {"scan-source", "word-counts", "components", "emission-model"},
	},
	["atoms-source-map"] = {
		module = "passes.atoms_source_map",
		kind   = "header-output",
		deps   = {"word-counts", "components", "emission-model"},
	},
	["dwarf-injection"] = {
		module = "passes.dwarf_injection",
		kind   = "shared",
		deps   = {"scan-source", "atoms-source-map"},
		groups = { "post-link" },
	},
	report = {
		module = "passes.report",
		kind   = "report",
		deps   = {"annotation", "static-analysis", "atoms-source-map"},  -- +atoms-source-map (consolidated-report-files refactor, 2026-07-26)
		groups = { "pre-link" },
	},
}

-- ────────────────────────────────────────────────────────────────────────────
-- Phase-root selection: Derive the sorted set of roots belonging to a named build-phase group, then append them to `args.requested_set`.
-- topo_sort closes the transitive deps from there; dispatch_passes runs every resolved pass without phase-filtering.
-- ────────────────────────────────────────────────────────────────────────────

--- @param group_name string  -- Build-phase group ("pre-link" | "post-link")
--- @return string[]          -- Sorted root pass names belonging to that group
local function roots_for_group(group_name)
	local names = {}
	for name, pass in pairs(PASSES) do
		if pass.groups then
			for _, g in ipairs(pass.groups) do
				if g == group_name then
					names[#names + 1] = name
					break
				end
			end
		end
	end
	table.sort(names)
	return names
end

--- Append every root belonging to `group_name` to `args.requested_set`.
--- Errors loudly if no PASSES row declares the group, so a typo'd or future-removed group name
--- cannot silently fall through to pre-link (or any other default) and dispatch nothing.
--- @param args ParsedArgs
--- @param group_name string
local function request_roots_for_group(args, group_name)
	local roots = roots_for_group(group_name)
	if #roots == 0 then
		error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field"
			, group_name, group_name))
	end
	for _, name in ipairs(roots) do
		args.requested_set[#args.requested_set + 1] = name
	end
end

-- Pass-kind taxonomy: Which kinds stop the build on errors?
--
-- Report severity is independent from process exit policy.
-- A "diagnostic" pass still writes every `error`/`warning` finding into its report file,
-- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero.
-- Adding a new pass kind requires listing it here explicitly; An unknown kind must not silently fall back to "true".
local PASS_KIND_STOP_ON_ERROR = {
	["shared"]        = false,
	["header-output"] = true,
	["validation"]    = true,
	["diagnostic"]    = false,
	["report"]        = false,
}

-- Closed set of CLI flags -> pass names.
-- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
local PASS_FLAG_TO_NAME = {
	["--word-counts"]       = "word-counts",
	["--components"]        = "components",
	["--validate"]          = "annotation",
	["--offsets"]           = "offsets",
	["--static-analysis"]   = "static-analysis",
	["--atoms-source-map"]  = "atoms-source-map",
	["--report"]            = "report",
	["--scan-source"]       = "scan-source",
	["--all"]               = ALL_PASSES_SENTINEL,
}

--- Append every pass name to args.requested_set.
--- Names are derived from PASSES (no parallel name list); used by --all and by any caller that wants the full closure.
--- @param args ParsedArgs
local function request_all_passes(args)
	local names = {}
	for name in pairs(PASSES) do names[#names + 1] = name end
	table.sort(names)
	for _, n in ipairs(names) do
		args.requested_set[#args.requested_set + 1] = n
	end
end

-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it). 
-- Returning nil + os.exit() handles termination flags (--help). 
local FLAG_HANDLERS = {}

-- ════════════════════════════════════════════════════════════════════════════
-- CLI parsing
-- ════════════════════════════════════════════════════════════════════════════

--- Print the CLI usage to stdout and exit 0.
local function print_help()
	io.write([[
ps1_meta.lua - Tape-atom metaprogram orchestrator

USAGE:
  ps1_meta.lua [PASS_FLAGS] [COMMON_FLAGS]

PASS_FLAGS:
  Pick a phase or one-or-more individual passes:
    --pre-link           [phase; default] Run the pre-link group + transitive deps.
                         The root set is data-driven from each PASSES row's groups` field; no parallel name list is maintained.
    --post-link          [phase] Run the post-link group + transitive deps.
                         Requires --elf. Sets --gdb-runtime and --dwarf-injection opt-in flags as well.
    --all                Select every row of the PASSES table. Pass-local opt-in guards remain active, so --dwarf-injection still requires
                         --elf and --gdb-runtime still requires a runtime emission.
  Or pick any subset:
    --scan-source         Scan sources into the fat SourceScan payload
    --word-counts         Load metadata.h + scan for existing .macs.h
    --components          Generate <srcdir>/gen/macs.h (per-directory aggregation)
    --validate            Run atom annotation DSL validation
    --offsets             Generate <srcdir>/gen/offsets.h (per-directory aggregation)
    --atoms-source-map    Generate <basename>.atoms.sourcemap.txt per source
    --dwarf-injection     [opt-in] Select the post-link dwarf-injection pass + set the opt-in flag. Requires --elf.
    --static-analysis     Static analysis: GTE pipeline-fill, mac_yield, ABI handoff, cycle budget
    --report              Render per-project summary

COMMON_FLAGS:
  --unity-root FILE     Unity source root: load root + direct quoted authored includes only. Mutually exclusive with --source.
  --source FILE         Exact source file to process (repeatable, never expands includes). Mutually exclusive with --unity-root.
  --metadata PATH       Path to metadata.h (required)
  --out-root DIR        Output root for reports (default: build/gen)
  --project-root DIR    PS1 repository root (default: derived from <repo>/code/duffle/word_count.metadata.h)
  --gdb-runtime         Also emit <out_root>/gdb_tape_atoms_runtime.gdb (post-link, requires --elf)
  --elf PATH            Path to linked .elf (for --gdb-runtime / --dwarf-injection)
  --verbose             Print per-pass debug output
  --help                Show this help and exit

EXIT CODES:
  0  All requested passes succeeded
  1  Validation errors found
  2  Metaprogram internal error

EXAMPLES:
  ps1_meta.lua --pre-link  --metadata code/duffle/word_count.metadata.h --unity-root code/gte_hello/hello_gte.c
  ps1_meta.lua --post-link --metadata code/duffle/word_count.metadata.h --unity-root code/gte_hello/hello_gte.c --elf build/hello_gte.elf
  ps1_meta.lua --all       --metadata metadata.h --source code/foo.c --source code/bar.c
]])
end

local FLAG_VALUE_NAMES = {
	["--source"]       = "FILE",
	["--unity-root"]   = "FILE",
	["--metadata"]     = "PATH",
	["--out-root"]     = "DIR",
	["--project-root"] = "DIR",
	["--elf"]          = "PATH",
}

local function require_flag_value(argv, arg_idx, flag)
	local value      = argv[arg_idx + 1]
	local next_known = type(value) == "string"
		and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
	if value == nil or next_known then
		io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
		os.exit(EXIT_INTERNAL_ERROR)
	end
	return value, arg_idx + 1
end

-- Per-flag handlers. Each takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
-- Termination flags like --help call os.exit() instead.
-- Populated AFTER print_help so the --help handler can reference it as an upvalue (Lua resolves locals at closure-call time, 
-- but if the closure is defined before the local, it falls back to _G). 

FLAG_HANDLERS["--help"]    = function(args) print_help(); os.exit(0) end
FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true      end

FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
	local value, value_idx = require_flag_value(argv, arg_idx, "--source")
	args.sources[#args.sources + 1] = value
	return value_idx
end
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
	local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root")
	args.unity_root = value
	return value_idx
end
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
	local value, value_idx = require_flag_value(argv, arg_idx, "--metadata")
	args.metadata = value
	return value_idx
end
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
	local value, value_idx = require_flag_value(argv, arg_idx, "--out-root")
	args.out_root = value
	return value_idx
end
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
	local value, value_idx = require_flag_value(argv, arg_idx, "--project-root")
	args.project_root = value
	return value_idx
end

-- Per-pass stash flags. Read by `passes/atoms_source_map.lua` to opt into the post-link gdb-runtime emission.
-- Same shape as the existing per-flag handlers. mutates `args.flags` (which propagates into `ctx.flags`).
FLAG_HANDLERS["--gdb-runtime"] = function(args)
	args.flags = args.flags or {}
	args.flags.gdb_runtime = true
end
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
	local value, value_idx = require_flag_value(argv, arg_idx, "--elf")
	args.flags = args.flags or {}
	args.flags.elf_path = value
	return value_idx
end
-- Enable DWARF injection (default OFF). Opts in to the post-link pass and sets the flag in one shot.
-- The explicit handler below owns both selection and opt-in state, so --dwarf-injection is intentionally absent from PASS_FLAG_TO_NAME.
FLAG_HANDLERS["--dwarf-injection"] = function(args)
	args.flags                 = args.flags or {}
	args.flags.dwarf_injection = true
	args.requested_set[#args.requested_set + 1] = "dwarf-injection"
end
-- Build-phase flags: --pre-link and --post-link request the roots of their declared groups (see roots_for_group).
-- topo_sort closes transitive deps from those roots; dispatch_passes runs every pass in the resolved closure without phase-filtering.
FLAG_HANDLERS["--pre-link"] = function(args)
	request_roots_for_group(args, "pre-link")
end
-- Batch post-link phase: gdb-runtime + dwarf-injection in one luajit cold start.
-- Sets the same opt-in flags as --gdb-runtime + --dwarf-injection and selects the post-link build-phase group.
-- elf is required; parse_args enforces it after all flags are parsed.
FLAG_HANDLERS["--post-link"]      = function(args)
	args.flags = args.flags or {}
	args.flags.gdb_runtime     = true
	args.flags.dwarf_injection = true
	request_roots_for_group(args, "post-link")
end

-- `--dwarf-injection` also emits atom-local debug data.

-- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set.
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
	local name = PASS_FLAG_TO_NAME[a]
	if name == ALL_PASSES_SENTINEL then
		request_all_passes(args)
		return
	end
	args.requested_set[#args.requested_set + 1] = name
end

--- Parse argv into a structured table. Validates against a closed enum.
--- @param argv string[]
--- @return ParsedArgs
local function parse_args(argv)
	local args = {
		requested_set = {},
		sources       = {},
		unity_root    = nil,
		metadata      = nil,
		out_root      = DEFAULT_OUT_ROOT,
		project_root  = nil,
		verbose       = false,
	}

	local pos = 1
	while pos <= #argv do
		local a       = argv[pos]
		local handler = FLAG_HANDLERS[a]
		if handler then
			pos = handler(args, argv, pos) or pos
		elseif PASS_FLAG_TO_NAME[a] then
			FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY](args, a)
		else
			io.stderr:write("ps1_meta: unknown flag '" .. a .. "'\n")
			io.stderr:write("Run with --help for usage.\n")
			os.exit(EXIT_INTERNAL_ERROR)
		end
		pos = pos + 1
	end

	-- Default: --pre-link if no explicit pass flags were given.
	-- The first invocation of a build is always pre-link, so this avoids silently also invoking post-link work in builds without an ELF artifact.
	if #args.requested_set == 0 then request_roots_for_group(args, "pre-link") end

	if not args.metadata then
		io.stderr:write("ps1_meta: --metadata PATH is required\n")
		os.exit(EXIT_INTERNAL_ERROR)
	end

	-- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location. 
	-- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately.
	if not args.project_root then
		local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata))
		local code_root    = duffle.dirname(metadata_dir)
		args.project_root  = duffle.dirname(code_root)
	else
		args.project_root = duffle.normalize_path(args.project_root)
	end

	local has_unity = type(args.unity_root) == "string" and args.unity_root ~= ""
	if has_unity and #args.sources > 0 then
		io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n")
		os.exit(EXIT_INTERNAL_ERROR)
	end
	if not has_unity and #args.sources == 0 then
		io.stderr:write("ps1_meta: either --unity-root FILE or at least one --source FILE is required\n")
		os.exit(EXIT_INTERNAL_ERROR)
	end

	-- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF.
	-- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early.
	-- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself.
	local flags      = args.flags or {}
	local elf_path   = flags.elf_path
	local has_elf    = type(elf_path) == "string" and #elf_path > 0
	local post_links = flags.gdb_runtime or flags.dwarf_injection
	if post_links and not has_elf then
		io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n")
		os.exit(EXIT_INTERNAL_ERROR)
	end

	return args
end

-- ════════════════════════════════════════════════════════════════════════════
-- Build ctx from parsed args
-- ════════════════════════════════════════════════════════════════════════════

--- Build the PassCtx from parsed args. Exact mode opens only the repeated `--source` inputs;
--- unity mode delegates direct-include resolution to duffle.resolve_source_corpus`. 
--- Scanning remains pass-owned (`src.scan`).
--- @param args ParsedArgs
--- @return PassCtx
local function build_ctx(args)
	local normalized_project_root  = duffle.normalize_path(args.project_root)
	local project_root             = normalized_project_root
	local project_root_is_absolute = normalized_project_root:match("^%a:/")
		or normalized_project_root:sub(1, 2) == "//"
		or normalized_project_root:sub(1, 1) == "/"
	if not project_root_is_absolute then
		-- canonical_path_key validates ordinary relative paths and rejects drive-relative paths before the absolute-path rewrite is performed.
		duffle.canonical_path_key(normalized_project_root)
		project_root = duffle.normalize_path(duffle.to_absolute_path(normalized_project_root))
	else
		-- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
		duffle.canonical_path_key(project_root)
	end
	local resolution
	if args.unity_root then
		local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, {
			unity_root   = args.unity_root,
			project_root = project_root,
		})
		if not ok_resolve then
			io.stderr:write("ps1_meta: cannot resolve --unity-root " .. tostring(args.unity_root) .. ": " .. tostring(resolved) .. "\n")
			os.exit(EXIT_INTERNAL_ERROR)
		end
		resolution = resolved
	else
		local source_order    = {}
		local sources_by_path = {}
		local resolver = {
			resolved = {},
			skipped  = {},
			shadowed = {},
		}
		for _, input_path in ipairs(args.sources) do
			local path = duffle.normalize_path(input_path)
			local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
			if not key_ok then
				error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
			end
			local file = io.open(path, "r")
			if not file then
				io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
				os.exit(EXIT_INTERNAL_ERROR)
			end
			local text = file:read("*a")
			file:close()

			local source = {
				path     = path,
				text     = text,
				dir      = duffle.dirname(path),
				basename = duffle.basename_no_ext(path),
			}
			source_order[#source_order + 1] = source
			local key = key_or_error
			if not sources_by_path[key] then sources_by_path[key] = source end
			resolver.resolved[#resolver.resolved + 1] = {
				include_path  = path,
				include_text  = nil,
				root_source   = nil,
				root_line     = nil,
				candidate_a   = path,
				candidate_b   = nil,
				selected_path = path,
				disposition   = "exact",
			}
		end
		resolution = {
			unity_root      = nil,
			project_root    = project_root,
			code_root       = duffle.normalize_path(project_root .. "/code"),
			source_order    = source_order,
			sources_by_path = sources_by_path,
			sources_by_dir  = duffle.group_sources_by_dir(source_order),
			resolver        = resolver,
		}
	end

	local corpus = {
		unity_root              = resolution.unity_root,
		project_root            = resolution.project_root,
		code_root               = resolution.code_root,
		source_order            = resolution.source_order,
		sources_by_path         = resolution.sources_by_path,
		sources_by_dir          = resolution.sources_by_dir,
		atoms_by_name           = {},
		binds_by_name           = {},
		atom_infos              = {},
		register_alias_registry = {},
		type_name_registry      = {},
		atom_views              = {},
		atom_ctxs               = {},
		atom_phases             = {},
		word_counts             = {},
		components              = {},
		component_body_index    = {},
		collisions              = {},
		resolver                = resolution.resolver,
	}
	local ctx = {
		metadata_path = args.metadata,
		shared        = { corpus = corpus },
		out_root      = args.out_root,
		project_root  = corpus.project_root,
		flags         = args.flags or {},
		verbose       = args.verbose,
	}

	-- Source records and directory buckets are owned by the corpus.
	-- Consumers read `corpus.source_order` and `corpus.sources_by_dir` directly.
	-- The corpus is the sole source of truth for source records and module grouping; `ctx` only holds per-pass execution state.
	return ctx
end

-- ════════════════════════════════════════════════════════════════════════════
-- Topological sort (Kahn's algorithm + cycle detection)
-- ════════════════════════════════════════════════════════════════════════════

--- Topologically sort the requested pass set, augmented with all transitive deps.
--- Detects cycles and errors out with details.
--- @param passes        table<string, PassDescriptor>
--- @param requested_set string[]
--- @return string[]  -- execution order
---
--- Dependency closure, in-degree calculation, queue seeding, and sorting are local blocks.
--- Keeping these blocks local makes the topological sort self-contained.
local function topo_sort(passes, requested_set)
	-- Dependency closure: include every pass transitively required by `requested_set`.
	local needed = {}
	for _, name in ipairs(requested_set) do needed[name] = true end
	local changed = true
	while changed do
		changed = false
		for name, _ in pairs(needed) do
			local pass = passes[name]
			if not pass then error("unknown pass '" .. name .. "' requested") end
			for _, dep in ipairs(pass.deps) do
				if not needed[dep] then
					needed[dep] = true
					changed = true
				end
			end
		end
	end

	-- In-degree calculation: count each needed pass's needed dependencies.
	local in_degree = {}
	for name, _ in pairs(needed) do in_degree[name] = 0 end
	for name, _ in pairs(needed) do
		for _, dep in ipairs(passes[name].deps) do
			if needed[dep] then
				in_degree[name] = in_degree[name] + 1
			end
		end
	end

	-- Ready-queue seeding: add zero-in-degree passes in deterministic order.
	local ready = {}
	for name, deg in pairs(in_degree) do
		if deg == 0 then ready[#ready + 1] = name end
	end
	table.sort(ready)

	-- Ready-queue drain: decrement dependents when each pass is emitted.
	-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
	local order = {}
	while #ready > 0 do
		local just_finished = table.remove(ready, 1)
		order[#order + 1] = just_finished
		for name, _ in pairs(needed) do
			if name ~= just_finished then
				for _, dep in ipairs(passes[name].deps) do
					if dep == just_finished then
						in_degree[name] = in_degree[name] - 1
						if in_degree[name] == 0 then
							ready[#ready + 1] = name
							table.sort(ready)
						end
					end
				end
			end
		end
	end

	-- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0
	-- (the cycle closed on itself before Kahn could process them).
	-- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing.
	local needed_count = 0
	for _ in pairs(needed) do needed_count = needed_count + 1 end  -- count hash entries; Lua's #t doesn't work
	if #order ~= needed_count then
		for name, deg in pairs(in_degree) do
			if deg > 0 then
				error("dependency cycle detected involving pass '" .. name .. "'")
			end
		end
	end

	return order
end

-- ════════════════════════════════════════════════════════════════════════════
-- Main Orchestrator
-- ════════════════════════════════════════════════════════════════════════════

--- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr.
--- Returns true if any validation errors were reported.
--- @param pass_name string
--- @param pass      PassDescriptor
--- @param result    PassResult
--- @return boolean
local function report_validation_errors(pass_name, pass, result)
	local   has_errors = result.errors and #result.errors > 0
	if not (has_errors and PASS_KIND_STOP_ON_ERROR[pass.kind]) then return false end
	for _, e in ipairs(result.errors) do
		io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
	end
	return true
end

--- (internal) Run each pass in `order` in topological sequence.
--- @param ctx   PassCtx
--- @param order string[]
--- @return boolean  -- true if any validation errors were reported
local function dispatch_passes(ctx, order)
	local had_errors = false
	for _, pass_name in ipairs(order) do
		local pass   = PASSES[pass_name]
		local mod    = require(pass.module)
		local result = mod.run(ctx)
		if report_validation_errors(pass_name, pass, result) then
			had_errors = true
		end
	end
	return had_errors
end

--- Main entry point. Runs the requested passes in dep-topological order.
--- @param argv string[]
local function main(argv)
	local ok, err = pcall(function()
		local args = parse_args(argv)
		local ctx  = build_ctx(args)

		local requested = args.requested_set
		local closed    = topo_sort(PASSES, requested)

		local had_errors = dispatch_passes(ctx, closed)
		if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
	end)

	if not ok then
		io.stderr:write("[ps1_meta] internal error: " .. tostring(err) .. "\n")
		os.exit(EXIT_INTERNAL_ERROR)
	end

	os.exit(EXIT_OK)
end

-- Module export for in-process consumers (tests that dofile this script).
-- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua");
-- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller.
local _M = {
	PASSES                   = PASSES,
	PASS_KIND_STOP_ON_ERROR  = PASS_KIND_STOP_ON_ERROR,
	parse_args               = parse_args,
	build_ctx                = build_ctx,
}

if arg and arg[0] and arg[0]:match("ps1_meta%.lua$") then
	main({...})
end

return _M
