--- name: vault-one-fact-one-owner-dedupe description: "\"Collapse per-payload/per-attempt vault pages into one owner page per endpoint without losing a fact: port-before-cut orphan census, doubled-page repair, trash-aware tools, delegated-write verification, index regeneration, and the merge-resolution order.\"" --- # One fact, one owner: dedupe the vault without losing a fact Use when a repo has accumulated one page per attempt/era (runbooks, bench logs, swap narratives) and the law is **one page owns one thing**. Applies to `serverAppConfigs-docs/` (Obsidian vault, `AGENTS.md` → *Documentation Policy*), but the mechanics are general. ## 1. Decide ownership before touching files - Endpoint/service page owns: current shape, **Eras** table (window | payload | runtime | shape | replaced because | rollback), **Laws**, wire contract, files. - Technique page owns the mechanism and the arithmetic — **no per-endpoint throughput table or boot ledger**. - `log.md` owns incidents and laws only. A swap is an Eras row, never a log entry. - `AGENTS.md` carries operator-critical numbers + cross-border laws; everything else links. - A **retired** endpoint still gets one locator page: era windows quoted verbatim from each era page's own banner, no numbers restated, and any date conflict recorded as an open question instead of silently resolved. Never pick a winner between two primary sources. ## 2. Single-writer rule (the mistake that costs the most) Facts get **ported into the owner page before they are cut** anywhere else. So while any subagent may port facts, only ONE writer may touch the owner pages. Spawn children with an explicit `DO NOT EDIT ` and require a `needsPort` list in their report instead of letting them append. Send the narrowing message early — a child already editing them has to be reconciled by hand. ## 3. Port-before-cut census (proves nothing was lost) For every deleted line, extract distinctive keys and check them against the vault minus the file being rewritten. Any line with **no** surviving key is an orphan → port, or accept deliberately and say so. Use `scripts/port-census.py ` (numbers with ≥ 4 int digits, decimals, flags, paths, backticked spans, `--`, ALL-CAPS, or ≥ 2 hyphens). Two calibration traps: - **Prose words are not facts.** A hyphen or trailing period inside a word (`long-standing`, `deployed:`) was counted as a tracked token while real identifiers hid behind it; that noise buried 300 real lines under 979 fake ones. Strip punctuation, require fact shape. - **The corpus decides the question.** Corpus = the new file only → "what did this rewrite drop?" Corpus = whole vault → "is the dropped fact orphaned repo-wide?" Ask one at a time. Also grep `AGENTS.md` separately — a fact it carries is not an orphan. ## 4. When a page is the same page twice Signature: a page whose `wc -l` is ~2× its `HEAD` size, with a **second frontmatter block in the body** whose closing `---` is glued onto the end of a body/timestamp line. Cause chain: a frontmatter rewrite that re-reads the file it just wrote (so its own output becomes the body), then a fence-repair pass keyed on `line == "---"` that cannot see a glued fence and inserts a fence mid-page, so the next pass treats the tail as a new document. Un-double rule that worked: find the first frontmatter's close (accepting a glued fence), then the first `type:` line after it that has more keys (`title|description|tags|timestamp`) behind it — that is the second copy, and a stray `type:` in a code block has no key run. Keep the copy whose body is a **superset** (compare after stripping the dropped copy's own frontmatter), un-glue the fence, and prove `0` census losses before writing. Refuse to guess: if the halves differ materially, stop. ## 5. `.AGENTTRASH` is not vault content Every tool that walks the vault must exclude it — **both the scan set and the resolution index**. The link checker resolving links *into* the trash turns "live page points at a deleted page" into a false PASS, which is worse than a false alarm. The `docs_completeness` seam's `find` must carry `-not -path '*/.AGENTTRASH/*'` in both walks, or a trashed page demands an index row and a trashed page can satisfy the docs requirement for a live service. Trash is gitignored; move with plain `mv` into the dated dir. `git mv` into an ignored path stages a rename the next agent cannot see (`git restore --staged `). Never `rm`, never `git clean -f`. ## 6. Rewire + regenerate indexes mechanically ```bash grep -rnoE "\[\[(page-a|page-b|...)\]\]" --include="*.md" serverAppConfigs-docs | grep -v AGENTTRASH grep -rnoE "\((page-a|page-b|...)\.md\)" --include="*.md" serverAppConfigs-docs | grep -v AGENTTRASH ``` Both wikilinks and markdown path refs must be repointed (Research `See Also` lists and bench pages are where path refs hide). Then **regenerate** index rows with a script, never by hand. The generator bug to check for: presence tested as a **bare stem substring** — a page whose name prefixes another's (`frank-medium` inside `frank-medium-rocmfpx`) then silently never gets a row. Test the wikilink form `[[]]`. ## 7. Enforce the law with a seam `tests/docs_completeness.bats`: derive the live set instead of hardcoding it — an endpoint is live when `scripts/` holds **both** `.env` and `.service`; fail if any `-*.md` page exists, and guard against a vacuous derivation (`derived -ge 3`). A re-created era page then cannot pass tests. For the length hint, prefer **derived** exemptions (live endpoint, or a `retired-endpoint` tag: a campaign ledger that exists nowhere else is owned by the page that measured it) plus one documented per-page opt-out; never let a size ceiling force a ledger to be split across two owners. ## 8. Edit-tool hazard (costs the most time) Body rows beginning with `-` or `+` are op markers. The documented `+-` escape **writes a literal `+`** — do not use it. Safe rewrites inside an existing bullet: keep the `- ` opener line untouched and PUT only the indented continuation lines, or convert the note to a nested bullet whose body lines start with two/four spaces. If a wrapped `[[Two Word Page]]` shows up as an unresolved link, the target was hard-wrapped across lines — unwrap it. ## 9. Delegated writes: verify bytes, never narrative Measured in one campaign, all four failure modes live: - A subagent killed by the runtime limit yielded a 495-line owner page, a verified era table and a byte census — **the file did not exist** and its table was self-referential. - Another wrote its output to disk and died without reporting, leaving a rewritten `log.md`; the census showed it had dropped laws held nowhere else. - Verdict tables claimed "0 sole records / no contradictions" while missing the contradiction sitting in their own evidence row, and invented bullet IDs — one renamed a bullet to the **opposite** claim. - A scout called accurate docs "WRONG" because the docs said `wip: "down"`. Rules: after ANY delegated write run `wc`/`ls` + the orphan census yourself; re-grep every claimed holder before cutting a line; treat `cancelled`/timed-out yields as unverified no matter how concrete they read; and re-read the source bullet before trusting a table's ID. ## 10. Merge order (a long-lived branch under a moving main) 1. Commit your work in atomic groups **before** merging (a dirty tree cannot merge cleanly). 2. Inspect the incoming commits: `git log --oneline --stat HEAD..origin/main`. If they modify a page you deleted, **port their additions into the owner page**, then merge and resolve the modify/delete as `git rm`. 3. Resolve a content block with `write conflict://` (`@ours` / `@theirs` / explicit text), then **`git add` the file** — resolving content does not stage it. 4. **Re-probe live state after the merge, before you write any number.** A campaign that reported fleet shapes at the start was made stale by a mid-campaign reslice on main: probe `/slots` and `tr '\0' '\n' < /proc/$(pidof )/cmdline` again at the end, and let the live answer beat both your earlier probe and the docs. 5. `git * --no-verify` is forbidden here. Sweep dead paths in `AGENTS.md` (`grep -oE "[A-Za-z0-9._-]+\.md" AGENTS.md | sort -u`, then `find` each; hard-wrapped names are false positives — confirm with `ls`). ## 11. Verify, open, land - `./tests/run.sh` (full suite) + `python3 scripts/check-vault-links.py serverAppConfigs-docs` → must print `0 unresolved links`, and the OKF linter `0 violations`. - Env trims: prove value-neutrality mechanically (`git show HEAD:scripts/.env | grep -E '^[A-Z_]+=' | sort` vs working copy, diff empty). **`scripts/.env` is a deploy-planner path**: a comment-only trim makes the next service-scoped deploy restart a healthy endpoint. - Open the PR through the API with a 600-perm curl config (managed skill `gitea-api-pr-operations`), end the body with the `cwd:` / `session:` footer, then `xdg-open ` the instant it exists. Never merge without Frank's current explicit OK. - Docs-only change ⇒ **no deploy**. Fast-forward the deploy checkout only (`git -C ~/Projects/serverAppConfig merge --ff-only origin/main`). - Say what was NOT done, in the PR body: soft lint tallies left (missing `## Citations`), conflicts left standing, repo config that was never installed on a host.