Skip to content

docs: prune docs/ to guides, deployment, records, assets (#1357) - #1395

Merged
radandevist merged 18 commits into
developfrom
lane/wt-1357
Aug 25, 2026
Merged

docs: prune docs/ to guides, deployment, records, assets (#1357)#1395
radandevist merged 18 commits into
developfrom
lane/wt-1357

Conversation

@radandevist

@radandevist radandevist commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #1357

What this does

Prunes docs/ to exactly four directories and wires a repo-wide dead-link guard so the prune is provably safe:

docs/
├── guides/       standing rules AGENTS.md links (maintained)
├── deployment/   production operations (separate by owner decision 2026-08-25)
├── records/      dated, write-once evidence, YYYY-MM-DD-<type>-<topic>.md
└── assets/

Inventory method

The move/delete decision is mechanical and reproducible: a committed script (packages/scripts-ts/src/audit-docs-prune.ts, commit 1) scans every file outside the four target directories, resolves who references it among the survival surfaces (AGENTS.md, DESIGN.md, docs/guides, docs/deployment, apps/, packages/, .github, the justfile — deliberately NOT docs/README.md indexes or the retired archive-guard manifest), and emits the inventory committed as evidence at docs/records/2026-08-25-audit-docs-prune.md.

The inventory carries two independent freshness controls:

  1. Byte equality: audit-docs-prune.ts --check regenerates the record from the merge-base tree and exits non-zero if the committed copy differs byte-for-byte. Enforced twice — in the required docs-archive-gate CI job and locally in recipe ci-doc-links (manifest reconciled through just ci-drift). Paired proof: editing one row of the record makes --check exit 1 naming the file; reverting restores exit 0.
  2. Git fidelity (added in round 2): before the byte comparison, --check cross-validates every rendered row against git diff -M --name-status <merge-base>..HEAD. Git's rename detection is an INDEPENDENT source of truth for what actually moved, so a decision-table omission — a real rename left unmapped, hence rendered as a deletion — now fails loudly instead of regenerating identically-wrong evidence. This closes the structural blind spot of control 1 alone: regenerating from the same wrong table matches byte-for-byte and proves nothing.

Moved vs deleted

93 candidate files: 11 moved to docs/records/, 1 kept in place, 81 deleted (counts after merging origin/develop — see below).

  • Moved (11): ten survivors each referenced by at least one survival surface, plus two deliberate exceptions documented below; every survivor got a canonical dated record name, e.g. docs/implementation-plans/identity-scoped-tenant-cookie.mddocs/records/2026-01-31-plan-identity-scoped-tenant-cookie.md, and every link to it was rewritten repo-wide (AGENTS.md, DESIGN.md, guides, app code comments).
  • Kept in place (1): docs/README.md itself, rewritten as the one-page filing index.
  • Deleted (81): everything unreferenced — all of docs/archive/ (history lives in git and the old-front-final tag), plus superpowers/, reviews/, front-migration/, implementation-plans/, spikes/, roadmaps/, misc/, audits/, and one orphan under analysis/. No content edits beyond link rewrites on survivors.

Correction to the first version of this description: it said 9 moved / 82 deleted. That under-counted by exactly one file — docs/superpowers/specs/2026-08-25-paid-modules-design.md. Git shows it as an R100 rename to docs/records/2026-08-25-spec-open-core-paid-modules.md, but the generator's hand-kept MOVES table omitted it, so the committed inventory rendered it as a deletion and byte-equality --check reproduced the same wrong answer. Round-2 review caught it; the fix is under "Round 2 fix" below.

Honest provenance for the exception: no survival surface references the paid-modules spec. It is preserved because #1355 merged it into develop while this lane was in flight, and the lane deliberately keeps work develop already merged rather than deleting it in the prune. Its record name carries -open-core- through an explicit topic: override in the decision table (deriveTopic() alone would yield paid-modules, which is not the name that landed). The committed inventory's Notes section states this plainly.

Mid-flight merges, handled explicitly: develop merged #1355 while this lane branched (adding the paid-modules spec back under a directory this lane deletes) and later #1385 (adding the bulk-actions spec, docs/superpowers/specs/2026-08-25-820-bulk-actions-design.md). This branch merged origin/develop (merge b068baa). The #820 spec follows the same precedent as paid-modules — no survival surface references it either, and it is preserved deliberately, landing at docs/records/2026-08-25-spec-820-bulk-actions.md. Develop also MODIFIED the already-pruned DLQ triage design (docs/analysis/2026-08-24-dlq-unclassified-triage-design.md); it stays deleted per the mechanical rule (unreferenced by any survival surface, history preserved in git). With develop as ancestor, the regenerated inventory lists both mid-flight specs as their own rows (93 candidates total); both --check controls pin the committed record to that regeneration AND to what git actually renamed.

Round 2 fix

Adversarial review round 2 returned two MAJOR findings; both fixed with RED→GREEN proof:

  1. Miscounted inventory: a real rename classified as a delete (finding 1). The fidelity gate is new code (listGitDocRenames + assertGitFidelity wired into --check ahead of the byte comparison).
    • RED (against the old committed inventory and the unmapped table), captured as produced in .dump/red-transcript-r2.txt:

      $ node packages/scripts-ts/src/audit-docs-prune.ts --check
      (old inventory still committed: 92 candidates — 9 moved / 1 kept / 82 deleted; MOVES table omits paid-modules)
      Inventory disagrees with git diff -M (f2811483ae335c2a76dceccc61f7e5c6f64651aa..HEAD) on 1 row(s):
        - docs/superpowers/specs/2026-08-25-paid-modules-design.md: git records a rename to docs/records/2026-08-25-spec-open-core-paid-modules.md, but the inventory classifies it as "delete".
      exit=1
      
    • GREEN: the MOVES table gained the paid-modules row with its explicit topic, the inventory regenerated to the true counts (then 10 moved / 81 deleted / 1 kept), and --check exits 0 on the committed tip. Fix commit: 7bbf128.

    • Live proof on real data: when this lane later merged origin/develop, the SAME gate flagged a REAL destination mismatch on the newly arrived #820: restore bulk management actions in the staff-users selection mode #1385 spec before any wrong evidence could be committed — the mapping named …-spec-820-bulk-actions.md while the merge auto-planted …-820-bulk-actions-design.md. Renaming the planted file to the canonical name turned the gate green (c0a507c).

  2. False provenance claim (finding 2). The previous description said paid-modules survives because surfaces reference it. Nothing does. Corrected here and in the inventory Notes: the preservation reason is the mid-flight docs(specs): open-core paid modules design #1355 merge, stated without embellishment.

A regression suite pins the gate end-to-end: packages/scripts-ts/src/audit-docs-prune.test.ts (commit 7b8ce98) runs the REAL generator inside throwaway git repositories across four fixtures — (1) RED replay: rename recorded as delete fails --check naming the row; (2) a correctly mapped move passes both controls; (3) inverse lie: an inventory claiming a move git does not show fails; (4) a destination mismatch between mapping and git's rename target fails. All four pass on the committed tip.

Local gates re-run before this update: full scripts-ts vitest suite (pre-merge run: 27 files, 689 passed | 2 skipped; re-run green after the develop merge), including these four fixtures and the doc-links guard fixtures — plus node packages/scripts-ts/src/audit-docs-prune.ts --check (exit 0 post-merge), the doc-links guard itself (59 files, 133 links), and just ci-drift (7/7). A formatting-only commit (2cef5aa) satisfies oxfmt --check on the new fixture file.

Round 1 fix

Review round 1 returned CHANGES_REQUIRED (stale evidence, unenforced reproducibility control). All findings fixed, one commit each:

  1. Inventory regenerated at the tip — 92 candidates including the merged docs(specs): open-core paid modules design #1355 paid-modules row; --check exits 0 (ea66f0c).
  2. --check wired into docs-archive-gate + ci-doc-links, classifier pattern wakes the gate on generator edits, manifest reconciled honestly (6e744c1).
  3. Guard scans untracked non-ignored files; new fixture test RED vs old code; RED transcript rewritten from a real run (8412095 + 73fddcf).
  4. Code-surface docs/… literal scanner added with fixture test and paired RED (eb2fc37).

Round 1 fix implemented by Ox Alpha via Nous Portal (max, jcode). Round 2 fix implemented by Ox Alpha (stealth/ox-alpha via Nous Portal, max, jcode).

docs-archive gate retarget

  • Old gate: check-archive-records.ts enumerated files that were supposed to exist under docs/archive/ and failed when they went missing. With the archive deleted it would have asserted an empty set forever — vacuously green. Per the issue's rule ("never left passing vacuously") the script, its test, and their manifest entries are deleted, not loosened.
  • New gate: the same workflow (.github/workflows/docs-archive.yml, required context unchanged) now runs three things: the check-doc-links.ts guard, its fixture suite, and the prune-inventory freshness checks above.
  • Guard scope: every tracked *.md in the whole repo plus untracked non-ignored working-tree files (git ls-files --others --exclude-standard), so a broken link planted but never staged still fails a local run. On top of Markdown links, the guard scans the code surfaces (apps/, packages/, .github/, the justfile, AGENTS.md, DESIGN.md) for docs/… path literals whose target does not exist — a code comment naming a deleted doc is as broken as a dead link. docs/records/ bodies stay exempt (write-once evidence, same policy the old archive carried); fenced code, inline code spans, URLs, branch names and test/spec fixtures are out of scope so examples cannot trip it. Wired into the local gate identically: recipe ci-doc-links, manifest entries reconciled through just ci-drift.
  • RED/GREEN proof: committed at docs/records/2026-08-25-analysis-doc-links-guard-proof.md. GREEN on the clean tree (58 files scanned, 132 relative links checked, exit 0). RED from a real run, captured as written:
$ echo 'broken [link](./no-such-target.md) here.' > docs/guides/_tmp-red-proof-untracked.md
$ node packages/scripts-ts/src/check-doc-links.ts
1 broken relative link(s) in tracked or untracked non-ignored Markdown:
  docs/guides/_tmp-red-proof-untracked.md:1: -> docs/guides/no-such-target.md
exit=1

with an exempt frozen-record link planted in the same run staying silent, and a paired literal-scan RED (broken docs/… literal in apps/api/Lib/AppEnvironment.cs → exit 1 naming file and line). Both plantings were removed immediately after capture; the clean tree went green again. The fixture suite pins these modes permanently (13 tests), including the untracked-file case (RED against the old tracked-only code) and the literal-scan cases.

Public/private rule

Business/licensing/strategy content never enters this public repo — it lives in the owner's private notes.

Ruleset note

The required check context keeps the name docs-archive-gate: only the job's steps changed, not its name: (conditional docs-archive-gate on pull_request/merge_group, docs-archive-push-check on push). No rename, so the branch ruleset keeps matching and no PR gets spuriously blocked.

Known unrelated CI failure

Build e2e images fails due to the org-move issue (#1396). Not caused by this PR.


Implementer: Ox Alpha (stealth/ox-alpha via Nous Portal, max, jcode)

Unverified

  • Local e2e stack intentionally NOT run per captain policy (2026-08-23): CI runs front-e2e on this PR and that is the accepted evidence.
  • Full API integration suite and full front unit suite not run locally for this docs-only lane; CI covers both on the PR.
  • All local claims in this update ran against tip 2cef5aa (scripts-ts suites, prune --check, just ci-drift, doc-links guard); the remaining evidence is this PR's own checks at that tip.

Add packages/scripts-ts/src/audit-docs-prune.ts, which enumerates every tracked file under docs/ outside guides/, deployment/, assets/, sweeps exactly the #1357 survival surfaces (AGENTS.md, DESIGN.md, docs/guides, docs/deployment, apps/, packages/, .github, justfile; root README.md deliberately excluded, as are docs/README.md and the ci-gate manifest), applies a reviewed move/delete decision table, and renders docs/records/2026-08-25-audit-docs-prune.md.

Reproducibility: the audit reads a git revision - by default the merge-base of origin/develop and HEAD (the pre-prune tree being pruned), overridable with --rev <sha> - so the evidence stays byte-for-byte regenerable after the prune lands; --check enforces equality against the copy committed on this branch.

Inventory result: 91 candidates - 9 moved to docs/records/, 81 deleted, 1 kept (docs/README.md, rewritten later in this change).
git mv each surviving record to the flattened YYYY-MM-DD-<type>-<topic>.md naming and rewrite every reference across the repo (AGENTS.md, DESIGN.md, guides, deployment docs, front source comments, shared-ts constants).

- archived gray-ui migration design -> records (referenced by DESIGN.md source annotations)
- deployment-guide -> records (superseded-by pointer from production-deployment-design.md)
- old-front screen reviews -> records (bulk-action guide, front flags.ts pointers)
- parity contract -> records (locked contract default in conventions + routes)
- identity-scoped tenant cookie plan -> records (shared-ts constants pointer)
- marketing landing bands + epic-d publishing specs -> records (AGENTS.md/DESIGN.md pointers)
- kiota redirect-header-leak audit -> records (dependency-health guide + redirect-scrub test)

No content edits beyond link rewrites. The inventory evidence keeps the pre-prune source paths; the decision-table script is intentionally untouched here.
docs/README.md now describes the target layout - guides/, deployment/, records/ (YYYY-MM-DD-<type>-<topic>.md), assets/ - with one filing rule per surface and records as the write-once home for specs, plans, reviews, audits, spikes and analyses (superpowers skills included). AGENTS.md's Documentation Organization section states the same closed set.

Every normative-surface pointer into directories that commit 4 deletes is rewritten now: archive references in AGENTS.md, README.md, five guides and production-deployment-design.md now point at git history or the old-front-final tag instead of docs/archive/old-front. Landing this before the deletions keeps every intermediate commit free of dangling pointers from maintained surfaces.
… links

Delete the 81 unreferenced records under docs/archive/, analysis/, front-migration/, implementation-plans/, misc/, reviews/, roadmaps/, spikes/ and superpowers/ per the committed inventory. Remove the archive-records guard (check-archive-records.ts + its fixture suite) whose entire subject tree is gone.

Retarget .github/workflows/docs-archive.yml in place so the required docs-archive-gate context keeps reporting for every PR: the changes classifier pattern, the push paths filter and the heavy job now run the new repo-wide dead-relative-link guard (check-doc-links.ts) over tracked Markdown, with docs/records/ bodies exempt as write-once evidence. Reconcile ci-gate-manifest.json with the retargeted steps, swap package.json's check:docs-archive-records for check:doc-links, and replace the justfile ci-docs-archive-records recipe with ci-doc-links inside just ci.

Housekeeping tied to the same deletions: env.test.ts's dead-env-var sweep exempts docs/records/** instead of the removed docs/archive/**; the oxlint-disables audit drops its docs/superpowers/plans ignore; project-closure-v1.json keeps requiring docs-archive-gate, which still reports. Fix three latent broken relative links the new guard caught on its first run (apps/front/e2e/README.md twice, guides/front/conventions.md).
Capture the guard's real behavior as committed evidence: GREEN (exit 0, 56 Markdown files, 132 relative links) on the clean tree, then RED (exit 1) naming a planted broken link in a guide while an exempt docs/records/ body with an equally dead link stayed silent in the same run. Both planted files were removed immediately after capture; only this record remains.

The standing regression net is packages/scripts-ts/src/check-doc-links.test.ts, which executes the real guard against throwaway git repositories and pins every documented failure mode; it runs in just ci (ci-doc-links) and under the retargeted docs-archive-gate.
…rged paid-modules spec)

Round-1 review CRITICAL: the committed record was generated before develop
merged #1355, so `audit-docs-prune.ts --check\) failed at the tip — the
inventory omitted the very row (docs/superpowers/specs/2026-08-25-
paid-modules-design.md) the tip commit lands. Regenerated mechanically:
92 candidates — 9 moved / 1 kept / 82 deleted. `--check` now exits 0 at
this commit (byte equality against a fresh regeneration).
Round-1 review CRITICAL: `audit-docs-prune.ts --check` — the switch that
advertises byte equality between the committed inventory and its generator —
was wired into no workflow and no local recipe, so staleness could never be
caught by CI (the committed record was, in fact, already stale at the
reviewed tip).

- docs-archive.yml: new step 'Check prune inventory freshness (--check)'
  runs audit-docs-prune.ts --check after the doc-links guard; a stale
  record now fails the required docs-archive-gate context.
- The changes classifier pattern gains the audit script itself so an edit
  to the generator wakes the gate; pinned by a new ci-changed-paths test
  case extracted from the real workflow YAML (RED vs the old pattern).
- Local mirror: just ci-doc-links runs the identical --check invocation.
- Manifest reconciled honestly via check-ci-drift (two hash updates + one
  new entry whose mirrors are the identical commands).

Paired proof (real run): committing an edit to one row of the record makes
--check exit 1 naming the file; reverting restores exit 0.
…view r1 MAJOR)

The r1 RED proof planted a broken link as an UNTRACKED file and the
guard — listing `git ls-files` only — stayed green (exit 0), so the
proof was not reproducible as written. The guard now scans tracked
files PLUS `git ls-files --others --exclude-standard` (untracked,
non-ignored), so a local pre-commit run catches an unstaged broken
link; gitignored files remain out of scope.

Fixture suite: kept all 7 existing tests, added 2 for the untracked
case — the failing-link test is RED against the old guard (verified:
old code exits 0 where the test demands 1).
…iew r1 MEDIUM)

The prune inventory counted apps/, packages/, .github/, justfile, AGENTS.md
and DESIGN.md among the survival surfaces, but the dead-link guard only
scanned *.md — a code comment or workflow step naming a moved/deleted doc
went unguarded.

The guard now literal-scans the same code surfaces for docs/... paths whose
target does not exist (exact file, .md completion, or directory — the same
existence model as the link scan). Out of scope by design: URLs, branch
names without a dotted segment, directory mentions, test/spec fixtures, and
audit-docs-prune.ts's pre-prune decision table. Markdown surfaces scanned
here get the same fenced-block stripping as the link scan.

Fixture tests: existing literals pass; a broken literal fails naming
file:line; URLs/branches/test files stay out; audit table exempt. Paired
RED: with the finding-3 code, a planted broken literal in
apps/api/Lib/AppEnvironment.cs kept exit 0 (58 md / 132 links OK); with this
change it fails naming apps/api/Lib/AppEnvironment.cs:<line>; reverted,
clean tree green.
…nt clean

- Notes now state #1355 merged into develop after branching and that this
  lane lands its content at docs/records/2026-08-25-spec-open-core-paid-modules.md
- Regenerated record via audit-docs-prune.ts itself (92 candidates unchanged)
- unicorn/prefer-string-starts-ends-with: entry.endsWith('/')
…iles, untracked RED, literal scan, 13 tests)
radandevist added a commit that referenced this pull request Aug 25, 2026
…866)

Round-1 finding 5: the design note moves out of docs/analysis ahead of the
#1357/#1395 pruning (guides/deployment/records/assets only), landing at its
final name docs/records/2026-08-25-analysis-email-log-actor.md so the two PRs
do not conflict.

Content fixes while moving (records are dated, not retro-edited — these land
pre-merge so the record ships accurate):
- §1 quotes K-6 with the real table name email_log_evidence_events (was
  email_log_events).
- New paragraph records WHY this table uses two columns (actor_kind +
  actor_id) where the job_dead_letter_events precedent used one (detected_by):
  the kind/correlation split keeps both halves machine-checkable end to end.
- §6 records the round-1 amendments (value type, CHECK constraints, explicit
  replay index) applied before merge.

Refs #866.
…ename detection (review r2 MAJOR)

The byte-equality --check could not see a misclassification the
generator itself shared: paid-modules was an R100 rename to
docs/records/ but MOVES omitted it, so the inventory said delete and
regen == committed == wrong (9 moved / 82 deleted instead of 10/81).

- derive ground truth from `git diff -M --name-status` merge-base..HEAD;
  --check fails naming any row where classification, existence, or
  destination disagrees with git's renames (generation fails too)
- add the paid-modules row to MOVES with explicit topic
  'open-core-paid-modules' (deriveTopic alone would name it -paid-modules);
  regenerate the inventory: 92 candidates — 10 moved, 1 kept, 81 deleted
- rewrite the Notes prose honestly: no surface references it; preserved
  as a deliberate exception because #1355 had already merged it
- new fixture suite executes the real script against throwaway repos:
  RED replay of the paid-modules defect plus inverse-lie and
  destination-mismatch cases

RED transcript (.dump/red-transcript-r2.txt): old committed inventory +
fidelity gate -> exit 1 naming the row. GREEN: this commit.
Resolve docs conflicts per the prune's mechanical rule: #1357's survival
rule plus the paid-modules precedent. The #820 bulk-actions spec (added to
develop mid-flight, same shape as paid-modules) moves to
docs/records/2026-08-25-spec-820-bulk-actions-design.md via its own MOVES
row added next commit; the modified-but-unreferenced DLQ triage design
stays deleted (history preserved in git, matching the committed audit).
…nerate inventory (93 candidates, 11 moved / 81 deleted / 1 kept)
@radandevist
radandevist merged commit 86f3acb into develop Aug 25, 2026
26 checks passed
@radandevist
radandevist deleted the lane/wt-1357 branch August 25, 2026 14:09
radandevist added a commit that referenced this pull request Aug 25, 2026
…lands

The audit resolved its evidence tree via merge-base(origin/develop, HEAD),
which equals HEAD once the #1395 prune lands, making --check fail on any
docs/-touching lane. Walk origin/develop's first-parent history to the
youngest tree still carrying every decision-table source instead (bounded,
--rev override kept), and pin the rename-fidelity check to that same rev.

Covered by two new tests: a post-prune lane passes --check, and a tampered
record still fails. Regenerated the inventory record (94 rows: adds the
email-log-actor analysis delete).
radandevist added a commit that referenced this pull request Aug 25, 2026
…m committed history alone (Closes #1425, rescope after #1422) (#1426)

fix(docs-archive): prune-inventory check derives its scope from committed history, never from a fetched ref

The docs-prune inventory check (packages/scripts-ts/src/audit-docs-prune.ts) went red
develop-wide after #1395 + #1420 because it compared against an ambient origin/develop ref
that differs between PR checkouts, push events and a plain clone. The audited pre-prune
tree is now derived from committed history alone — the youngest ancestor of the
record-introducing commit whose tree still carries every decision-table source — so the
verdict is identical on every event and on a checkout that never fetched the ref. The
misleading "delete" row for docs/records/2026-08-25-analysis-email-log-actor.md (a file
#1389 MOVED into the protected destination) is fixed at the cause: the candidate filter
excludes docs/records/, the move target itself. A push-event fixture pins the single-squash
shape. The ci-gate-manifest entry changed only its reason text; mirror invocation and gate
hash are untouched.

Proof: RED transcript (.dump/red-rescope-b.log) on the #1389 shape before the scope fix,
GREEN after; --check exit 0 with the exact CI invocation at the tip; scripts-ts suite
693 passed under the heavy lock.

Closes #1425

Model: Ox Alpha via Nous Portal (jcode), effort max. Reviewer: tencent/hy3:free
(adversarial round 1, APPROVED at d10a217 — 3 notes: a deletion FROM docs/records/ is
covered by the protected-destination guard, not this check; the "+2 skipped" claim in the
lane's report was stale (0 skipped at tip); manifest reason-only change confirmed).
Unverified: nothing beyond CI and the reviewer's re-run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: prune to guides/ deployment/ records/ assets/ — delete archive and one-off record dirs, one-page README, dead-link guard

1 participant