kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

Build coordinator — internal module notes

Historical implementation notes. The durable feature spec is ../BUILD_COORDINATOR.md, which specifies what the content-addressed build coordinator does (the trace model, the storage state machine, the resolution algorithm, the recipe protocol). This doc records how the implementation is carved into modules under src/build/: for each module, what it is responsible for, which other modules it depends on, and a pseudocode sketch dense enough to prove the interfaces in the src/build/*.h spike are sufficient to implement every module in parallel. Where a sketch exposed a gap, it is called out inline and collected in Interface findings.

The public surface is <kit/build_coord.h> (coordinator + recipe-side client) plus a kit build driver command, gated by KIT_BUILD_ENABLED, mirroring the <kit/cas.h> + kit cas precedent. Everything below is internal to src/build/ except the composition root src/api/build.c and the driver glue.

Layering and the dependency graph

The build layer sits entirely on the public kit surface — the content store (<kit/cas.h>), digests (<kit/hash.h>), the canonical writer/heap/slice core (<kit/core.h>), and signed packages (<kit/package.h>) — and adds exactly one thing the CAS lacks: a mutable per-target trace index plus the resolution algorithm that drives it. Every id it computes is a plain BLAKE2b-256 of canonical bytes, which is a CAS blob id (kit_blob_info().id), so need-overlay maps, argv vectors, and deepset closure nodes content-address themselves through the ordinary CAS as ordinary blobs (self-verifying content). Trace bodies are content-addressed too, but live in build/trace/ rather than cas/, because a trace is a claim (not self-verifying) — the criterion that sorts the two stores is self-verifiability, not content-addressedness.

Internal modules form a strict DAG (no cycles — verified against the #include sets in the spike). Arrows point "depends on":

                         ┌─────────────────────────────────────────┐
  L6  composition        │  src/api/build.c   (KitStatus, ctx->diag)│
                         └───────────────┬─────────────────────────┘
                                         │ uses everything below
  L5  recipe execution        runner ───────────────► bundle (trace sharing)
                                 │  │  │                  │
  L4  algorithm              resolve │  │                 │
                                 │   │  │                 │
  L3  coordinator state         coord ◄──────────────────┘
                              ┌──┴───┬────────┬─────────┐
  L2  I/O + remote          store  remote   (client ── recipe side, L1-only)
                              │
  L1  pure value/byte    ┌── trace ──┐   cfg    defn   protocol
      logic (no I/O)     │           │
  L0  foundation         └──────── build.h ───────────────────────┘
                          (constants, ids, result convention, diag)
Layer Modules Property
L0 foundation build constants, BUILD_OK/ERR, magics, build_id_eq, build_target_key, build_diagf. No internal deps.
L1 pure logic cfg, defn, trace, protocol, client byte/value transforms only: emit / parse / hash / encode / decode. No I/O, no CAS, no coordinator. Unit-testable in isolation.
L2 store + remote store, remote the on-disk state machine and the untrusted fetch rung. Touch the filesystem (via host vtables) and the CAS; interpret no trace semantics beyond trace.
L3 coordinator coord the KitBuildCoordinator context: process-lifetime memos, the jobs semaphore, per-target futures, base-input probes.
L4 algorithm resolve the 3-phase resolve() + build chain + materialization ladder + leafset union/refresh.
L5 execution runner, bundle Phase 3 (spawn + service a recipe, record both traces) and signed trace import/export.
L6 composition src/api/build.c maps internal intKitStatus, routes detail through ctx->diag, owns section-buffer sizing. The public functions in <kit/build_coord.h> live here.

client is L1 by dependency (it only needs protocol + a transport) but is a peer process role, not part of the coordinator. It backs the public kit_build_client_* recipe-side API.

Result/error convention (inherited from src/dist). Internal modules return int (BUILD_OK / BUILD_ERR); parse/emit modules additionally take a char* err, size_t errcap sink. Only src/api/build.c knows KitStatus and ctx->diag. All ids are lowercase hex on the wire and in every canonical body.

One id invariant the whole layer leans on: for any canonical byte string b, kit_blob_info(&info, b, len); info.id equals the BLAKE2b-256 that names b as a CAS blob. So "compute the overlay-id/argv-id/trace-id" and "store the blob and read its id back" agree by construction — a trace can reference an overlay map by id and later recover the exact bytes with kit_cas_get_blob. Effective config maps are hashed for resolution identity but are not trace content. Pure index keys that are never stored as content (target-key, glob-result-hash) use the same hash over a domain-separated buffer.


L0 — build (foundation)

Role. Shared vocabulary for every other module: hash length, the BUILD_OK/ERR convention, the canonical magics (kit-build-shallow 1, …-deep 1, …-record 1, …-config 1, …-argv 1, …-traces 1, kit-build 1), the fixed row capacities, byte-exact id equality, the per-target index-key derivation, and the diag shim.

Depends on. <kit/cas.h> (hash length), <kit/hash.h>, <kit/core.h>. No internal deps.

Pseudocode.

int build_target_key(KitSlice name, uint8_t out[32]):
  // target-key = BLAKE2b("kit build target v1" || name); a pure index key,
  // never stored, so hash a domain-separated scratch buffer directly.
  buf = stack[ sizeof(DOMAIN)-1 + name.len ]      // bounded by BUILD_TARGET_MAX
  memcpy(buf, BUILD_TARGET_KEY_DOMAIN, ...); append name
  KitBlobInfo bi; kit_blob_info(&bi, buf, len); memcpy(out, bi.id, 32)
  return BUILD_OK

void build_diagf(ctx, fmt, ...):   // mirror cas_diagf
  if !ctx || !ctx->diag: return
  va_start; format into a bounded stack buffer; ctx->diag(ctx->diag_user, msg); va_end

build_id_eq is the existing inline. No interface gap.


L1 — cfg (propagated config + local argv)

Role. Canonical propagated-config maps and local argv vectors. Effective propagated maps are hashed for the in-memory resolution identity but traces keep only consumed observations. Explicit need overlay maps and local argv vectors are serialized by value as CAS blobs so an imported shallow trace can replay a need exactly. Pure value logic: init / set / get / overlay / canonical emit / parse / id. Entries hang off caller storage (the DistTree growable-buffer pattern, no VLAs).

Depends on. build (L0), <kit/core.h> (KitWriter, KitHeap), <kit/cas.h> (kit_blob_info), <kit/build_coord.h> (KitBuildKV overlay pairs).

Pseudocode.

int build_config_set(cfg, key, val):
  if key.len > BUILD_KEY_MAX-1 || val.len > BUILD_VAL_MAX-1: return BUILD_ERR
  i = lower_bound(cfg.entries, key)               // keep sorted, dedup-on-set
  if i found and equal-key: overwrite value; else insert (grow if n==cap → ERR if no room)
  return BUILD_OK

int build_config_overlay(base, overrides[], n, out):  // the `need` overlay
  copy base into out (already canonical/sorted)
  for kv in overrides: build_config_set(out, kv.key, kv.value)   // re-sorts/dedups
  return BUILD_OK

int build_config_emit(cfg, w):                    // canonical, byte-stable
  write "kit-build-config 1\n"
  for e in sorted(cfg.entries): write e.key, ' ', e.value, '\n'   // sort enforced here
  return kit_writer_status(w)

int build_config_id(heap, cfg, out[32]):
  kit_writer_mem(heap, &w); build_config_emit(cfg, w)
  bytes = kit_writer_mem_bytes(w, &len); KitBlobInfo bi; kit_blob_info(&bi, bytes, len)
  memcpy(out, bi.id, 32); kit_writer_close(w); return BUILD_OK

int build_config_parse(data, len, out, err, errcap):
  require first line == BUILD_CONFIG_MAGIC else parse-error (→ caller treats absent)
  for each "key value" line: enforce strictly-increasing key, no dups → build_config_set
  // non-canonical ordering / unknown shape / dup = BUILD_ERR

build_argv_* mirror this but order is significant (positional vector, magic kit-build-argv 1, one arg per line, no sort). The empty argv has a fixed argv-id = hash of just the magic line — the default when a request supplies none. No interface gap.


L1 — defn (build definition)

Role. Parse the build-definition file (kit-build 1) mapping each target name → one recipe path, and expose a by-name lookup. The coordinator reads it to resolve a target. The only definition-derived input of a target is its resolved recipe-id, refreshed by recomputing it through the live definition (build_defn_find → recipe path → hash) — there is no whole-definition source leaf, so editing an unrelated stanza does not bust unrelated targets, and a repoint to a same-content recipe is correctly a no-op. The definition carries no argv — local argv comes entirely from the request.

Depends on. build (L0), <kit/core.h>. Pure parse + lookup, no I/O.

Pseudocode.

int build_defn_parse(data, len, out, err, errcap):
  out.bytes = data; out.len = len            // retained so coord can hash file as a source leaf
  require first line == "kit-build 1"
  loop: expect "[target <name>]" then "recipe <path>"
        validate name (BUILD_TARGET_MAX) and path (build path-valid), append stanza
        enforce sorted-unique target names (canonical) else BUILD_ERR
  return BUILD_OK

const BuildTargetDefn* build_defn_find(defn, name):
  binary-search defn.targets by name; return match or NULL

Interface note. out.bytes aliases the caller's buffer; the coordinator must keep the definition bytes alive for the coordinator's lifetime (it does: coord.defn_bytes). The recipe-id is not here — coord hashes the recipe file named by recipe_path. No interface gap.


L1 — trace (trace bodies + target record)

Role. The immutable, content-addressed heart of the cache: emit / parse / id for the shallow trace, the deep trace, the deepset closure node, and the mutable target record. Pure byte logic — strict, byte-stable INI-style text; any non-canonical input (unknown key, dup row, mis-ordering) is a parse error, and a parse error is treated as absent, never as a match (fail safe). Sections hang off caller-provided growable buffers.

Depends on. build (L0), <kit/core.h>. No I/O — store persists/loads these, resolve matches against them.

Pseudocode.

int build_shallow_emit(t, w, err, errcap):
  write "kit-build-shallow 1\n"
  write "target ", t.target, '\n'; "recipe ", hex(t.recipe), '\n'
        "output ", hex(t.output), '\n'; "argv ", hex(t.argv), '\n'
  write "[config]\n"; for k in sort(configs): write observation(k), '\n'
  write "[source]\n"; for s in sort_by_path(sources): require path-valid; write s.path, ' ', s.absent?"-":hex(s.blob), '\n'
  write "[glob]\n";   for g in sort_by_pattern(globs): write g.pattern, ' ', hex(g.result_hash), '\n'
  write "[dep]\n";    for d in sort_by(name,overlay,argv): write d.name,' ',hex(d.overlay_id),' ',hex(d.argv_id),' ',hex(d.output_tree),'\n'
  return kit_writer_status(w)

int build_deep_emit(t, w, err, errcap):           // no [dep], no [config], no inline closure
  write "kit-build-deep 1\n"; target/recipe/output; "root-config ", hex(t.root_config)
  "argv ", hex(t.argv); "deepset ", hex(t.deepset)   // one pointer to the closure DAG

int build_deepset_emit(t, w, err, errcap):        // one node of the closure DAG
  write "kit-build-deepset 1\n"; "target ", t.target; "recipe ", hex(t.recipe)
  "[source]\n"; for s in sort_by_path(sources): write s.path, ' ', s.absent ? "-" : hex(s.blob), '\n'
  "[glob]\n";   for g in sort_by_pattern(globs): write g.pattern, ' ', hex(g.result_hash), '\n'
  "[child]\n";  for c in sort(children): write hex(c), '\n'    // direct deps' deep-set-ids

void build_trace_id(body, len, out[32]): KitBlobInfo bi; kit_blob_info(&bi, body, len); memcpy(out, bi.id, 32)
// build_deepset_id is the same hash; a deepset is stored as an ordinary CAS blob, so deep-set-id == blob-id.

int build_record_prepend(rec, row):               // MRU, per-kind cap
  drop any existing row equal in (kind, trace_id)
  shift rows down; rec.rows[0] = row
  truncate the run of rows with row.kind to KIT_BUILD_RECORD_CAP (other kind untouched)

Parsers are the strict inverse; build_*_parse reject non-canonical bytes with BUILD_ERR. No interface gap — emit takes KitWriter, parse fills caller buffers, exactly the dist_tree/dist_manifest shape.


L1 — protocol (recipe wire format)

Role. The recipe command set (config-get, source, glob, need) and its length-prefixed framing, as pure encode/decode over a KitBuildConn. No I/O, no transport: the coordinator's runner drives the server side, client drives the recipe side, both calling these codecs. Every request returns a value and logs a dependency; an input read but not requested is invisible to the cache.

Depends on. build (L0), <kit/core.h>, <kit/build_coord.h> (KitBuildKV, KitBuildConn).

Wire format. This is the source of truth for build_proto_* and the protocol goldens. The KitBuildTransport vtable hands codecs a complete frame body. For byte-stream transports, the transport wraps each body as:

u32le body_len
u8[body_len] body

body_len must be <= BUILD_FRAME_MAX (65536). In-process transports may pass the body directly, but must enforce the same cap. The body format is:

Request bodies:

Command Body
CONFIG_GET u8 cmd, str key
SOURCE u8 cmd, str workspace_path
GLOB u8 cmd, str pattern
NEED u8 cmd, str target, u16 noverrides, {str key, str value}*, u16 argc, {str arg}*
NEED_SUBMIT same as NEED
NEED_AWAIT u8 cmd, u64 token

Response bodies begin with u8 status. ERROR is valid for any command and has the common body u8 ERROR, u16 kit_status, str message; clients return the KitStatus value and may surface message through diagnostics.

Non-error response bodies:

For command Status Body
CONFIG_GET OK u8 OK, str value
CONFIG_GET UNSET u8 UNSET
SOURCE OK u8 OK, u8[32] blob_id, str realpath
SOURCE ABSENT u8 ABSENT
GLOB OK u8 OK, u16 npaths, {str path}* (one batch)
GLOB GLOB_END u8 GLOB_END
NEED OK u8 OK, u8[32] output_tree, str materialized_path
NEED_SUBMIT OK u8 OK, u64 token
NEED_AWAIT OK u8 OK, u8[32] output_tree, str materialized_path

Glob is the only multi-frame response: zero or more GLOB/OK path-batch frames, terminated by exactly one GLOB/GLOB_END. Empty globs send only GLOB_END. Every other command is exactly one request body and one response body.

Pseudocode.

int build_proto_encode_req(req, buf, cap, n):
  cursor c over buf (cap ≥ BUILD_FRAME_MAX)
  put u8 req.cmd
  if cmd==CONFIG_GET/SOURCE/GLOB: put_slice(req.arg)
  if cmd==NEED or cmd==NEED_SUBMIT:
     put_slice(req.arg)   // target
     put u16 noverrides; for kv: put_slice(kv.key), put_slice(kv.value)
     put u16 argc;       for a:  put_slice(a)
  if cmd==NEED_AWAIT:
     put u64 req.token
  *n = c.len; return (c overran) ? BUILD_ERR : BUILD_OK

int build_proto_decode_resp(buf, len, cmd, out, glob_cb, glob_user):
  out.status = get_u8
  if status==ERROR: out.error_status=get_u16; out.text=get_slice; return OK
  switch cmd:
    CONFIG_GET:  if status==UNSET return OK(present=0); else out.text = get_slice
    SOURCE:      if status==ABSENT return OK(present=0); get_bytes(out.id,32); out.text = get_slice
    NEED, AWAIT: get_bytes(out.id,32); out.text = get_slice            // tree-id + path
    NEED_SUBMIT: out.token = get_u64                                   // coordinator-assigned
    GLOB:        u32 m = get_u32; repeat m: p = get_slice; if glob_cb(user, p) != 0 break   // streamed
  return malformed ? BUILD_ERR : BUILD_OK

build_proto_decode_req fills caller-provided ovr_storage / argv_storage for a need/need-submit, and reads req.token for need-await. Frames are bounded by BUILD_FRAME_MAX.

Required tests before dependents start.

No interface gap.


L1 — client (recipe-side, src/build/client.c)

Role. Implements the public kit_build_client_* API a recipe links (or reaches through the in-process transport / a kit helper subcommand). Each call encodes a request with protocol, writes it over the transport, reads the response, decodes it. It is the mirror of runner's service loop and shares zero coordinator state.

Depends on. protocol (L1), <kit/build_coord.h> (KitBuildTransport, KitBuildClient, KitBuildResult), <kit/core.h>.

Pseudocode.

struct KitBuildClient { ctx; const KitBuildTransport* t; KitBuildConn* conn; uint8_t frame[BUILD_FRAME_MAX]; }

KitStatus kit_build_client_open(ctx, transport, out):
  c = alloc; c.t = transport; endpoint = getenv(KIT_BUILD_ENV_SOCK)
  transport->dial(transport->user, endpoint, &c.conn); *out = c; return KIT_OK

KitStatus kit_build_client_source(c, path, blob[32], realpath):
  build_proto_encode_req({SOURCE, path}, c.frame, ...); transport->write_frame(conn, frame, n)
  transport->read_frame(conn, frame, cap, &n); build_proto_decode_resp(frame, n, SOURCE, &resp, 0,0)
  if resp.status != OK: return KIT_<err>; memcpy(blob, resp.id, 32); *realpath = resp.text; return KIT_OK

KitStatus kit_build_client_need(c, req, out):     // req carries overlay + argv
  encode NEED(req.target, req.config, req.argv); round-trip; decode → out.output_tree, out.path

Interface note (gap → see findings #1). client is a real translation unit with no internal header in the spike. That's fine (its interface is <kit/build_coord.h>), but the parallel-work plan must assign it an owner. Listed here so it isn't lost.


L2 — store (on-disk state machine)

Role. The build store under <store>/build/ and its crash-safe rules: immutable content objects (trace/, materialized cache/) written tmp→fsync→atomic- rename; the one mutable, ordering-sensitive object (the target record/) updated by read-modify-write into tmp then atomic rename; recipe sandboxes under tmp/. Owns content-keyed path layout (<pp>/<id>), atomicity, and the record RMW. It interprets no trace semantics beyond calling trace's emit/parse.

Depends on. build (L0), trace (L1, for BuildTargetRecord and record emit/parse), <kit/cas.h> (the shared CAS, blob/tree ops), <kit/build_coord.h> (KitBuildStoreIo atomicity extras, KitCasHost for file_io).

Pseudocode.

int build_store_put_trace(s, body, len, out_id[32]):    // idempotent content write
  build_trace_id(body, len, out_id)
  build_store_trace_path(s, out_id, final, …)
  if exists(final): return BUILD_OK                       // re-derivation is a no-op
  io->make_temp_dir(build/tmp, tmpdir); tmp = tmpdir/"t"
  cas_host->file_io->open_writer(tmp, &w); w.write(body,len); w.close()
  io->sync_path(tmp); io->rename(tmp, final); io->sync_path(parent(final))
  return BUILD_OK

int build_store_record_update(s, key[32], name, kind, trace_id[32]):
  L = io->lock ? io->lock(key) : NULL                     // optional advisory lock
  build_store_record_load(s, key, name, &rec)             // missing/garbage → empty rec (fail safe)
  build_record_prepend(&rec, {kind, trace_id})            // MRU + cap, from trace.h
  kit_writer_mem(heap,&w); build_record_emit(&rec,w,…); bytes = mem_bytes(w,&n)
  write bytes to build/tmp/rec-XXXX; sync; build_store_target_path(s,key,final); io->rename(tmp, final); sync parent
  if L: io->unlock(L); return BUILD_OK

int build_store_cache_materialize(s, tree_id[32], path_out, cap):   // CAS → cache/
  if kit_cas_verify_tree(cas, tree_id) != KIT_OK: return BUILD_ERR  // bytes absent → caller climbs ladder
  io->make_temp_dir(build/tmp, stage)
  kit_cas_materialize_tree(cas, tree_id, stage)                     // verifies each blob, applies modes
  build_store_cache_path(s, tree_id, final, cap); io->rename(stage, final); io->sync_path(parent)
  copy final into path_out; return BUILD_OK

int build_store_ingest_output(s, out_dir, out_tree_id[32], path_out, cap):
  kit_cas_add_tree_from_dir(cas, out_dir, out_tree_id)              // hashes+stores every file → tree id
  return build_store_cache_materialize(s, out_tree_id, path_out, cap)  // install into cache/ (idempotent)

int build_store_sandbox_new(s, sandbox_out, scap, out_dir_out, ocap):
  io->make_temp_dir(build/tmp, sandbox_out)                         // run-<n>/
  cas_host->mkdir_p(sandbox_out + "/out"); copy "<sandbox>/out" → out_dir_out; return BUILD_OK

Interface notes.


L2 — remote (untrusted object fetch)

Role. The trustless rung of the materialization ladder: when an object is absent locally and an object-remote is configured, render the user's fetch-recipe argv template, spawn it via the exec host into a temp file, verify the bytes against the requested content id, and only then install into the CAS. A corrupt/malicious mirror fails the hash check and is discarded. Remotes are tried in order.

Depends on. build (L0), <kit/build_coord.h> (KitBuildExec, KitBuildObjectRemote), <kit/cas.h> (install + verify).

Pseudocode.

int build_remote_fetch(ctx, exec, remotes[], n, cas, tmp_dir, kind, id[32]):
  if n == 0: return BUILD_ERR
  kit_hex_encode(hex, id, 32); pp = hex[0:2]
  for r in remotes:
    out = tmp_dir + "/fetch"
    argv = render(r.fetch_argv_template, {kind: kind==BLOB?"blob":"tree", pp, id:hex, out})
    exec->spawn(argv, …, &proc); exec->wait(proc, &code); if code != 0: continue
    bytes = file_io->read_all(out)
    if kind==BLOB:
        kit_blob_info(&bi, bytes.data, bytes.len); if !id_eq(bi.id, id): discard; continue
        kit_cas_add_blob(cas, bytes.data, bytes.len, &bi)              // re-derives same id
    else:  // tree manifest: store as blob, then it can be verified/materialized by id
        store tree manifest bytes; if resulting tree-id != id: discard; continue
    return BUILD_OK
  return BUILD_ERR

Interface note (minor → findings #3). The tree case needs an "ingest a tree manifest (not a directory) whose id I already know, and verify it equals id" CAS operation. kit_cas_add_blob stores raw bytes (the manifest is canonical bytes); verifying the stored tree then needs its referenced blobs present too. In practice a tree fetch is followed by blob fetches for its entries on demand during materialize. The fetch primitive itself only needs to install + id-check the manifest bytes; materialize already verifies the whole tree before use. The header suffices; the .c documents the two-step (tree manifest, then per-blob) fetch.


L3 — coord (coordinator context, memos, concurrency)

Role. Owns the KitBuildCoordinator and all process-lifetime state — no globals. Memoizes every base-input probe (source hash, glob, config-by-id, argv-by-id, recipe-id), every interned deepset node (by deep-set-id, with a refresh-valid cache so a shared subtree is checked once), and every target resolution, so a diamond builds once and a file hashes once. Owns the jobs semaphore, the per-target futures (cross-path memo and in-flight dedup) built on the optional KitBuildSched, and the per-target pulled set that makes the lazy trace-remote pull idempotent. Does not contain the resolution algorithm (that's resolve) or recipe execution (runner).

Depends on. build, cfg, defn, store, trace (all included by the spike header), <kit/build_coord.h> (host vtables, options), <kit/cas.h>.

Pseudocode.

KitStatus build_coord_open(ctx, host, store_root, opts, out):
  c = alloc; c.ctx_storage = *ctx; c.ctx = &c.ctx_storage; c.host = *host; c.opts = *opts
  kit_cas_open(ctx, host->cas_host, store_root + "/cas", &c.cas)
  build_store_open(ctx, c.cas, host->store_io, host->cas_host, store_root, &c.store)
  c.defn_bytes = file_io->read_all(opts->build_def_path)            // tracked source bytes
  build_defn_parse(c.defn_bytes.data, .len, &c.defn, err, …)
  c.jobs_limit = (host->sched && opts->jobs > 0) ? opts->jobs : (host->sched ? host_default : 1)
  init memos (source/glob/config/argv/targets hash maps over ctx->heap)
  if host->sched: c.lock = sched->mutex_new(); c.jobs_sem = sem_new(c.jobs_limit)
  *out = c; return KIT_OK

int build_coord_source_hash(c, path, out_blob[32], present):        // memoized
  lock; if path in c.sources: copy; unlock; return OK
  fd = file_io->read_all(workspace_root + "/" + path)
  if absent: *present = 0; memo (path → ABSENT); else kit_blob_info(&bi, fd.data, fd.len); out_blob = bi.id; *present = 1
  memo insert; unlock; return OK

int build_coord_glob(c, pattern, out_result_hash[32], cb, cb_user):  // memoized
  lock; if pattern in c.globs: replay matches to cb; copy hash; unlock; return OK
  matches = host glob over workspace (sorted); for each: source_hash(match)
  result_hash = build_glob_result_hash(sorted (path, blob) listing)   // BLAKE2b of canonical listing
  memo insert; replay to cb; unlock; return OK

int build_coord_config_by_id(c, id[32], out):                        // replay path
  lock; memo hit? copy. else: kit_cas_get_blob(cas, id, &fd); build_config_parse(fd.data, .len, out, …)
  kit_cas_release(cas, &fd); memo; unlock
  // build_coord_argv_by_id mirrors with build_argv_parse

int build_coord_recipe_id(c, target, out[32]):
  d = build_defn_find(&c.defn, target); if !d: return BUILD_ERR
  build_coord_source_hash(c, d->recipe_path, out, &present)          // recipe-id = hash(recipe file bytes)
  return present ? BUILD_OK : BUILD_ERR

int build_coord_deepset_load(c, deepset_id[32], out_node):           // Phase-1 replay
  if id in c.deepsets: *out_node = memoized; return BUILD_OK
  kit_cas_get_blob(cas, deepset_id, &fd)  // (remote blob fetch first if absent + remote)
     else return BUILD_ERR                                           // absent => Phase 1 falls through
  build_deepset_parse(fd.data, .len, &ds, ...)
  for child_id in ds.children: build_coord_deepset_load(c, child_id, &child) or return ERR  // recurse+intern
  node = intern(target, recipe, direct leaves, child nodes); c.deepsets[id] = node; *out_node = node

int build_coord_trace_remote_pull_once(c, target, out_pulled_now):   // lazy shared-trace pull
  if c.opts.n_trace_remotes == 0 or target in c.pulled: *out_pulled_now = 0; return BUILD_OK
  c.pulled.add(target); *out_pulled_now = (build_trace_remote_pull(c, target) == BUILD_OK)
  return BUILD_OK

// ---- concurrency ----
void build_coord_jobs_acquire(c): if c.jobs_sem: sem_wait(c.jobs_sem)   // no-op in sequential mode
void build_coord_jobs_release(c): if c.jobs_sem: sem_post(c.jobs_sem)

int build_coord_target_intern(c, target, cfg_id, argv_id, out_future, is_fresh):
  key = (target, cfg_id, argv_id); lock
  if key in c.targets: *out_future = existing; *is_fresh = 0
  else: f = new future(state=PENDING); c.targets[key] = f; *out_future = f; *is_fresh = 1
  unlock; return OK

int build_coord_target_await(c, f, out):                  // futures over sched cond
  lock; while f.state == PENDING: sched->cond_wait(f.cond, c.lock)
  ok = (f.state == DONE); if ok: *out = f.result; unlock; return ok ? OK : ERR
void build_coord_target_complete(c, f, res): lock; f.result=*res; f.state=DONE; cond_broadcast(f.cond); unlock
void build_coord_target_fail(c, f):          lock; f.state=FAILED; cond_broadcast(f.cond); unlock

In sequential mode (host->sched == NULL): jobs_* are no-ops, the lock is a no-op, and a fresh future is completed inline before any other caller can observe it, so await never blocks — futures degrade to a plain memo exactly as the public header promises.

Interface notes.


L4 — resolve (the three-phase algorithm)

Role. build_resolve(T, cfg, argv, chain) → {output-tree, path, leafset}, doing the least work necessary: Phase 1 deep fast path (same argv-id and matching scope-projected config observations, did sources move?), Phase 2 shallow path (config/argv/sources moved — guard consumed keys + direct leaves, re-resolve recorded deps by their recorded ids, compare output tree-ids), Phase 3 run the recipe (delegated to runner). Owns the per-path build chain (cycle detection), the materialization ladder, and leafset union/refresh.

Depends on. coord (L3, memos + futures + store access), cfg (L1), trace (via coord/store), runner (Phase 3 — see note), build (L0).

Pseudocode.

int build_resolve(c, T, cfg, argv, chain, out):                  // INLINE (top-level + fused need)
  if build_chain_extend(c, chain, T, cfg.id, argv.id, &chain', err) != OK: diag(err); return BUILD_ERR  // cycle
  build_coord_target_intern(c, T, cfg.id, argv.id, &f, &fresh)
  if !fresh: return build_coord_target_await(c, f, out)         // memo / dedup (no pop — chain is immutable)

  rc = resolve_phases(c, T, cfg, argv, chain', out)       // 1 → 2 → 3 below, on THIS thread
  if rc == OK: build_coord_target_complete(c, f, out) else build_coord_target_fail(c, f)
  return rc

int build_dispatch(c, T, cfg, argv, chain, &f_out, err):         // ASYNC (need-submit)
  if build_chain_extend(c, chain, T, cfg.id, argv.id, &chain', err) != OK: return BUILD_ERR  // cycle, fail fast
  build_coord_target_intern(c, T, cfg.id, argv.id, &f, &fresh)
  if fresh:
     if build_coord_spawn(c, worker, pack(c,T,cfg,argv,chain',f)) != OK:   // no sched → resolve inline now
        rc = resolve_phases(c,T,cfg,argv,chain',&r); rc==OK ? complete(f,r) : fail(f)
  *f_out = f; return BUILD_OK                              // returns WITHOUT awaiting
  // worker(args): rc = resolve_phases(...); rc==OK ? build_coord_target_complete(f,r) : build_coord_target_fail(f)

resolve_phases(c, T, cfg, argv, chain, out):
  key = build_target_key(T); rec = store_record_load(key, T)
  if rec empty: build_coord_trace_remote_pull_once(c, T, &pulled); if pulled: rec = store_record_load(key, T)

  // ---- Phase 1: deep fast path -------------------------------------------------
  for trace_id in rec where kind==DEEP, newest-first:
    D = load+parse deep trace (absent/garbage → skip)
    if id_eq(D.root_config, cfg.id) and id_eq(D.argv, argv.id):
      if build_coord_deepset_load(c, D.deepset, &node) != OK: continue   // blob absent → fall through
      build_leafset_refresh(c, node, &all_match)                         // rehash/reglob + recompute recipe-id
      if all_match and build_materialize(c, D.output, out->path, …) == OK:
        out->output_tree = D.output; out->leafset = node; return BUILD_OK
      // materialize MISS → keep scanning, ultimately Phase 3 (no rebuild inside materialize)

  // ---- Phase 2: shallow path ---------------------------------------------------
  for trace_id in rec where kind==SHALLOW, newest-first:
    S = load+parse shallow trace (absent/garbage → skip)
    if !id_eq(S.recipe, recipe_id(T)): continue                      // recipe edit/repoint (live defn)
    if !id_eq(S.argv, argv.id):        continue                      // local config differs
    if any observation in S.configs differs from cfg: continue
    if any direct source/glob leaf of S changed (refresh): continue
    ok = 1; children = []
    for (dep, overlay_id, dargv_id, recorded_tree) in S.deps:        // may fan out on threads
       dcfg = overlay(cfg, config_by_id(overlay_id)); dargv = argv_by_id(dargv_id)  // recompute effective config
       r = build_resolve(c, dep, dcfg, dargv, chain, &child)         // replay the need exactly
       if r != OK or !id_eq(child.output_tree, recorded_tree): ok = 0; break
       children.push(child.leafset)                                  // child deepset nodes
    if ok:
       build_leafset_union(c, direct_node(S), children, &node)       // builds + stores the root deepset
       write_deep_trace(c, T, cfg.id, argv.id, node.id, S.output)    // refresh → next call hits Phase 1
       if build_materialize(c, S.output, out->path, …) != OK: continue   // bytes gone → Phase 3
       out->output_tree = S.output; out->leafset = node; return BUILD_OK

  // ---- Phase 3: run the recipe -------------------------------------------------
  return build_run_recipe(c, T, cfg, argv, chain, out)               // runner.h

int build_materialize(c, tree_id, path_out, cap):                    // PURE locator — never runs a recipe
  if build_store_cache_lookup(store, tree_id, path_out, cap) == OK: return OK     // on disk
  if build_store_cache_materialize(store, tree_id, path_out, cap) == OK: return OK // local CAS → cache
  if opts.n_object_remotes and build_remote_fetch(…, tree_id) == OK:              // remote (trustless)
       return build_store_cache_materialize(store, tree_id, path_out, cap)
  return BUILD_ERR                                                   // bytes gone → caller falls to Phase 3

int build_chain_extend(c, parent, T, cfg_id, argv_id, &out, err, errcap):  // cactus stack, no pop
  for fr = parent; fr; fr = fr->parent:                                    // walk to the root
     if fr==(T,cfg_id,argv_id): render "//a → //b → //a" into err; return BUILD_ERR   // cycle
  f = arena_alloc(c.build_arena, BuildChainFrame); *f = {parent, T, cfg_id, argv_id}
  *out = f; return BUILD_OK
  // Immutable + parent-linked: concurrent dispatched siblings each allocate their own frame
  // over the shared read-only `parent`; nothing is copied and nothing is popped.

Interface notes.


L5 — runner (Phase 3: spawn + service a recipe)

Role. Run a recipe under (cfg, argv): acquire a job slot, stage a sandbox, spawn via the exec host with KIT_BUILD_SOCK/OUT/TARGET in the env, service its protocol connection (logging every request as a dep, recursing into build_resolve for each need), then on success ingest the output tree, write both trace kinds, prepend them to the record, install the output in the cache, and return. A nonzero exit propagates failure and writes no trace.

Depends on. coord (L3), resolve (L4, for need recursion + materialize), protocol (L1, wire codec — runner.c includes it), trace (L1), cfg (L1), store (via coord), <kit/build_coord.h> (exec + transport vtables).

Pseudocode.

int build_run_recipe(c, T, cfg, argv, chain, out):
  if c.opts.verify and a trace says "unchanged": run anyway, compare fresh tree-id, diag on mismatch  // verify mode
  build_coord_jobs_acquire(c)                                  // bounds *actively running* recipes
  build_store_sandbox_new(store, sandbox, out_dir)
  listener = transport->listen(&endpoint_name)                 // per-recipe endpoint  ← findings #5
  env = CLEAN: { KIT_BUILD_SOCK=endpoint_name, KIT_BUILD_OUT=out_dir, KIT_BUILD_TARGET=T,
                 workspace_root } + cfg's `env.`-prefixed keys (KIT_BUILD_ENV_PREFIX) as env vars
  exec->spawn(recipe_argv = [recipe_path] ++ argv.args, env, cwd=workspace_root, &proc)  // host inherits nothing
  log = {}; transport->accept(listener, &conn)
  build_runner_service(c, conn, T, cfg, chain, &log)           // drives protocol; recurses for `need`
  transport->close(conn); transport->close_listener(listener)
  exec->wait(proc, &code)
  build_coord_jobs_release(c)
  if code != 0: build_store_sandbox_done(store, sandbox); return BUILD_ERR        // NO trace
  build_store_ingest_output(store, out_dir, &output_tree, out->path, …)
  build_store_sandbox_done(store, sandbox)
  put serialized cfg map (→ cfg.id) and argv vector (→ argv.id) into the CAS (kit_cas_add_blob)
  build_runner_record_traces(c, T, cfg, argv, &log, output_tree, &node)           // builds+stores deepset, both kinds
  out->output_tree = output_tree; out->leafset = node; return BUILD_OK

int build_runner_service(c, conn, T, cfg, chain, log):
  loop:
    transport->read_frame(conn, frame, cap, &n); if EOF/closed: return BUILD_OK   // recipe done
    build_proto_decode_req(frame, n, &req, ovr_storage, …, argv_storage, …)
    switch req.cmd:
      CONFIG_GET: build_config_get(cfg, req.arg, &v, &present)
                  append req.arg to log.config_keys (consumed, set or unset)
                  reply {present?OK:UNSET, text=v}
      SOURCE:     build_coord_source_hash(c, req.arg, blob, &present)
                  append (req.arg, blob, absent=!present) to log.sources
                  reply present ? {OK, id=blob, text=realpath} : {OK absent, no path}  // live workspace path
      GLOB:       build_coord_glob(c, req.arg, result_hash, collect_paths, &acc)
                  append (req.arg, result_hash) to log.globs
                  reply matches in OK batch frames, then a GLOB_END frame             // streamed, multi-frame
      NEED:       dcfg = build_config_overlay(cfg, req.overrides, …); dargv = build_argv_set(req.argv, …)
                  overlay_id = store(overlay(empty, req.overrides))      // just the delta; effective cfg is derivable
                  build_coord_jobs_release(c)                            // ← yield slot while blocked (findings #6)
                  build_resolve(c, req.arg, &dcfg, &dargv, chain, &r)    // inline (no worker)
                  build_coord_jobs_acquire(c)                            // ← reclaim before resuming work
                  record_dep(log, req.arg, overlay_id, dargv.id, &r)     // append edge + push child node
                  reply {OK, id=r.output_tree, text=r.path}
      NEED_SUBMIT: dcfg = overlay; dargv = set; overlay_id = store(overlay(empty, req.overrides))
                  if build_dispatch(c, req.arg, &dcfg, &dargv, chain, &f, err) != OK: reply {ERROR}  // cycle/unknown
                  tok = log.next_token++; log.pending.push({tok, req.arg, overlay_id, dargv.id, f})
                  reply {OK, token=tok}                                  // does NOT block
      NEED_AWAIT: p = find_pending(log, req.token); if none: reply {ERROR}
                  build_coord_jobs_release(c); build_coord_target_await(c, p.future, &r); build_coord_jobs_acquire(c)
                  if r failed: reply {ERROR}
                  record_dep(log, p.dep, p.overlay_id, p.argv_id, &r)    // edge logged HERE, on await
                  remove p from log.pending; reply {OK, id=r.output_tree, text=r.path}
    // on EOF: for each still-pending need, exec->kill its in-flight recipes (best-effort); record NO edge

int build_runner_record_traces(c, T, cfg, argv, log, output[32], &out_node):
  recipe_id = build_coord_recipe_id(c, T)
  build_leafset_union(c, direct_node{T, recipe_id, log.sources, log.globs}, log.child_leafsets, &node)
      // builds the root deepset node, emits it, stores it as a CAS blob, yields node.id (= deep-set-id)
  S = shallow{T, recipe_id, output, argv.id, log.configs, log.sources, log.globs, log.deps}
  emit S → bytes; build_store_put_trace(store, bytes, &sid); build_store_record_update(store, key(T), T, SHALLOW, sid)
  D = deep{T, recipe_id, output, cfg.id, argv.id, deepset=node.id}    // one pointer, not an inline closure
  emit D → bytes; build_store_put_trace(store, bytes, &did); build_store_record_update(store, key(T), T, DEEP, did)
  *out_node = node; return BUILD_OK

Interface notes.


L5 — bundle (signed trace sharing)

Role. Export/import traces as signed .kpkg bundles, reusing the <kit/package.h> manifest + minisign + trust machinery wholesale. A trace is a claim (not self-verifying), so import is gated by signature/trust; the output bytes it references stay trustless (verified by tree-id/blob-id on use). Also pulls trace bundles from configured trace-remotes (the trusted counterpart to remote).

Depends on. coord (L3), store/trace (via coord), <kit/package.h> (kit_pkg_create/kit_pkg_verify/kit_minisig_*/kit_trust_*), <kit/build_coord.h>.

Pseudocode.

int build_bundle_export(c, opts):
  stage = make_temp_dir()                                  // assemble the bundle payload tree
  claims = []
  for T in opts.targets:
    rec = store_record_load(key(T), T)
    for row in rec (newest candidates): copy build/trace/<row.trace_id> → stage/trace/<id>
       parse the body → collect referenced overlay-id, argv-id (and output-tree for the claim row)
       claims.push({T, row.kind, row.trace_id, output_tree})
  for each referenced overlay-id/argv-id: kit_cas_get_blob → stage/cas/blob/<id>     // required for replay
  if opts.include_outputs: also copy referenced output trees + their blobs into stage/cas/
  build_bundle_manifest_emit(claims, &w) → stage/manifest "kit-build-traces 1"
  kit_pkg_create({ root_dir: stage, format: opts.format, sk: opts.sk, keyid: opts.keyid,
                   name:"traces", out_path: opts.out_path })                         // signs the whole tree
  return BUILD_OK

int build_bundle_import(c, opts, result):
  unpack = make_temp_dir()
  kit_pkg_verify({ pkg_data, pkg_len, format, pubkey_bytes, trusted_keys, tofu, unpack_dir: unpack }, &vr)
  if !verified: return BUILD_ERR                            // signature/trust failure
  read unpack/manifest → build_bundle_manifest_parse → claims
  for blob in unpack/cas/blob/*: kit_cas_add_blob(cas, bytes)         // overlays/argv/(outputs), hash-checked
  for c in claims:
     install unpack/trace/<c.trace_id> via build_store_put_trace      // id re-checked on store
     build_store_record_update(store, key(c.target), c.target, c.kind, c.trace_id)   // prepend
  result.n_traces = |claims|; result.keyid = vr.keyid; result.tofu_pin = vr.tofu_pin; result.tofu_pk = vr.tofu_pk
  return BUILD_OK

int build_trace_remote_pull(c, T):                          // trusted fetch, mirrors remote.c
  for tr in opts.trace_remotes:
    out = tmp/"bundle"; argv = render(tr.fetch_argv_template, {target: T, out}); exec->spawn; exec->wait
    if code != 0: continue
    build_bundle_import(c, { pkg_data: read(out), trusted_keys: tr.trusted_keys, tofu: tr.tofu }, &res)
    if ok: return BUILD_OK
  return BUILD_ERR

// build_bundle_manifest_emit/parse: canonical "kit-build-traces 1" rows
//   <target> <kind> <trace-id> <output-tree-id>, sorted, mirroring dist_manifest_*

Interface notes.


L6 — src/api/build.c (composition root)

Role. Implements the public <kit/build_coord.h> coordinator + export/import entry points by calling the internal modules, mapping BUILD_OK/ERR (and the err buffers) to KitStatus + ctx->diag, and owning the sizing of every caller-provided section buffer (the trace/record/config growable arrays) so no inner module allocates policy. Gated by KIT_BUILD_ENABLED; a stubbed-out build compiles these to KIT_UNSUPPORTED like config_stubs.c.

Depends on. Everything (it is the root). The recipe-side kit_build_client_* are implemented in client.c but composed here under the same KIT_BUILD_ENABLED gate.

Pseudocode.

KitStatus kit_build(coord, req, out):
  BuildConfig cfg over stack/heap storage; for kv in req.config: build_config_set(cfg, kv.key, kv.value)
  build_config_id(heap, &cfg, &cfg.id)
  BuildArgv argv; build_argv_set(&argv, req.argv, req.argc); build_argv_id(heap, &argv, &argv.id)
  reset coord per-build arena (chain frames + dispatched-worker tracking live here)
  BuildResolved r; rc = build_resolve(coord, req.target, &cfg, &argv, /*chain=*/NULL, &r)  // root: no parent
  join any outstanding dispatched workers; if rc != OK: return build_status_from(coord->ctx, rc)
  memcpy(out->output_tree, r.output_tree, 32); strcpy(out->path, r.path); return KIT_OK

kit_build_traces_export/import thinly wrap build_bundle_export/import; kit_build_client_* live in client.c.


Concurrency model (cross-cutting)


Interface findings

Concrete adjustments the pseudocode surfaced. All applied to the src/build/*.h spike, the public <kit/build_coord.h>, and BUILD_COORDINATOR.md as of this revision; the entries below are the rationale of record. None block starting the L0/L1 modules.

  1. client module ownership. The recipe-side kit_build_client_* need a real translation unit (src/build/client.c) with no internal header — assigned an owner in Wave B below so it isn't mistaken for part of coord. (No header change; planning note only.)
  2. build_run_recipe prototype placement. (Applied — resolve.h, runner.h.) resolverunner is a deliberate mutual recursion. build_run_recipe is now declared in resolve.h (the Phase-3 continuation) and implemented in runner.c; runner.h carries only build_runner_service / build_runner_record_traces. Avoids an awkward resolve.h → runner.h include.
  3. Tree fetch in remote. (Applied — remote.h comment.) The two-step tree fetch (manifest bytes id-checked + installed, then per-blob fetch on demand during materialize) is documented; the function signature is sufficient.
  4. build_glob_result_hash in build.h. (Applied — build.h.) Added as a pure helper over a new foundation BuildPathBlob pair type, so the sole producer of the [glob] value is separately unit-testable and byte-stable.
  5. Transport listener (public header change). (Applied — <kit/build_coord.h>.) Added the per-recipe endpoint to KitBuildTransport: listen(user, char* name_out, size_t cap, KitBuildListener** out), accept(user, KitBuildListener*, KitBuildConn** out), close_listener. Concurrent recipes each get a uniquely-named endpoint; the client dial side was already there.
  6. Jobs slot released while blocked on need. (Applied — coord.h, runner.h, BUILD_COORDINATOR.md §Parallelism + the run_recipe sketch.) The semaphore bounds actively running recipes; a recipe blocked on a need yields its slot, preventing a jobs < chain-depth deadlock.
  7. build_coord_leafset_intern. (Applied — coord.h.) runner/resolve hand back a process-lived BuildLeafSet* (BuildResolved.leafset is borrowed-for-the-process); the interning entry point lives in coord rather than leaking heap ownership upward.

Design-review decisions (a second pass that pressed on correctness and seams; all applied to the spike headers, <kit/build_coord.h>, and BUILD_COORDINATOR.md):

  1. Deep-trace soundness vs. recipe changes. (Applied — trace.h, coord.h, resolve.h, defn.h.) The flat deep trace checked source/glob leaves but never recipe identity, so a recipe edit (T's own or any descendant's) with unchanged sources produced a stale Phase-1 hit. Fix: recipe-id is a per-node scalar on the deepset, refreshed by recomputing through the live definition. Content-only (same-content repoint = no-op) and transitive (a descendant's recipe change busts the root). The whole-definition source leaf is removed — only the resolved recipe-id is a definition-derived input.
  2. Structural deepset closure adopted now. (Applied — trace.h BuildDeepSet, coord.h node + deepsets memo + build_coord_deepset_load, resolve.h union/refresh, build.h BUILD_DEEPSET_MAGIC.) The closure is a Merkle DAG of kit-build-deepset 1 nodes stored as CAS blobs (deep-set-id == blob-id — self-verifying content, so cas/, not build/). The deep trace holds one deepset <id> pointer. Refresh is O(changed frontier) via the validity memo. BuildLeafSet is now that in-memory node (was a flat source/glob list).
  3. Absent source leaf. (Applied — trace.h BuildSourceLeaf.absent, protocol/runner.) An absent source read is recorded (<path> -) so creating the file later busts the trace — the source analogue of consumed-while-unset.
  4. materialize is a pure locator. (Applied — resolve.h.) Dropped target/cfg/argv/chain; returns BUILD_ERR on a total miss and resolve falls through to Phase 3. Removes the materialize → runner edge and the rebuild-returns-a-different-tree hazard.
  5. Lazy trace-remote pull. (Applied — coord.h build_coord_trace_remote_pull_once + pulled set; resolve.h Phase-0 hook.) A clean-checkout record miss attempts one signed-bundle pull (idempotent per target) before Phase 3, so shared traces can actually be hit.
  6. Clean recipe environment + env.*. *(Applied — <kit/build_coord.h> KitBuildExec
    • KIT_BUILD_ENV_PREFIX, runner.h.)* spawn's env is the recipe's COMPLETE environment (host inherits nothing); declared env comes from tracked env.-prefixed config, so it is hermetic and cache-visible.
  7. Glob responses span frames. (Applied — protocol.h BUILD_RESP_GLOB_END.) A glob match set larger than one 64 KB frame streams as batch frames terminated by an END frame, instead of silently truncating.
  8. Future-based need (concurrent fan-out). (Applied — <kit/build_coord.h> KitBuildNeedToken + need_submit/need_await; protocol.h NEED_SUBMIT/NEED_AWAIT + token fields; coord.h build_coord_spawn; resolve.h build_dispatch + cactus BuildChainFrame; runner.h BuildPendingNeed.) A recipe submits its fan-out (each need-submit → dispatched on a worker, returns a token) then need-awaits the tokens, so independent deps build concurrently; the fused blocking need stays for the one-at-a-time case. Three correctness constraints: the dep edge is logged on await (un-awaited submits are speculative, cancelled at exit); await is token-specific (no await-any) so the awaited set stays input-deterministic; and the chain becomes a parent-linked cactus stack so concurrent siblings extend a shared read-only prefix without copying. Sequential mode resolves submit eagerly inline (transparent).

Suggested parallel implementation order

The DAG makes the seams obvious; teams can take whole layers independently behind the spike headers.

Parallel-agent handoff contracts

Each module owner should treat its header as the contract and this table as the acceptance gate. A module is "done" only when its focused tests land with it and no downstream module has to reinterpret its bytes, ownership rules, or error behavior. Keep module tests targeted (make test-build-<module> once wired); do not require an end-to-end coordinator to validate L0-L2 work.

Module Owner edits May depend on Must provide Required focused tests Must not do
build src/build/build.c, src/build/build.h comments only if needed public hash/core/CAS APIs build_target_key, build_glob_result_hash, build_diagf; stable constants fixed id vectors for target-key and glob-result; sorted path/blob listing; invalid/overflow inputs no filesystem, no coordinator state, no allocation except bounded scratch/writer
cfg src/build/cfg.c, src/build/cfg.h build sorted propagated map; positional argv; canonical emit/parse/id; overlay emit golden bytes and ids; parse rejects duplicates/out-of-order/unknown forms; overlay replaces and preserves sort; empty argv id vector no CAS I/O, no ambient config/env reads, no hidden defaults
defn src/build/defn.c, src/build/defn.h build strict kit-build 1 parser and lookup canonical definition golden; sorted-unique target rejection; unrelated stanza edit does not affect find(T) result no recipe hashing, no source dep recording, no argv defaults
trace src/build/trace.c, src/build/trace.h build shallow/deep/deepset/record emit/parse/id/prepend golden bodies+ids for all four formats; absent source row; non-canonical rejection; record MRU dedup and per-kind cap no store/CAS access, no matching against live workspace
protocol src/build/protocol.c, src/build/protocol.h build, public slices exact wire codec specified above golden hex frames; malformed corpus; glob multi-frame decode; round-trip re-encode no transport I/O, no malloc, no coordinator side effects
client src/build/client.c, public <kit/build_coord.h> only if a real API gap appears protocol, KitBuildTransport public kit_build_client_* round-trips; borrowed response slices valid until next call loopback scripted server for each public call; error mapping; absent source; glob callback streaming; close releases conn no coordinator/store access, no filesystem, no process spawning
store src/build/store.c, src/build/store.h build, trace, public CAS, host vtables path layout; trace put/get; target record RMW; cache materialize; sandbox lifecycle fake KitBuildStoreIo tests for idempotence, malformed-record-as-empty, crash injection, cache verify, sandbox cleanup no trace matching, no recipe execution, no remote fetch
remote src/build/remote.c, src/build/remote.h build, public CAS, KitBuildExec fetch-template rendering; verified blob/tree/deepset install; try remotes in order fake exec good/corrupt/missing; hash mismatch rejected; second remote succeeds; deepset fetched as blob no trust decisions, no trace import, no network API beyond exec
coord src/build/coord.c, src/build/coord.h cfg, defn, store, trace, public CAS context open/close; source/glob/config/argv/recipe memos; deepset intern/load; futures; stats; jobs semaphore fake workspace memo counts; config/argv by-id load; recursive deepset load; sequential degrade; future dedup; stats bumps under lock no resolution phase policy, no recipe spawn loop
resolve src/build/resolve.c, src/build/resolve.h coord, cfg, trace; calls build_run_recipe by declared seam chain/cycle detection; Phase 1/2/3 selection; materialization ladder; leafset refresh/union deep hit; shallow hit after changed dep with same output; recipe-id bust; materialize miss; cycle path; exact stats deltas no process I/O, no protocol service loop
runner src/build/runner.c, src/build/runner.h coord, resolve, protocol, trace, cfg spawn environment; protocol service; dep log; job-slot release across needs; trace recording scripted transport/exec: config/source/glob/need logs; nonzero exit writes no trace; need-submit/await determinism; jobs=1 deep chain no Phase 1/2 matching policy, no signed bundle logic
bundle src/build/bundle.c, src/build/bundle.h coord, store, public package/CAS signed trace export/import; trace remote pull package round-trip; untrusted signer rejected; tofu pin result; imported trace has overlay/argv/deepset blobs; bad trace id rejected no object remote fetch, no recipe execution
api src/api/build_coord.c or chosen composition TU, Makefile gating all internal modules public coordinator, stats, trace import/export, stubs under disabled gate public API smoke using fake host; unsupported gate; status/diag mapping no policy hidden in driver
driver driver/cmd/build_coord.c or final command name, driver tables public API only CLI parses flags, supplies hosted vtables, exposes shell helper subcommands for recipes run.sh subprocess/socket smoke; helper commands call client API; clean env behavior no internal src/build/* includes

Cross-agent rules.