# The gates. `make lint` green is the definition of a booted coordinator.
#
# MEMORY_GATE_RATIFIED_ID is the id the memory-attribution gate checks its
# --ratified argument against (its MODE_AUTHORITY). At genesis it IS the
# genesis ruling id -- one block ratifies the imported doctrine and authorises
# this gate with it -- and genesis derives it from GENESIS_DECISION_ID unless
# the instance answers it. They diverge only as an instance ages and the gate
# is re-authorised by a later ruling; that is why this is its own token and no
# longer GENESIS_DECISION_ID (2026-09-05, the decision-id token split).
MEMORY_GATE_RATIFIED_ID ?= {{MEMORY_GATE_RATIFIED_ID}}

.PHONY: lint project-type-check doc-index doc-index-check doc-index-selftest decision-gate \
        decision-citation decision-registry-drift decision-id-stability \
        memory-attribution history-anchors scrub lint-code \
        code-map code-map-check post-clear-manifest post-clear-manifest-check \
        grep-count-census grep-count-census-selftest \
        stderr-redirect-census stderr-redirect-census-selftest \
        comparator-census comparator-census-selftest \
        assumption-registry-check assumption-registry-selftest \
        file-roles-selftest file-roles-banner-selftest file-roles-coverage-census \
        postmortem-ledger-lint postmortem-ledger-lint-selftest \
        postmortem-recurrence-selftest postmortem-mirror-selftest

lint: decision-citation memory-attribution doc-index-check code-map-check history-anchors post-clear-manifest-check grep-count-census stderr-redirect-census project-type-check assumption-registry-check file-roles-selftest file-roles-banner-selftest file-roles-coverage-census postmortem-ledger-lint postmortem-ledger-lint-selftest postmortem-recurrence-selftest postmortem-mirror-selftest stamp-selftest role-section-single-home role-section-single-home-selftest
	@echo "lint: PASS"

# postmortem-ledger-lint -- every post-mortem is a countable row of
# docs/postmortems/ledger.tsv (one row per post-mortem; a post-mortem without
# its row is a lint refusal). A post-mortem file with no row, a `none` rule, an
# unresolved anchor or an out-of-vocabulary area/trigger refuses. A fresh render
# carries the header row only, so the lint is green until the first post-mortem.
# The three selftests: both polarities of the lint on fixture roots, the 7-day
# recurrence count (watchdog check 69 and the mirror call it), and the daily
# 4:00am mirror against a local bare repository (no network).
postmortem-ledger-lint:
	python3 scripts/postmortem-ledger-lint.py

postmortem-ledger-lint-selftest:
	bash testing/lint/postmortem-ledger-lint-selftest.sh

postmortem-recurrence-selftest:
	python3 testing/lint/postmortem-recurrence-selftest.py

postmortem-mirror-selftest:
	bash testing/lint/postmortem-mirror-selftest.sh

# The post-compact protocol's stamp and the SessionStart hook's single-home
# rule (port sweep run 27; the source's lint-nocargo legs of the same names).
stamp-selftest:
	bash scripts/stamp.sh --selftest

role-section-single-home:
	bash testing/lint/role-section-single-home.sh

role-section-single-home-selftest:
	bash testing/lint/role-section-single-home.sh --selftest

# assumption-registry-check / assumption-registry-selftest -- the assumption
# validation registry (docs/assumption-validation.md). `check` is the GATE and
# is a `lint` leg: header exact, ids unique and slug-shaped, class/scope/moments
# in vocabulary, every event's id present, and the three GENERATED counter
# columns equal to what the append-only event log says they are -- a hand-typed
# counter is itself an assumption about what happened.
#
# The SELFTEST is acceptance for that gate, not a gate, so it is NOT a `lint`
# leg -- it is rostered in gates/selftest-roster.tsv at wiring=fast, the same
# wiring doc-index-selftest carries and for the same reason, spelled in that
# target's own comment above.
assumption-registry-check:
	python3 scripts/assumption-registry.py check

assumption-registry-selftest:
	bash testing/doctrine/assumption-registry-selftest.sh

# file-roles-selftest / file-roles-banner-selftest / file-roles-coverage-census
# -- the FILE-ROLES REGISTRY's three legs (docs/file-roles.tsv: which role
# automation may act on for each path, proven by running the role's predicate
# against the real file in both polarities). The registry is THIS instance's:
# genesis DERIVED it at step 7 (`scripts/lib/file-roles-derive.sh`), it is
# never carried from the template, and `file-roles-selftest` fails the moment
# a re-derivation would not reproduce it byte for byte -- re-derive with
#   . scripts/lib/file-roles-derive.sh && file_roles_derive_tree_only "$$PWD" > docs/file-roles.tsv
#
# THE COVERAGE LEG'S NEWNESS REF IS origin/main, DECIDED (2026-10-02, port
# sweep run 21). "New" means new relative to what the remote already has, so
# before the first push there is no baseline to be new against: the leg SKIPS
# BY NAME until origin/main resolves, and judges requirement (f) from the first
# push on. A fresh render runs `make lint` before its first commit exists, so
# no other ref would mean anything there either. AGENT-DERIVED.
file-roles-selftest:
	bash testing/lint/file-roles-selftest.sh

file-roles-banner-selftest:
	bash testing/lint/file-roles-banner-selftest.sh

file-roles-coverage-census:
	@if git rev-parse --verify -q origin/main >/dev/null 2>&1; then \
	  bash testing/lint/file-roles-coverage-census.sh; \
	else \
	  echo "file-roles-coverage-census: SKIP -- origin/main does not resolve yet (no first push); the newness ref is origin/main, so requirement (f) is judged from the first push on"; \
	fi

doc-index:
	python3 scripts/generate-doc-index.py --write

doc-index-check:
	python3 scripts/generate-doc-index.py --check

# doc-index-selftest — proves scripts/generate-doc-index.py goes RED on every
# violation class it claims to catch and GREEN on a clean tree. Hermetic: a
# synthetic git repo under a temp dir, never this checkout.
#
# NOT A LEG OF `lint`, AND THAT IS THE DELIBERATE WIRING, NOT AN OVERSIGHT
# (2026-09-13, task template-doc-index-selftest-orphan). It is acceptance for a
# gate, not a gate — the gate is `doc-index-check`, which `lint:` already
# depends on above. THE SUITE IS WIRED TO THE ROSTER:
# gates/selftest-roster.tsv carries its row at wiring=fast, so
# gates/genesis-e2e.sh leg (p) and gates/selftests.sh both run it inside a
# RENDERED instance — which is the only place it can run at all, since the
# shipped copy under skeleton/ still carries genesis tokens and will not
# execute unrendered. Membership there is MECHANICAL, not a convention:
# census_check (gates/lib/selftest-roster.sh, genesis-e2e leg (q)) sweeps
# `git ls-files` for {testing,ops}/**/*selftest*.sh and FAILS naming any file
# the roster omits. The source project wires it the same way and for the same
# reason — its `lint:` does not depend on its own doc-index-selftest either.
#
# SO WHAT IS THIS TARGET FOR? Running the suite BY NAME, by hand, and nothing
# else: the template's roster has no `target` column (roster_run_pass_row runs
# `bash <path>` directly), so a Makefile target here has zero gate consequence.
# That is precisely why the disposition is written down. A bare target with no
# `lint:` edge and no note reads as an orphan to anyone auditing this file, and
# this one was filed as an orphan twice — once when the roster was built
# (2026-08-25) and again in 2026-09-13's sweep — both times against a suite
# that had been running on every render the whole time. AGENT-DERIVED.
doc-index-selftest:
	testing/doctrine/doc-index-selftest.sh

# code-map / code-map-check — the GENERATED name-grain map of every Rust item
# under crates/ (template-port-code-map-generator, 2026-08-26; mirrors the
# source project's own `make code-map` / `code-map-check` pair). Same
# anti-drift shape as doc-index: the map is derived from the committed source
# under crates/, and the check regenerates it and proves the committed copy
# byte-identical, so a map left stale fails lint. Deliberately NOT a leg of
# testing/lint/gate.sh (the cargo gate): this is a stdlib-only Python lexer —
# no cargo, no box, no network — so it hangs off `lint` as a prerequisite and
# stays runnable on a project that has no code yet, same as doc-index-check.
# On a fresh instance `crates/` is empty (this template ships no code), so the
# check passes trivially until a project's own crates/ exists.
code-map:
	python3 scripts/generate-code-map.py --write

code-map-check:
	python3 scripts/generate-code-map.py --check

# post-clear-manifest / post-clear-manifest-check — docs/POST-CLEAR.md's word
# counts, GENERATED from `wc -w` on the files its `<!-- wc:path --><!-- /wc -->`
# markers name (template-post-clear-manifest-design, 2026-08-26; a
# schema-agnostic marker convention, deliberately NOT a port of the source
# project's item/table/boot-total schema — see the generator's own docstring).
# Same byte-identical drift-gate shape as doc-index and code-map: --write
# refreshes every marker from disk, --check proves the committed copy
# matches. No cargo, no corpus, no network — hangs off `lint`.
post-clear-manifest:
	python3 scripts/generate-post-clear-manifest.py --write

post-clear-manifest-check:
	python3 scripts/generate-post-clear-manifest.py --check

decision-gate: decision-registry-drift decision-id-stability decision-citation memory-attribution

decision-citation:
	python3 scripts/decision-citation-gate.py

decision-registry-drift:
	python3 scripts/extract-decision-registry.py --check

decision-id-stability:
	testing/doctrine/id-stability.sh

memory-attribution:
	python3 scripts/memory-attribution-gate.py --enforce --ratified $(MEMORY_GATE_RATIFIED_ID)

history-anchors:
	testing/doctrine/history-anchors.sh

# project-type-check — THE RECORDED PROJECT TYPE AND THE RENDERED TREE AGREE.
# Intake question 2 (`./genesis.sh --intake`) answered `backend` or
# `browser-ui`; this asserts the answer is still in the closed set AND that
# `ui/` exists iff the type says it should. Both directions are errors, which
# is what stops a deleted tree from silently redefining the project and turning
# the ui gate leg off. A `lint` prerequisite because it is hermetic — no cargo,
# no node, no network, ~0.1s.
project-type-check:
	testing/lint/project-type-check.sh

# Code gates. Skipped when the project has no cargo workspace yet, so that
# `make lint` is green on a freshly instantiated project that has no code.
#
# `gate.sh all` picks its own legs from the project type: a browser-ui project
# gets the `ui` leg (tsc/eslint/prettier) on top of the cargo legs. That
# decision is gate.sh's, through scripts/lib/project-type.sh, so there is one
# predicate and this Makefile does not re-derive it.
lint-code:
	@if [ -f Cargo.toml ]; then \
	  testing/lint/gate.sh all; \
	else \
	  echo "lint-code: SKIP - no Cargo.toml yet"; \
	fi

# grep-count-census / grep-count-census-selftest — the COUNTING-GREP FALLBACK
# gate. Refuses any counting grep (`grep -c`, `grep -vc`, `grep --count`,
# `pgrep -c`) in this project's own tracked shell whose FAILURE ARM appends
# output. `grep -c` PRINTS its count AND STILL EXITS 1 at zero, so
# `$(grep -c ... || echo 0)` captures the two-line string "0\n0" and every
# numeric compare after it dies with `[: 0\n0: integer expected`, falling
# through to whichever arm happens to be next. It bites ONLY when the count is
# genuinely zero at capture time, so it hides on every re-run and is invisible
# to review — one instance of it manufactured a false PRODUCT verdict out of a
# wait loop. A `lint` prerequisite because the rule had been written down in
# comments for months and enforced NOWHERE. Hermetic, no cargo, no box, ~1.6s.
grep-count-census:
	testing/lint/grep-count-census.sh

# grep-count-census-selftest — the controls for the gate above, both
# polarities. Not a leg of `lint`: acceptance for a gate, not a gate. On
# gates/selftest-roster.tsv (wiring=fast), same disposition and same reasons as
# doc-index-selftest above; found undispositioned by that task's census of this
# file (2026-09-13, template-doc-index-selftest-orphan). AGENT-DERIVED.
grep-count-census-selftest:
	testing/lint/grep-count-census-selftest.sh

# stderr-redirect-census / stderr-redirect-census-selftest — no `>&2` in this
# project's own tracked shell may FOLLOW a redirect that already moved fd2 in
# the same simple command (2026-09-11).
#
# Bash applies redirections LEFT TO RIGHT and `>&2` copies fd2's CURRENT
# target, so `tail -30 "$LOG" 2>/dev/null >&2` sends the log tail to
# /dev/null and `docker logs "$CT" 2>&1 >&2` sends BOTH streams to stdout,
# where a `$(…)` caller captures the diagnostic instead of seeing it. Three
# consecutive failing runs of one box gate printed their two section headers
# with nothing under either while the log they were tailing sat 58 lines long
# and readable — the diagnostic, not the assertion, is what quietly broke, so
# nothing goes red and the verdict is simply unexplainable. That cost a lane a
# whole remote dispatch, and the rule had been written down in comments in two
# gate files and enforced NOWHERE.
#
# The fix is always the same shape — put `>&2` first — which is what makes the
# class mechanical. A `lint` prerequisite for the same reason as
# grep-count-census: no cargo, no box, no network (~0.3s over ~1000 scripts).
stderr-redirect-census:
	python3 scripts/gate-stderr-redirect-census.py

# stderr-redirect-census-selftest — both polarities of a matcher that has to be
# loose enough to catch `2>&1 >&2` and tight enough to spare the three shapes
# that look exactly like it: a `2>` inside a balanced `$(…)`, a `>&2` inside a
# `>(…)`, and the CLOSING line of a two-line `$(…)`. All three are live shapes
# in the consumer this gate was written in, and all three are negative controls
# here. Twenty assertions behind an exact control count; every planted
# violation is assembled by `printf` so the suite's own leg (S), which runs the
# gate over the LIVE estate, cannot be tripped by this file. Hermetic
# `git init` fixtures, ~5s. Not a leg of `lint`: acceptance for a gate, not a
# gate.
stderr-redirect-census-selftest:
	testing/doctrine/stderr-redirect-census-selftest.sh

# comparator-census / comparator-census-selftest — THE LOCALE-PINNED
# COMPARATOR gate (2026-09-12). Refuses any order-critical comparator (`comm`,
# `sort -c`/`-C`, `uniq`) at command position in this project's own tracked
# shell that reads its collation from the ambient locale. uutils coreutils
# 0.8.x, which Ubuntu 26 ships as /usr/bin/{sort,comm,uniq}, sorts by LOCALE
# while `comm` checks order by BYTES and `uniq` compares ADJACENT lines, so a
# UTF-8 LANG makes `sort -u` delete a distinct line, makes `comm` refuse its
# input with a message nobody captured, and makes `sort | uniq -d` miss a real
# duplicate. Every one of those goes GREEN when it should be red. The gate and
# the predicate that fixes the sites (scripts/lib/sorted-compare.sh) came here
# together; testing/parity/comparator-census.sh's own header carries the three
# measurements. Hermetic, no cargo, no box, ~2s.
#
# NOT A `lint` PREREQUISITE YET, AND THAT IS A MEASUREMENT, NOT AN OVERSIGHT.
# On the tree this gate landed in it reports 8 unpinned sites across 5 files --
# testing/doctrine/history-anchors.sh, testing/lint/gate.sh (2),
# testing/fleet/selftest-fleet-exec-claim.sh, testing/preflight-selftest.sh and
# testing/profile-selftest.sh (3). FILE NAMES AND COUNTS, NOT `path:line`
# COORDINATES, DELIBERATELY: the census this comment sits beside makes exactly
# that argument about its own escape hatch -- a `path:line` row in a side file
# goes stale the moment anything above it moves -- and `make comparator-census`
# prints the live coordinates in one call anyway. Three of those files are porter-manifest
# paths whose consumer-side source ALREADY carries the fix as a restructure,
# so pulling them forward is a porter sweep's work and a hand edit here would
# be reverted by the next re-port; the other two are template-owned debt.
# Adding this target to `lint:` before they are fixed would redden step 7f of
# the merge window in every rendered instance on arrival, which is how a gate
# teaches people to route around it. The target exists and is runnable by
# name; the one-word `lint:` edit belongs to the commit that clears the eight.
comparator-census:
	testing/parity/comparator-census.sh

# comparator-census-selftest — the controls for the gate above AND for
# scripts/lib/sorted-compare.sh, both polarities, under EVERY coreutils
# userland the box has. Asserts INTERCHANGEABILITY (the same question, asked
# under each userland, comes back with the same answer) rather than "it passes
# here"; the census legs run against FIXTURE repos, never the live tree, with
# its own fixture per polarity. Hermetic, ~2s. Not a leg of `lint`: acceptance
# for a gate, not a gate. On gates/selftest-roster.tsv.
comparator-census-selftest:
	testing/parity/comparator-lc-all-c-selftest.sh
