Part III Scale It From Loop to System
13 min read

Chapter 8 – Keep the Map Fresh (Map-Updaters)

Drift is the default state. Docs fall behind code. Specs lag behind services. Inventories and indexes rot. Policies miss new surfaces.

In Software Development as Code (SDaC), you do not fight drift with reminders. You fight it with a loop that turns “this Map surface is wrong” into a bounded, reviewable patch.

Enter the Map-Updater. A Map-Updater is the specialized loop that keeps one Map surface synchronized with its corresponding Terrain. It observes reality, compares it to the current Map, and emits a concrete, reviewable patch when the two diverge.

A Map-Updater matters because a Map is part of a context slice. You rarely want to hand an Effector the whole Terrain. You want a compact Map that says what matters, what is in-bounds, and what must hold. A well-kept Map can be more useful than a raw dump because it preserves intent without dragging in stale detail.

The Operating Map

At small scales, the Map is easy to point to: a schema, an interface spec, an inventory, a policy file. At larger scales, it becomes the operating Map: strategy, architecture decisions, operating rules, incident learnings, onboarding guidance, brand voice, tone, and culture.

That does not mean every one of those surfaces becomes a hard CI gate. It means they are all memory surfaces the system should stop forgetting.

Map-Updaters are how that memory stays coupled to reality. A changed API should update the contract. A repeated incident should update a runbook or validator. A new architectural boundary should update the architecture Map and policy. Without these loops, the organization relearns the same lessons in tickets, chat threads, and review comments.

The Map-Updater Loop: Sense → Normalize → Compare → Propose

Skeleton-First Rule (Prevent Map Contamination)

Extract the skeleton from Terrain first, then let the model fill prose around it. If the skeleton changes, fail the update and write a receipt.

Interactive updater
Toggle extraction and run bounded attempts. You should see either a clean PASS or a deterministic ESCALATE when the budget runs out.
Sense/Normalize
Extract skeleton
Propose
Generate prose patch
Validate
Hard skeleton gate
Ledger
State of record
Contamination pressure 40/100
Retry budget 5x
Extractor
Step attempt
Bounded loop
Reset
Canonical skeleton (Sensor)
    Proposed skeleton (Map)
      Mismatch count
      0
      Contamination risk
      0/100
      Skeleton gate
      PASS
      Budget remaining
      5x
      Constraint → deterministic gate
      Skeleton must match exactly PASS
      No invented structure PASS
      Retry budget not exceeded PASS
      Receipt written to Ledger PENDING
      Updater state of record
      Status READY
      Attempt 0/5
      Last run --
      ledger/map-updaters/skeleton-first/attempt-00.json
      Mismatch trend (W=4)
      Collecting baseline.
      Ready to run one bounded attempt.
      Loop attempts
      Cyan = PASS, red = FAIL, violet = ESCALATE

      A Map-Updater follows a consistent four-step process:

      1. Sense: read the Terrain directly from the source of truth: a running service, a repository, a cloud config, a schema file. The output is raw reality.

      2. Normalize: turn that sensed data into a structured, canonical form that is easy to compare: parse it, sort it, standardize names, and drop irrelevant transients.

      3. Compare: read the current Map, normalize it too, then deterministically compare normalized Map and normalized Terrain. The output is a precise diff.

      4. Propose: generate the patch that brings Map and Terrain back into alignment, in the direction your authority model permits. The patch must be reviewable.

      This is the same Sandwich pattern, aimed at a Map surface: Sense/Normalize/Compare is Prep, Propose is the stochastic step, and Validation is the hard gate.

      Keep the loop honest by emitting a reviewable diff plus Validator output into the Ledger.

      If you want the loop in one screen:

      terrain = normalize(sense_terrain())
      map = normalize(read_map())
      diff = compare(map, terrain)
      if diff:
          propose_patch(diff)  # reviewable output, not prose advice

      One key reversal to name explicitly: Chapters 1–7 mostly run Map → Terrain (intent drives changes). Map-Updaters often run Terrain → Map (reality updates documentation and intent artifacts). Both directions are valid, but they require a clear authority model.

      Meta-Pattern: Skeleton-First Rule (Maps get skeletons from Sensors)

      Map-Updaters are where teams get hurt by fabricated structure. Chapter 2 covered the Skeleton-First rule in general; here is the Map-specific version.

      Rule: build the Map on a deterministically extracted skeleton, then (optionally) let the model add “flesh” such as explanations or formatting. The model must never invent the skeleton.

      Concrete skeletons you can extract deterministically:

      If the model writes the skeleton, it will eventually invent routes, symbols, or fields. Those invented facts then get fed back in as context on the next run, and the updater starts converging on a clean story instead of reality. This is Map Contamination in SDaC: generated structure gets mistaken for extracted fact.

      Mechanism: extract the skeleton from the Terrain, constrain edits to an allowed region, then re-extract the skeleton from the candidate Map and require an exact match. Treat mismatch as a hard fail in CI.

      If the re-extracted skeleton does not match exactly, stop immediately. Do not let the model “explain” or prose-polish its way past a skeleton mismatch.

      When you can, make the skeleton the whole story: generate the Map deterministically from Sensors, with zero AI in the critical path.

      Brownfield Adoption: The Hollow Map (Flashlight)

      In a legacy monolith, a full Context Graph is often the wrong first project. If you try to map the whole Terrain at once, you’ll stall before you ship the first loop.

      The Hollow Map pattern is a brownfield on-ramp: you build a temporary Map that contains only the path you’re touching today, then you throw it away.

      If the same slice keeps showing up, promote it into a cached snapshot later. But do not start by trying to maintain a “perfect architecture doc” for a system you can’t yet reason about.

      Example: Keep agent instructions aligned (a self-referential Map-Updater)

      A practical Map-Updater is one that keeps its own operating instructions current.

      In this repository, we keep per-directory agent-instructions files as local operator manuals: what this directory is for, what invariants matter, and how to run the tools here. Different agent runtimes use different conventions, but the pattern is the same: keep a small Map close to the Terrain it governs.

      The concrete implementation is the agent-instructions Map-Updater:

      This is intentionally an advisory updater: if AI is unavailable it emits an explicit no-change marker and returns success. That keeps the build deterministic while still making drift visible whenever the tool can run.

      @@ NO CHANGE @@

      Here is the loop in action:

      $ python -m tools.update_agents --path mk/book.mk
      INFO: AGENTS update suggestions written to build/update_agents/mk_agents_suggestions.md

      The suggestions file is a small, reviewable patch format:

      FILE: mk/AGENTS.md
      @@ SUGGESTED PATCH @@
      @@
      -<old lines…>
      +<new lines…>

      If you want a fully automated “maintenance sweep,” this repo includes an example Make target that runs the instructions updater on a few anchor files and then applies the suggestions in one pass: make update-agents-epoch. The key move is that it still leaves you with a clean git diff to review: the loop proposes; humans accept.

      Alternative: The Low-Tech Start

      You can also do a “cheap Map-Updater” version without wiring the full deterministic pipeline yet. Keep a small instruction file close to the code it governs, then ask a programming AI agent to propose an update when the directory’s purpose, commands, or boundaries change. Review the diff like any other doc change.

      Different agent runtimes use different conventions for these files. For example:

      AGENTS.md
      CLAUDE.md

      The power move is locality. Put these files in subdirectories so the Map stays focused on the Terrain it’s describing:

      repo/
        AGENTS.md
        tools/AGENTS.md
        services/payments/AGENTS.md

      This is not “complete SDaC” on day one. Your sensing and comparison may be less formal than make help parsing, but it still tightens intent. It reduces context pollution (“what is this directory for?”) and gives both humans and agents a small, versioned Map surface to anchor on.

      The important point is not the particular file name. The point is the direction: your terrain changed, so you update the Map that tells future operators how to move through that terrain.

      Example: Update meta/ from chapters (Map surfaces for the manuscript)

      Not every Map surface is an operations manual. In this book’s engine, meta/ is a Map for the manuscript: it carries the concepts, outline, and the editorial constraints that make downstream writing tools converge instead of drift.

      This repo contains a Workflow-based Map-Updater that proposes changes to meta/ based on a chapter’s current prose:

      make apply-meta SUGGESTIONS=build/update_meta

      This is the same pattern as the agent-instructions updater, pointed at a different Map surface: observe Terrain (chapter prose), normalize (extract candidates), compare to the current Map, then propose a reviewable patch.

      Authority Models: Terrain-first vs. Map-first Surfaces

      The direction of the patch is determined by the authority model:

      Trying to make one surface both Terrain-first and Map-first creates a tug-of-war over what “truth” is.

      Different memory surfaces also compile differently. Contracts and policies often become hard gates. Brand voice, tone, and onboarding rules usually compile into templates, directives, retrieval defaults, review checklists, or Judge criteria. Treating them all as if they were schemas is naive; treating them all as versioned memory is correct.

      Map Quality Criteria (what “fresh” means)

      Keeping a Map “fresh” is not the same as making it long. A good Map is:

      Property Why it matters
      Bounded A Map that tries to cover everything becomes noise
      Skeleton-anchored The non-negotiable structure comes from Sensors, not generation
      Explicit about scope What the updater may edit (and must not touch) is written down
      Reviewable Output is a diff, not a paragraph
      Auditable Every change has evidence (diff + validator output) in the Ledger
      Stable under iteration It doesn’t oscillate because the authority model is clear

      CI Gate: Drift Becomes a Build Failure

      A Map-Updater becomes powerful when drift becomes non-ignorable:

      1. Run the Map-Updater in CI for every relevant change.
      2. If drift exists, fail the build (or block merge) with the proposed patch attached as evidence.
      3. Require the Map surface to be updated in the same pull request, or require an explicit waiver.

      This turns drift from a quiet, compounding problem into an immediate, bounded, reviewable change.

      Two practical constraints matter in CI:

      Not every updater is a pure Physics gate. Some Map-Updaters are advisory (they generate suggested prose patches). That’s still valuable: the output becomes part of the Decision Trace, and humans can review and accept or reject the update.

      Documentation Physics (validators for Maps)

      If a Map-Updater can write prose, you need Physics for prose.

      At minimum, you want validators that catch structural drift and invented structure:

      In this repo, the manuscript has its own Physics targets (for example make text-health plus tools.lint_concepts). The existence of these validators is the point: a Map-Updater only becomes trustworthy when the Map surface is gated by constraints it cannot argue with.

      North Star: Map-Updaters are a plank of the Hofstadter Bridge

      At first, a Map-Updater feels like housekeeping: “keep docs fresh.”

      The higher-leverage view is that you are turning documentation from an output into an input. Map/Terrain sync today is the prerequisite for Maps that can become executable constraints tomorrow. Chapter 13 takes that next step in full (the Hofstadter Bridge): the Map stops being advisory and starts acting as an enforcer.

      The larger north star is a Torus: think of a looped surface rather than a straight line, where the operating Map shapes execution, execution produces evidence, evidence updates the Map, and the next loop starts from a truer operating Map. Over time, that is not just one loop but many bounded loops running in parallel across the organization at different levels.

      If you want that future safely, you start here: deterministic skeletons, bounded diffs, and hard gates.

      Meta-Maps: When Maps Need Maps

      A slice map (a saved Context Packet: what’s in, what’s out, and why) works for a feature. A handful of slice maps work for a release. But what happens when you have fifty slice maps across twelve teams?

      You need a map of maps—a meta-map.

      This is not new complexity. It is the same fractal structure showing up at scale. Just as a function’s docstring is a Map for its implementation, and a module’s API is a Map for its functions, a meta-map is a Map for your collection of slice maps.

      flowchart TD
        subgraph Meta["Meta-Map<br/>Q2 Platform Migration"]
          A["Slice Map A<br/>Auth Service<br/>12 slices<br/>Converging"] --> B["Slice Map B<br/>Data Layer<br/>8 slices<br/>Mixed"] --> C["Slice Map C<br/>API Gateway<br/>15 slices<br/>Early"]
          B --- Status["Status: B blocked on A slice 9<br/>Next convergence check: Friday"]
        end

      What a meta-map tracks:

      Field Purpose
      Child slice maps Which maps roll up to this one
      Cross-map dependencies Which slices in Map A gate slices in Map B
      Aggregate progress Convergence status across all children
      Escalation triggers When to surface blockers to humans
      Rollup schedule When child maps report status

      The key insight: meta-maps do not require new primitives. They reuse the same structure: explicit intent, typed artifacts, and validation gates, applied to maps instead of code. The Mission Object for a meta-map is “coordinate these child maps toward this milestone.” The Immune System checks cross-map invariants.

      This is how SDaC scales without becoming a different system at each level. Recursion is the scaling mechanism.

      When you need a meta-map:

      At depth 0-1, humans hold meta-maps in their heads or in project management tools. At depth 2+, meta-maps become explicit artifacts that agents read and update—the system managing its own coordination.

      Actionable: What you can do this week

      Choose one Map surface in your current project that drifts often.

      1. Identify Terrain and Map: pinpoint the source of truth and the Map file(s) that should track it.
      2. Choose an authority model: Terrain-first or Map-first. Write it down.
      3. Sketch the updater loop: Sense -> Normalize -> Compare -> Propose.
      4. Pick a skeleton you can extract deterministically: signatures, routes, schemas, inventories. Treat it as truth.
      5. Add one deterministic gate: re-extract the skeleton after the patch, or make drift fail in CI with the proposed patch attached as evidence.
      6. Make the output reviewable: emit a unified diff or a patch file, not a paragraph of advice.
      7. Wire it into your loop: run it on every relevant change, and decide whether it should fail the build or simply attach evidence to review.
      Share