# `mix ggen_igniter.sync`
[🔗](https://github.com/seanchatmangpt/ggen_igniter/blob/v26.9.8/lib/mix/tasks/ggen_igniter.sync.ex#L1)

CLI entrypoint: `mix ggen_igniter.sync --ontology path.ttl --query name=path.rq (repeatable) --template path.eex --out path.ex`.

Wires `Ontology.load!/1` -> `Query.run/2` (once per `--query`) -> `Render.render/2` ->
`Actuate.write_new_file!/2` in sequence.

Each `--query name=path.rq` result is bound in the EEx template under `name` as the
full list of result rows (string-keyed maps). Additionally, mirroring ggen's own
single-row-flattening convention (found live this session working with the Rust
`ggen` tool): a query whose result has EXACTLY ONE row also has that row's own
columns merged directly into the top-level bindings, atom-keyed, so a single-row
query like `spec` can be referenced as bare `module_name`/`package_name` instead of
`hd(spec)["module_name"]`. Later `--query` flags win on key collisions in the
flattened namespace -- pass queries in the order you want that tie-break to resolve.

## Multi-row fan-out (`--for-each NAME`)

Mirroring the real Rust ggen's `for_each:` frontmatter field
(`ggen-engine/src/template.rs`'s `Frontmatter.for_each`, `ggen-engine/src/sync.rs`'s
`ProjectionMode::Row` fan-out): pass `--for-each NAME` where `NAME` is one of the
named `--query` results to render the template once PER ROW of that query, each
render getting that row's own columns merged into the top-level bindings (same
atom-keyed flattening convention as the existing single-row case -- so row-local
fields are bare `module_name` etc., not `hd(modules)["module_name"]`), and to write
each rendering to its own output file.

Because there is no longer one static output path, `--out` is itself rendered
through `GgenIgniter.Render.render/2` using each row's bindings, so it must be an
EEx-renderable path template, e.g.:

    mix ggen_igniter.sync \
      --ontology test/fixtures/for_each_ontology.ttl \
      --query modules=test/fixtures/modules.rq \
      --for-each modules \
      --template test/fixtures/for_each_module.ex.eex \
      --out "lib/generated/<%= module_name %>.ex"

With three rows in the `modules` query result (`Multi.Alpha`, `Multi.Beta`,
`Multi.Gamma`), this writes three separate files:
`lib/generated/Multi.Alpha.ex`, `lib/generated/Multi.Beta.ex`,
`lib/generated/Multi.Gamma.ex`.

If `--for-each` is not given, behavior is unchanged: `--out` is a single static
path, written once, with the existing single-row-flatten convention.

## Engines

`--engine oxigraph` (default, since v26.8.27) runs every query in-process
against the loaded `%RDF.Graph{}` via a real, native oxigraph engine (a
Rustler NIF over `~/ggen/crates/ggen-graph-wasm`'s `OxigraphEngine`,
`GgenIgniter.Query.Oxigraph.run/2`) instead of the pure-Elixir `sparql` hex
package. This became the default because of a real, empirically confirmed
data-corruption bug in the previous default: `GgenIgniter.Query.run/2` (the
`sparql` hex package, v0.3.12) does not correctly honor `ORDER BY` -- a
join-shaped query mirroring the real gate fixtures (`?field ex:fieldOf
?entity ; ex:fieldOrder ?field_order . ?entity ex:entityStruct
?entity_struct .` with `ORDER BY ?field_order` over 10 rows) came back in
reverse order (`[9, 8, ..., 0]` instead of the requested ascending `[0, 1,
..., 9]`) -- see `GgenIgniter.Query`'s moduledoc for the full writeup. The
same query run through oxigraph (a real, independent, spec-conformant
SPARQL 1.1 engine) returned the correct ascending order. Silent row-order
reversal is a real corruption risk for any `--for-each` fan-out template
that assumes row order (e.g. numbering, positional joins), so the engine
that gets that right is now the one that runs unless `--engine` says
otherwise.

Two real, disclosed trade-offs from this default change, not silently
accepted:

  * **Row-value shape differs from `sparql`.** `--engine sparql`'s rows are
    plain unwrapped Elixir strings (`RDF.IRI.to_string/1` /
    `RDF.Literal.value/1`). `--engine oxigraph`'s rows are the real,
    unprocessed N-Triples-style term strings oxigraph itself returns --
    IRIs come back angle-bracket-wrapped (`<https://example.org/...>`) and
    literals come back quoted (and datatype/language-tagged when
    applicable, e.g. `"42"^^<http://www.w3.org/2001/XMLSchema#integer>`),
    not bare values. A template that renders a query column directly
    (`<%= module_name %>`) will see this real shape difference if it
    switches from `sparql` to `oxigraph`.
  * **A working Rust toolchain is required to compile this library at
    all**, regardless of which `--engine` a consumer ever actually invokes
    at runtime -- `lib/ggen_igniter/native/graph_nif.ex`'s `use Rustler`
    compiles `native/ggen_graph_nif` via a real `cargo` subprocess as part
    of that module's own compilation (confirmed by reading
    `Rustler.__using__/1`/`Rustler.Compiler.compile_crate/3` in the
    `rustler` 0.38 hex package -- there is no separate `mix compilers:`
    entry gating this; it runs whenever `graph_nif.ex` itself is
    compiled). This requirement already existed before this default
    changed (that loader module has been unconditionally part of this
    library's `lib/` since `--engine oxigraph` was first added as an
    opt-in engine) -- changing the *default* `--engine` string here is a
    runtime-only behavior change and adds no new compile-time requirement
    beyond what already existed. A consumer without `cargo` on `$PATH`
    already could not `mix compile` this library before this change,
    opt-in or not.

`--engine sparql` runs every query in-process against the loaded
`%RDF.Graph{}` via `GgenIgniter.Query.run/2` (the `sparql` hex package) --
still available, useful for a query shape known to depend on `sparql`
hex's specific (non-ORDER-BY) behavior, or to A/B a result against the new
default.

`--engine qlever` runs every query instead against a real, already-running
QLever SPARQL endpoint via `GgenIgniter.Query.Qlever.run/2` (`gno` + real
HTTP, no in-process SPARQL evaluation). `--ontology` is then still read as a
`%RDF.Graph{}` (via the same `Ontology.load!/1`), but only to look up the
`gnoa:Qlever`-typed store resource named by `--store-id` -- the query text
itself never touches this graph's data, it runs on the remote QLever store.
`--store-id` is required when `--engine qlever` is given.

## Comparison mode (`--engine oxigraph,sparql` / `--engine all`)

Per ADR-0008 (`docs/architecture/adr/0008-evidence-ranked-multi-engine-registry.md`):
`--engine` also accepts a comma-separated list (`--engine oxigraph,sparql`)
or the literal `--engine all`, parsed via `GgenIgniter.EngineRegistry.resolve/2`.
Resolving to more than one engine flips this task into **comparison mode**:
every named `--query` runs against EVERY resolved engine concurrently
(`GgenIgniter.EngineRegistry.run_all/4`), producing a
`GgenIgniter.EngineComparisonReport.t()` per named query. This is strictly
diagnostic-additive -- rendering/actuation still use only ONE **primary**
engine's rows (the first engine named, or `oxigraph` for `--engine all`),
so comparison mode changes no actuation, admission, receipt, or manifest
behavior at all.

`--engine all` expands to every engine `GgenIgniter.Engine.valid_names/0`
names whose preconditions are met: `qlever` is included only when
`--store-id` was given AND a real reachability probe against it succeeds
(mirroring `mix ggen_igniter.doctor`'s check 8) -- otherwise it is silently
excluded with a logged warning, never included-then-errored.

`--engine-report PATH` writes the report(s) to disk -- `.json` (via
`EngineComparisonReport.to_json/1`) or anything else (Markdown, via
`to_markdown/1`), chosen by `PATH`'s extension. Without `--engine-report`,
a compact summary (row count/elapsed time per engine, pairwise
row-set-agreement, per-engine errors) prints to stdout via
`Mix.shell().info/1` after the run.

## Example (default oxigraph engine)

    mix ggen_igniter.sync \
      --ontology test/fixtures/audit_trail_ontology.ttl \
      --query spec=test/fixtures/spec.rq \
      --template test/fixtures/extension.ex.eex \
      --out tmp_out/probe.ex

## Example (sparql engine)

    mix ggen_igniter.sync \
      --engine sparql \
      --ontology test/fixtures/audit_trail_ontology.ttl \
      --query spec=test/fixtures/spec.rq \
      --query sections=test/fixtures/sections.rq \
      --query entities=test/fixtures/entities.rq \
      --query fields=test/fixtures/fields.rq \
      --template test/fixtures/extension.ex.eex \
      --out tmp_out/probe.ex

## Example (qlever engine)

    mix ggen_igniter.sync \
      --engine qlever \
      --ontology config/gno/test/store.ttl \
      --store-id http://example.com/Qlever \
      --query spec=priv/ggen/some-pack/gates/010.rq \
      --template priv/ggen/some-pack/templates/out.ex.eex \
      --out lib/generated.ex

## Example (`--pack` convention)

Given `priv/ggen/audit-trail-pack/{ontology.ttl,gates/*.rq,templates/extension.ex.eex}`:

    mix ggen_igniter.sync --pack audit-trail-pack --out lib/generated.ex

`--ontology`/`--query`/`--template` are all still overridable explicitly; an
explicit flag always wins over the pack-derived default. `--pack-dir DIR` uses
`DIR` directly instead of resolving `priv/ggen/<pack>/`.

## Example (`--pack NAME:TEMPLATE` -- selecting one of several templates)

A pack with more than one file under `templates/` is normally ambiguous --
`--pack NAME` alone raises rather than guessing which one is "the"
template. Append `:TEMPLATE_STEM` (the template's filename up to its
first `.`) to `--pack` to select one explicitly, bypassing the ambiguity
error entirely for that pack:

    mix ggen_igniter.sync --pack ash-lifecycle-pack:resource --out lib/generated/resource.ex

Given `priv/ggen/ash-lifecycle-pack/templates/{resource.ex.eex,domain.ex.eex}`,
this selects `resource.ex.eex` specifically (`resource.ex.eex`'s stem is
`"resource"`); `--pack ash-lifecycle-pack:domain` selects `domain.ex.eex`
instead. Plain `--pack ash-lifecycle-pack` (no `:TEMPLATE_STEM`) keeps
today's behavior unchanged: auto-select when exactly one template exists,
raise the same "multiple templates found" error when there is more than
one. `--pack-dir` does not take this `:TEMPLATE_STEM` suffix -- pass
`--template` explicitly instead when using `--pack-dir` against a
multi-template pack.

# `run`

Reads --ontology/--query(N)/--template/--out/--engine/--store-id/--pack(-dir)
options and runs the pipeline.

## Template frontmatter (hygen/ggen parity)

Mirroring real hygen and real Rust ggen's own `---\n...\n---\n` header
convention (`ggen-engine/src/template.rs`'s `Frontmatter`, mirrored 1:1 by
`GgenIgniter.Frontmatter`): if `--template`'s file starts with a `---` fence
on its first line, everything between that fence and the closing `---` is
parsed as YAML frontmatter, and the remainder is the actual template body.
A template's own header supplies defaults for `to` (`--out`), `for_each`,
`unless_exists`, `skip_if` (literal-string form only), and `sparql` (named
queries given as inline query text, not file paths) -- so a self-contained
template can be rendered with just `--template`/`--ontology`, no repeated
`--out`/`--for-each`/`--query` flags, exactly like `hygen generate <name>`
needs no routing flags because the template's own header carries them.

Any explicit CLI flag always overrides the same-named frontmatter field.
`--query name=path.rq` and frontmatter `sparql:` inline queries can both be
present; an explicit `--query` with the same `name` overrides the
frontmatter's inline query text for that name. A template with no `---`
header behaves exactly as before this feature existed -- every routing
option must then come from the CLI/pack convention.

## Injection mode (`inject: true`)

Mirroring the real Rust ggen's own injection frontmatter fields
(`ggen-engine/src/template.rs`'s `Frontmatter.inject`/`before`/`after`/
`at_line`, mirrored 1:1 by `GgenIgniter.Frontmatter`): when a `mode: file`
template's frontmatter has `inject: true`, the rendered body is spliced
into the resolved output path's EXISTING content via
`GgenIgniter.Actuate.inject_content!/5`, instead of being written whole via
`GgenIgniter.Actuate.write_file!/3`. Exactly one of `before:`, `after:`, or
`at_line:` must be set as the anchor -- zero or more than one raises a
clear `ArgumentError` (an ambiguous or missing anchor is a template-authoring
error, never a silent pick, never a best-effort partial match):

  * `before: "marker"` / `after: "marker"` -- a literal string frontmatter
    value maps directly onto `inject_content!/5`'s own literal-marker
    "contains" match; splices the rendered body immediately before/after
    the single matched line.
  * `before:`/`after:` as a structured map (`pattern:`, `matcher:`,
    `case_sensitive:`, `scope:`, `occurrence:`, `trim:` -- the real
    `GgenIgniter.Frontmatter.MatchRule` shape) is converted into
    `inject_content!/5`'s `String.t() | Regex.t()` marker arg by this
    module's private `match_spec_to_marker!/2`:
      * `matcher: "contains"` (default) -- a plain string marker
        (`case_sensitive: true`, the default) or a case-insensitive
        unanchored `Regex` (`case_sensitive: false`).
      * `matcher: "exact"` -- a `^...$`-anchored `Regex` (escaped pattern),
        with `\s*` padding on both sides instead of bare anchors when
        `trim: true`.
      * `matcher: "regex"` -- the pattern string compiled directly as a
        `Regex` (never escaped -- it IS a regex), `case_sensitive: false`
        adding the `i` flag.
    `scope: "file"`, any `occurrence:` other than the default `"first"`,
    and `trim: true` paired with a `matcher` other than `"exact"` have no
    equivalent in `inject_content!/5`'s real anchor-resolution behavior
    (always-exactly-one-line, no whole-file mode, no
    pick-a-specific-occurrence logic) -- setting one of those raises a
    clear error naming the exact unsupported combination, rather than
    silently proceeding as if it had been honored.
  * `at_line: N` -- an anchor-free alternative: splices the rendered body
    at the 1-based line number `N` (`marker` is not used; `before`/`after`
    must both be unset).

`--dry-run` previews an injection the same honest way it previews a write:
the real anchor-resolution and idempotency check both run for real (via
`inject_content!/5`'s own `:dry_run` option), nothing is written, and the
notice line reads `"planned: inject #{out_path}"` (or `"planned: skip
#{out_path} (unchanged)"` when the content is already spliced in at that
exact position). A real, non-dry-run injection reports `"injected
#{out_path}"` on the first run and `"unchanged (skipped, identical
content): #{out_path}"` on an idempotent re-run -- distinct from
`write_file!/3`'s `"wrote #{out_path}"`, since the two are different
actuation paths.

Injection always requires the target file to ALREADY exist (mirroring
`inject_content!/5`'s own fail-closed gate) -- it is not a substitute for
file creation; a first-run template should not set `inject: true` against a
path nothing has created yet.

    mix ggen_igniter.sync \
      --ontology test/fixtures/audit_trail_ontology.ttl \
      --query spec=test/fixtures/spec.rq \
      --template test/fixtures/inject_before_marker.ex.eex \
      --out lib/existing_module.ex

## Execution mode (`mode: eval`)

`mode:` (frontmatter, or `--mode` on the CLI -- explicit CLI wins) selects
what happens to the rendered template body: `mode: file` (the default,
unchanged from every example above) writes it to disk via the write-safety
guards described elsewhere in this doc. `mode: eval` instead treats the
rendered body as real Elixir source and evaluates it in-process via
`GgenIgniter.Actuate.eval_code!/2`, using the exact same bindings the
template body renders with (so eval'd code can reference `module_name`,
a single-row query's flattened columns, a `--for-each` row's columns, etc.,
exactly like the template body itself can) -- nothing is ever written to
disk under this mode, so `--out`/`to:` is not required, and
`--unless-exists`/`--skip-if` are not applicable and are ignored.

    mix ggen_igniter.sync \
      --ontology test/fixtures/audit_trail_ontology.ttl \
      --query spec=test/fixtures/spec.rq \
      --template test/fixtures/eval_mode_module.exs.eex \
      --mode eval

With `--for-each`, the eval'd body runs once per row (same fan-out as file
mode, just evaluated instead of written). `--dry-run` shows a `"planned:
evaluate ..."` notice and does not actually evaluate anything.

This is a deliberate, disclosed arbitrary-code-execution capability:
ontology/RDF-driven query results become literally-executed Elixir code
under `mode: eval`. Threading an eval result into a *later* query/render/
write stage is not implemented in this pass -- each named query's results
and each eval's return value are independent of one another within a single
`sync` run.

## `sh_before:`/`sh_after:` shell hooks (frontmatter-only, gated by `--allow-sh`)

Mirroring the real Rust ggen's own `Frontmatter.sh_before`/`sh_after`
fields (`GgenIgniter.Frontmatter`'s own moduledoc, field-by-field
provenance): a template's frontmatter may declare a real shell command to
run before (`sh_before:`) and/or after (`sh_after:`) that row's real
`write_file!/3`/`inject_content!/5` call, executed via
`GgenIgniter.ShellHook.run/3` (`System.cmd("sh", ["-c", cmd], cd:
--manifest-dir/File.cwd!(), stderr_to_stdout: true)`, real timeout,
default 60s).

**`--allow-sh` is required** (default `false`) whenever ANY resolved
template's frontmatter sets `sh_before:`/`sh_after:` -- absent it, the
WHOLE run refuses before any actuation happens at all (fail-closed,
matching `--on-stale refuse`'s own default posture), naming the exact
template and field(s) that triggered the refusal. This refusal is checked
BEFORE `run_via_reactor/3`'s own Reactor dispatch AND before
`run_pipeline!/3`'s own inline actuation loop -- both are genuinely
separate call paths (see the `## Reactor dispatch` /
`GgenIgniter.Reactors.ReconcileReactor` sections), and
`ReconcileReactor.run/1` independently re-checks the same
`allow_sh`/`sh_before`/`sh_after` combination for ITS OWN direct callers
(not only calls arriving through this task) -- see that module's
moduledoc.

**DISCLOSED, INTENTIONAL LIMITATION** (mirrors ADR-0006's disclosure
style for `inject_content!/5`'s own scope, and the v26.8.30 CHANGELOG's
"`:run_queries` concurrency: investigated, NOT changed" entry): a
`sh_before:`/`sh_after:` command's real side effects are **NOT**
integrated into `GgenIgniter.PendingActuation`'s `operation()` type,
**NOT** inspected by `:admit`'s guards (duplicate-path refusal,
path-escape refusal, unowned-delete refusal), and **NOT** tracked by
`undo/4`'s compensation/revert machinery -- a template author declaring
`sh_before:`/`sh_after:` is trusted the same way this repo already
trusts a frontmatter `to:` path (an existing, accepted trust boundary,
not a new one). `--allow-sh` is the one new, deliberately small
admission-adjacent check this pass adds to mitigate the highest-severity
real finding here (a destructive command bypassing admission entirely) --
it is a single explicit opt-in flag, not a new operation-type/IR change.

**Failure semantics differ from every other row-level failure in this
module.** A nonzero exit or a real timeout from `sh_before:`/`sh_after:`
does **NOT** abort the whole run -- this is a genuinely new
failure-tolerance pattern for `sync.ex` (today, a raised exception from
any other row aborts the entire run). It produces a new per-row outcome
atom instead, extending the existing `:written`/`:injected`/`:unchanged`/
`:skipped_exists`/`:skipped_match` vocabulary:

  * `sh_before:` fails (nonzero exit or timeout) -- the row's real
    `write_file!/3`/`inject_content!/5` call is SKIPPED entirely (treated
    as a failed precondition), outcome `:sh_before_failed`.
  * `sh_after:` fails AFTER a real `:written`/`:injected` outcome -- the
    write/inject already genuinely happened and is NOT reverted (no
    compensation exists for this, per the disclosed limitation above);
    outcome `:sh_after_failed`.

See `outcome_summary_suffix/2`/`summary_bucket/1` for how these two new
atoms are counted and reported in the final run summary, alongside every
other outcome.

`--dry-run` previews a shell hook exactly like every other actuation
decision in this module: `"planned: run sh_before: <cmd>"` /
`"planned: run sh_after: <cmd>"` is printed, and `GgenIgniter.ShellHook.run/3`
is never called at all (the real subprocess never starts under
`--dry-run`, matching this whole module's "zero real side effects" dry-run
contract).

Every real `sh_before:`/`sh_after:` invocation (success, nonzero exit, or
timeout) is appended to `GgenIgniter.Receipt.commands` -- see that
module's moduledoc for the entry shape. `sync.ex`'s inline pipeline does
not otherwise construct a `GgenIgniter.Receipt` at all (verified: no
`Receipt.new/1`/`Receipt.append!/2` call existed anywhere in this file
before this feature); a minimal receipt (`standing: :alive` -- this
module's inline pipeline has no compensation/verification step of its own
to fail, so `:alive` here describes "an attempt was made and files were
actuated via the normal write-safety guards," not "every shell hook
succeeded" -- any hook failure is named explicitly in `reason`/`commands`
instead) is constructed and appended ONLY for a real (non-`--dry-run`) run
that actually declared `sh_before:`/`sh_after:`, so a run with no shell
hooks at all produces no new receipt traffic.

## Reconciliation manifest (stale-output detection, `--on-stale`)

Every real, disk-written `mode: file` output (whole-file `write_file!/3`
writes; NOT `inject: true` splices, NOT `mode: eval`) is recorded in a
RECONCILIATION MANIFEST at `<manifest_dir>/.ggen_igniter/manifest.json`
(`manifest_dir` defaults to `File.cwd!()` -- the consumer project's own
directory, i.e. wherever `mix ggen_igniter.sync` is actually invoked from;
override with `--manifest-dir DIR`), keyed by the `(--template,
--out/to:)` "recipe" pair (`GgenIgniter.Manifest.recipe_key/2` -- see that
module's moduledoc for the full, grounded reasoning for why THIS pair, and
not ontology path or pack name alone, is the real reconciliation identity).

Before writing anything, the manifest's EXISTING entry for this run's
recipe (if any) is read; this run's own real output-path set is computed
(every row's rendered `--out`, whether from `--for-each` fan-out or the
single static case); `stale = old_paths - new_paths` -- paths a PRIOR run
of this exact recipe wrote that this run does NOT write (the mechanical
signature of a rename or removal upstream in the ontology).

`--on-stale` (default **`refuse`** -- the safest default; silent orphaning
is never the default, and silent deletion is never the default either)
decides what happens when `stale` is non-empty:

  * `refuse` (default) -- if `stale` is non-empty, raises a clear
    `ArgumentError` naming every exact stale path, BEFORE writing anything
    at all this run (not even the non-stale outputs) -- complete
    reconciliation or a refusal before any partial actuation, never a
    silent orphan. Fix by re-running with `--on-stale prune` or
    `--on-stale preserve`.
  * `prune` -- proceeds with this run's writes, then really deletes
    (`File.rm/1`) every stale path, reporting each real deletion
    (`"pruned: PATH"`, or `"pruned (already absent): PATH"` if it was
    already gone).
  * `preserve` -- proceeds with this run's writes, leaves every stale path
    untouched on disk, and prints a clear warning naming each one every
    time (they are also dropped from the manifest's tracked output set for
    this recipe -- this pack no longer claims ownership of a path it
    isn't producing this run).

The manifest is only ever persisted AFTER this run's own writes (and, for
`prune`, the real deletions) fully succeed -- a raised exception mid-run
(a failed write, a `refuse` refusal) never touches the manifest file, so it
always reflects the last KNOWN-GOOD run, never a partial one. A run whose
real output-path-plus-content-hash set is IDENTICAL to what the manifest
already recorded (a true no-op re-run) does not rewrite the manifest file
at all -- not even its timestamp.

`--dry-run` previews reconciliation the same honest way it previews every
other actuation: a `refuse`-triggering `stale` set still raises (a dry run
is a real preview of what WOULD happen, and "this run would be refused" is
exactly that); `prune`/`preserve` print `"planned: prune PATH"` /
`"planned: preserve PATH"` lines instead of touching disk; the manifest
file itself is never written under `--dry-run`.

    mix ggen_igniter.sync --pack-dir priv/ggen/ash-lifecycle-pack \
      --ontology priv/ggen/ash-lifecycle-pack/ontology.ttl \
      --template priv/ggen/ash-lifecycle-pack/templates/resource.ex.eex \
      --for-each resource \
      --out "lib/support_desk/support/<%= String.downcase(resource_name) %>.ex" \
      --on-stale prune

## `--verify-cwd DIR` (Reactor pipeline only, `use_reactor: true`)

When the opt-in Reactor pipeline is active (see "Reactor dispatch" below),
its terminal `:verify` step runs a REAL `mix compile --warnings-as-errors`
subprocess to confirm the just-actuated project still builds
(`GgenIgniter.Reactors.ReconcileReactor`'s `:verify` step). That subprocess
needs a real Mix project directory (one containing `mix.exs`) to `cd:`
into. By default it uses `--manifest-dir` (falling back to `File.cwd!()`)
for this -- correct whenever the reconciliation manifest and the actual
Mix project live in the same directory, which is the common case.

`--verify-cwd DIR` overrides just this one directory, independently of
`--manifest-dir`, for the one real scenario where the two differ: writing
actuated output into an ISOLATED directory (e.g. a throwaway tmp dir used
as `--manifest-dir` so the reconciliation manifest and path-escape
boundary don't touch the real project at all) while still wanting
`:verify` to run its `mix compile` against the REAL project root. Concrete
worked example -- generating into an isolated tmp dir, verifying against
this repo itself:

    mkdir -p /tmp/ggen_verify_cwd_demo
    mix ggen_igniter.sync \
      --pack-dir priv/ggen/adr-index-pack \
      --out /tmp/ggen_verify_cwd_demo/out.md \
      --manifest-dir /tmp/ggen_verify_cwd_demo \
      --verify-cwd /Users/sac/ggen_igniter \
      --engine oxigraph

Without `--verify-cwd` in this exact scenario (`--manifest-dir` pointing
outside any Mix project), `:verify`'s `mix compile` subprocess runs `cd:`
into that same non-project tmp dir, Mix itself raises `** (Mix) Could not
find a Mix.Project...`, and this task's `:verify`-failure path
(`maybe_add_verify_cwd_hint/3` in `ReconcileReactor`) detects that exact
Mix error text and a nil `--verify-cwd` and prepends a concrete pointer at
this flag to the raised `RuntimeError`, rather than surfacing the bare Mix
crash text alone.

## Controller delegation (opt-in, thin-adapter mode)

When a real `GgenIgniter.Controller` `GenServer` is already running,
registered under the name `GgenIgniter.Controller` (`Process.whereis/1` --
the same registration idiom this codebase's own `GgenIgniter.Engine.Qlever`
already uses for `GgenIgniter.Finch`), THIS run's reconciliation work is
delegated to it (`GgenIgniter.Controller.reconcile/3`, wrapping the shared
`GgenIgniter.Reconcile.run/1` pipeline) instead of running the pipeline
inline -- giving this one invocation access to the controller's real,
in-process reconciliation history (`reconciliation_count`, surfaced in the
notice line below) instead of a fresh, state-free OS process. When no such
process is registered (the common case today -- the controller is
opt-in and off by default), behavior is EXACTLY the pre-existing inline
pipeline, unchanged.

Delegation only ever applies to a call within `GgenIgniter.Reconcile.run/1`'s
own deliberately bounded scope (see that module's moduledoc): the resolved
template must have NO frontmatter header at all, and `--for-each` must not
be requested (by flag or by frontmatter -- moot here since frontmatter is
required absent). Any call using frontmatter, `--for-each`, `inject: true`,
or `mode: eval`'s frontmatter defaults, or a `Controller.reconcile/3` whose
real arity/behavior no longer matches what this module was written against
(`function_exported?/3`, checked every call -- a live defensive guard, not a
one-time check, since this integration point was wired against a
concurrently-developed module), transparently falls back to the exact same
inline pipeline used when no controller is running at all -- never a silent
behavior change for a feature the controller's bounded pipeline does not
yet implement.

One real, disclosed trade-off of controller-mode delegation specifically:
the RECONCILIATION MANIFEST (`--on-stale`/`manifest.json`, described above)
is a property of the inline pipeline's own bookkeeping and is NOT consulted
or updated on the delegated path -- the controller's own in-process state
(keyed on `{template_path, --out}`) is the reconciliation record for that
call instead. This is intentional: controller mode exists precisely to
replace disk-based manifest tracking with in-process tracking for whichever
recipes it is enabled for, not to duplicate both.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
