mozilla / mozilla/markfluence

markfluence status: show the local page tree and whether Confluence has moved on

Open
#148 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Go
Stars
2
Forks
0
Avg merge
2h 8m
Merged PRs (30d)
49

Description

A read-only command that shows the page tree a project declares and, for each page, whether the local file and the Confluence page still agree. The thing markfluence cannot answer today at all is the reverse direction — the page is ahead of your local copy — and making that answerable needs one small piece of recorded state, which is most of this issue.

Motivation

Three separate wants meet in one command.

Seeing the tree. Nothing prints the page hierarchy a project declares. children prints the live tree, which needs credentials and answers a different question ("what is under this node in Confluence?"). Reading parent: across a tree of files, or across a markfluence.yaml pages: block (#139), is currently a manual exercise. This is also the want that made a nested pages: hierarchy tempting during #139's design, and the reason it was rejected there: storing the tree in order to display the tree is the expensive way round, and a read-only view is the cheap one.

Auditing. #139 deliberately makes several things invisible in normal use, because every diagnostic there is scoped to the files an invocation names: a pages: entry naming no file on disk, a file under the root registered in neither location, an entry whose page_id resolves to nothing. Each is legitimate in some situation (a branch that deleted a file, a draft, a sparse checkout), which is exactly why they must not be errors — and why there should be somewhere to ask.

Drift. "Would update do anything, and is anyone else editing these pages?"

What it shows

Sketch, not a specification:

$ markfluence status
root: /home/me/myrepo   space: ENG

  12345  docs/engineering-docs.md      Engineering Docs
  12346  ├─ docs/deploy-runbook.md     Deploy Runbook       modified
  12347  └─ docs/on-call-handbook.md   On-call Handbook     page changed (v7, you published v5)

      —  docs/draft.md                 (unmanaged)
      —  docs/removed.md               entry names no file

4 pages, 1 modified, 1 changed in Confluence, 1 unmanaged, 1 dangling entry.

A page whose parent: is an id rather than an in-project path is a root of the local forest, annotated with that id — the tree is a forest, not a single tree, and a page parented to a Cloud folder or to a page outside the project is normal.

Offline by default, network by opt-in

The tree, published-vs-unpublished, unmanaged files and dangling entries all come from disk. Requiring a token to look at a tree would be wrong, and the precedent is check, which is its own command precisely because it is the offline verb — the first whose run() never constructs a client.ConfluenceClient. A flag (--remote, --fetch, name TBD) adds one GET per page for the drift columns.

What "changed" can honestly mean

This is the part to get right, because two readings of it differ enormously in cost and in truthfulness.

Comparing content is a trap, and it is one docs/confluence/ already warns about. The converter targets semantic, not byte-for-byte, equivalence to storage. Worse, any save through the Confluence editor re-serializes the body through ADF — that is the coalesceSplitMarks case, where a mark markfluence nested around a link comes back split per text run. So a byte comparison of markfluence's output against the live storage would report "changed" for pages nobody touched. docs/confluence/ records "body.storage proves only what was stored, never what takes effect" as one of two traps that have each already produced a confident wrong conclusion. A semantic comparison would need a normalizer nobody has written, and it would become a second source of truth about what equivalence means.

Comparing timestamps is available today and is not sufficient on its own. version.createdAt against the file's mtime is exactly what update's mtime skip uses (cmd/update/update.go:250-258), so it faithfully reports what update would do — which is a good contract for one column. What it cannot do is tell you why the page is newer, and the failure is not marginal: publishing sets createdAt to now while the file's mtime is from whenever it was saved, so immediately after a successful update, every page is newer than its file. A "page is ahead" column built on timestamps alone would light up for the wrong reason most of the time.

Tracking state in a content property

Record the page version markfluence last published, as a content property on the page. Then "live version > recorded version" means precisely someone other than markfluence has written to this page since markfluence last did — no timestamp skew, no ambiguity with your own publishes, and a version number increments only when somebody actually saves.

This is an existing pattern applied to the body rather than a new idea. An attachment already records a SHA-256 in its comment, and that is exactly how client.SyncAttachments decides skip-vs-update; the body has had no equivalent. The machinery exists too: SetContentProperty/ListContentProperties, with SetContentProperty already carrying a retry-once on top because "a versioned PUT is not as idempotent as its method", and page_width already storing two properties per page (internal/pagewidth).

Why a version number rather than a hash of the body. A hash is more granular and is worse here: an ADF round-trip changes the stored bytes when someone opens the editor and saves without editing anything, so a hash reports "changed" where a version number reports, accurately, "somebody saved it". A hash is also strictly more state to keep correct. If a hash is ever wanted, note that it would compare Confluence's bytes now against Confluence's bytes when markfluence last wrote — both sides Confluence's own serialization — which sidesteps the semantic-equivalence problem above, unlike comparing against the converter's output.

Open implementation questions:

  • The property key, and whether this is one property per fact or a single markfluence-state property holding a small object. client.SetContentProperty takes a string value today, so an object would want a typed helper rather than callers hand-encoding JSON.
  • Which verbs write it. update and create clearly. fix is read-only against Confluence and should stay that way.
  • Pages published before this lands have no recorded version, which must read as "unknown", never as "changed" — the same tri-state discipline pagedoc.UserCache and StorageOptions.UserNames keep for an unresolved mention, and for the same reason: a fabricated answer written confidently is worse than an absent one.
  • A --dry-run publish must not write it, or a preview would silently claim a publish happened.

The more valuable consequence: update stops clobbering silently (#149)

The display is not the best thing this state buys. update currently avoids overwriting a Confluence-side edit only by accident: the mtime skip means a page newer than its file is skipped. But edit the file after someone's UI edit and the file wins on mtime, so the publish overwrites their work with no warning at all — and --force bypasses the check regardless (cmd/update/update.go:250-258).

A recorded version turns that into a real diagnostic: "this page was changed in Confluence since markfluence last published it; publishing will overwrite that." That is a safety property rather than a convenience, and it is arguably worth more than the status column that prompted it. Filed separately as #149, since it is a defect where this is a feature, and it is probably the better reason to build the recorded state — this issue can consume it once it exists.

Why not an existing command

Not a flag on children. children asks Confluence what is under a node: it takes a page or a space, needs credentials, reports folders, and reports live ids. A local view takes the root, needs none of that, has no folders to report, and has no id at all for an unpublished page — so it would share only the output shape, and children's argument rule is already "exactly one of PAGE or --space".

Not update --dry-run, though it overlaps. update --dry-run docs/**/*.md already reports skipped-vs-would-publish per file, honouring the mtime skip, so its forecast is real. What it does not give you is the hierarchy, the audit facts, or the reverse direction — and it reads as "here is what I would write", not "here is where things stand".

--json

A new command enum entry plus a statusResult definition and an if/then branch constraining both results.items and summary — a command added to the enum without a branch is completely unvalidated, which internal/schematest's document tests exist to catch. Every result field on a typed struct, no omitempty.

Out of scope

  • Semantic body comparison. See above; it needs a normalizer that does not exist.
  • Fixing drift. status reports; update and fix already act in their own directions.
  • Live-tree output. That is children.

Related

  • #139 — markfluence.yaml pages:. The audit facts and the rejected nested-hierarchy alternative both come from its design discussion (_plans/039_project-file-pages.md).
  • #100 — the project-file loader this reads through.
  • #149 — update silently overwriting a Confluence-side edit. The same recorded state fixes it, and it is a defect rather than a feature.
  • #10 — update enforcing space/parent and supporting moves; another case where knowing what the live page says before writing matters.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the existing offline check command and cmd/update/update.go:250-258, then inspect SetContentProperty/ListContentProperties and the internal/schematest document tests. Trace how project pages and parent: data are loaded before deciding the status result shape. Done means an offline status view, optional remote drift information, recorded publish state, and validated --json output without dry-run writes.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
api, cli
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.