Skip to content

docs(spec): correct six points of spec drift, and neutralise the --push blacklist claims - #66

Merged
lsimons merged 7 commits into
mainfrom
docs/spec-drift-sweep
Jul 31, 2026
Merged

docs(spec): correct six points of spec drift, and neutralise the --push blacklist claims#66
lsimons merged 7 commits into
mainfrom
docs/spec-drift-sweep

Conversation

@lsimons

@lsimons lsimons commented Jul 31, 2026

Copy link
Copy Markdown
Member

Docs only — nothing under src/ or tests/. Covers items 1, 4, 5, 6 and the meta
note of #54; items 2 and 3 (S07) land with the #50 fix. In every case the code is
what ships and the spec was wrong.
Every claim was re-verified against the code
before the spec was edited; all of #54's claims in this scope held up.

1. S30 + S09 — macro carrier serialisation

_render_confluence_macro emits a :::confluence-macro {name="…"} fenced div with
the depth-aware colon scheme, not the ```confluence-xml fence S30 claimed.
_render_raw_block is what emits that fence — for RawBlock(format="confluence-storage")
and any other format the writer does not recognise, which S30 also did not say.

S09 separately described a pre-IR markdownify / ```{=confluence} strategy.
That form now survives only in a comment in ai/rewrite.py and a bundled skill;
S09 now describes the IR path, and gained the S28/S29/S30 links it was missing.
The reader side was already correct and is untouched.

4. S09 — config schema and output_dir

spaces: is registered as section="confluence", collection="spaces" — nested,
not top level. S19 already documented it correctly, so S09 was the outlier rather
than a competing source of truth.

sync-space does not read output_dir from config at all: it uses --output, or
default_output_for_space()'s git-origin-URL heuristic. mdd search is the only
reader.
Now stated explicitly.

5. S39 — Confluence .mddignore wiring is no longer deferred

It has landed: confluence/sync/mddignore.py bridges the matcher to the page-title
hierarchy via synthetic rel-paths, wired to --ignore and --prune-ignored. S39
listed it as follow-up in five places, including a self-contradiction where it said
Confluence supports --prune-ignored and then said Confluence support was pending.
Lucid's follow-up statements are deliberately left alone.

6. S09 + S14 — the undocumented body-safety guards

_check_body_safety refuses an empty body, and a body under 10% of the remote
length, overridable with --allow-empty / --allow-shrink. These are the main
thing standing between a bad local file and a wiped page and were in no spec.

Documented in S09, where the flags and the implementation live, with a
cross-reference from S14 step 4f rather than duplicating the thresholds in two
places. Tracing the sync path turned up a detail the issue did not have:
sync-space's push calls update_page with no allow_empty/allow_shrink
kwargs, so sync always applies the strict guard with no override available, and a
trip becomes a recorded push failure in the run summary rather than a crash.

S14 step 4c said archive "update[s] the export header"; the code only flips
frontmatter status. Fixed to match, pointing at S27's implementation note.

Meta — S06's counts

"38 specs, 14 research notes" was wrong on both: it is 37 and 15. Rather than
swap in today's numbers, the count is dropped — this sentence is the thing that
just drifted, it will drift again (this batch is touching specs right now), and the
point of the sentence is "there is a lot of prior art and none of it is operator
docs", not a census.

Beyond #54: two things found on the way

S27 contradicted itself. Its Tests bullet said archive "flips frontmatter status
and export header", while its own implementation note correctly says the header is
never touched. The code agrees with the note. Fixed, in its own commit — S27 is not
one of #54's points. Worth having, because S14 now points at S27 as the authority
for exactly this.

Eight --push-triggered blacklist claims neutralised. The #50 fix moves
Confluence blacklist enforcement to the sync/export entry points, so a
blacklisted space aborts regardless of --push. A sweep for the claim rather than
the filename found it in three specs: S14 (three places), S09 (including a restated
copy of S07 §2.6's "local-only export is unrestricted"), and S39 (five places — its
whole precedence argument rested on a pull-vs-push contrast, now reframed onto
coarse-gate-versus-fine-filter, which holds either way).

None of these re-specify the new mechanism; each drops the trigger claim or defers
to S07, so this does not collide with the #50 branch.

Gates

mise run spec-check exit 0 · mise run ci green, 2415 passed, 88.03% coverage.

Refs #54

Co-Authored-By: lsimons-bot bot@leosimons.com

lsimons and others added 7 commits July 31, 2026 11:24
S30 said the generic ConfluenceMacro carrier emits a ```confluence-xml
fence; the writer actually emits a :::confluence-macro {...} fenced div
(src/mdd/markdown/ir/writer/blocks.py, _render_confluence_macro). The
confluence-xml fence is real but belongs to RawBlock(format=
"confluence-storage") (and any other unrecognised RawBlock format),
handled by _render_raw_block.

S09's "Markdown ↔ storage XHTML" section separately described a
pre-IR markdownify/{=confluence}-fence conversion strategy that no
longer exists in the writer (the {=confluence} shape now survives only
in an ai/rewrite.py comment and a bundled skill). Conversion goes
through the document IR (S28/S29/S30); corrected S09 to say so and
added those specs to "Related upstream specs".

Refs #54
Co-Authored-By: lsimons-bot <bot@leosimons.com>
Assisted-by: Claude:claude-sonnet-5
S09 documented spaces: at the top level of confluence.yaml.
src/mdd/search/sources.py registers it as section="confluence",
collection="spaces" — i.e. nested under confluence:. Corrected the
example config and cross-checked against S19, which already had the
nested shape right.

S09 also implied sync-space reads output_dir from config. It does
not: export-page takes --output directly, and sync-space either takes
--output or falls back to default_output_for_space()'s origin-URL
heuristic (src/mdd/confluence/export.py). Only mdd search consumes
output_dir, to discover configured mirror roots. Documented which
command reads the key and how sync-space actually picks its output
directory.

Refs #54
Co-Authored-By: lsimons-bot <bot@leosimons.com>
Assisted-by: Claude:claude-sonnet-5
S39's "Out of scope" section listed Confluence .mddignore wiring as
deferred follow-up work. It has landed: src/mdd/confluence/sync/
mddignore.py, wired into sync-space with --ignore (action="append")
and --prune-ignored (rejected together with --read-only at parse
time, per the argparse help text and _check_prune_readonly).

Added a "Confluence wiring" section describing how the matcher applies
to a page-title hierarchy rather than filesystem paths: synthetic
rel-paths mirror export.build_path_map's tree-walk and sanitisation,
then get the same is_ignored / prune_dir treatment SharePoint uses.
Updated the flag sections, the Related upstream specs list, and the
Out of scope bullet accordingly. Lucid wiring remains real follow-up
work and is left as-is.

Refs #54
Co-Authored-By: lsimons-bot <bot@leosimons.com>
Assisted-by: Claude:claude-sonnet-5
_check_body_safety (src/mdd/confluence/update.py) refuses an empty
body and refuses a body under 10% of the remote body's length,
overridable per-invocation with --allow-empty / --allow-shrink
(argparse flags on update-page). Neither S09 nor S14 mentioned either
guard, despite it being the main check standing between a bad local
file and a wiped Confluence page.

Documented both guards in S09 under update-page, since that is where
the implementation and the CLI flags live; cross-referenced from S14
step 4f, since sync's content-push reuses the same update_page() call
with neither flag set (push.py calls it with only yes=True), so a
sync run has no way to override either guard and records a tripped
guard as a push failure in the run summary.

Also fixed S14 step 4c, which said archive/unarchive "update[s] the
export header" — the code only flips frontmatter status; the header
is untouched. S27 already carries a note in its own implementation
section explaining why (mdd.confluence.header has no archived variant
and _apply_one_archive never touches it), so S14 now states the
correct behaviour and points at that note instead of duplicating the
explanation. Added S27 to S14's "Related upstream specs".

Refs #54
Co-Authored-By: lsimons-bot <bot@leosimons.com>
Assisted-by: Claude:claude-sonnet-5
S06's introduction said "38 specs, 14 research notes". Actual counts
today: 37 files matching docs/spec/S*.md, 15 matching docs/research/
R*.md (000-*.md index files excluded from both, matching the naming
convention in 000-specs.md). The issue's reported 37/15 was correct;
the spec's 38/14 was not.

Since this is exactly the kind of number that drifts every time a
spec or research note is added or renumbered — as this very report
demonstrates — reworded the sentence to not carry a count at all
("dozens of specs, a similarly-sized pile of research notes") rather
than replace one stale number with another that will be stale again
soon.

Refs #54
Co-Authored-By: lsimons-bot <bot@leosimons.com>
Assisted-by: Claude:claude-sonnet-5
… S07 rewrite

S07 §2.4/§2.6 are being rewritten (parallel PR) to describe the
confidentiality blacklist as entry-point gating rather than
--push-only gating: a blacklisted Confluence space or SharePoint site
aborts the whole command, not just the push step. Several lines in
S09, S14, and S39 asserted the old --push-only trigger explicitly
enough to contradict the rewritten S07 once both land.

Neutralised each one to describe the blacklist gate's role without
asserting when it fires, deferring to S07 as the single source of
truth for the trigger condition:

- S09 "Confidentiality" paragraph no longer says "--push (...) consults
  the blacklist" / "local-only export is unrestricted".
- S14's two mirror-backend-push bullets and its Related-upstream-specs
  entry no longer tie the blacklist gate to the push step specifically.
- S39's five blacklist mentions (intro, "Precedence vs. utils/
  blacklist.py", the "runs before the blacklist" design note, Related-
  upstream-specs, and the Out-of-scope bullet) no longer describe the
  blacklist as a "push-side"/"destination-push" gate contrasted with
  .mddignore's "pull-side"/"source-pull" filtering. Reframed the real,
  trigger-independent distinction instead: the blacklist is a coarse
  whole-space/whole-site gate, .mddignore is a fine-grained per-path
  filter; the two do not interact regardless of when either fires.

Refs #54
Co-Authored-By: lsimons-bot <bot@leosimons.com>
Assisted-by: Claude:claude-sonnet-5
S27's own Requirements-level "Tests" bullet said archive "flips
frontmatter status and export header" (unarchive flips them back),
while its Implementation Notes §"Export header for archive /
unarchive" (further down the same file) correctly says the header is
never touched — mdd.confluence.header has no archived variant and
_apply_one_archive only flips frontmatter status — and frames that as
the deliberate minimum-viable choice. The code agrees with the
Implementation Notes.

Corrected the Tests bullet to match, with a pointer to the existing
correct explanation rather than restating it. No change to the
Implementation Notes section itself; it was already right.

Co-Authored-By: lsimons-bot <bot@leosimons.com>
Assisted-by: Claude:claude-sonnet-5
@lsimons
lsimons merged commit 774ea8b into main Jul 31, 2026
7 checks passed
@lsimons
lsimons deleted the docs/spec-drift-sweep branch July 31, 2026 11:16
@lsimons lsimons added this to MDD Jul 31, 2026
@github-project-automation github-project-automation Bot moved this to Done in MDD Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant