Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ to use it. It also tells you where to find more documentation.
| [docs/root-model.md](docs/root-model.md) | the documentation root: how a tree of files maps to a tree of pages |
| [CONTRIBUTING.md](CONTRIBUTING.md) | how to contribute: the development setup, what to run before you open a pull request, the commit conventions, and how to file an issue |
| [docs/confluence/](docs/confluence/) | what we found out about Confluence by experiment: the API, the storage format, the scopes, and the traps that give you a confident wrong answer |
| [docs/guarantees.md](docs/guarantees.md) | the properties that markfluence holds itself to, each one with an honest status |
| [docs/design-principles.md](docs/design-principles.md) | the design principles that guide markfluence, and the trade-offs they accept |
| [docs/json-output.md](docs/json-output.md) | `--json` in detail: the status verbs, what counts as a result, and why the shapes are what they are |
| [schema/json-output/v1.json](schema/json-output/v1.json) | the `--json` schema. The `markfluence schema` command also prints it |

Expand Down
5 changes: 1 addition & 4 deletions _plans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ end at exactly the moment somebody needs it.
- **12 Go files cite a plan by number** — `internal/project/project.go`,
`internal/convert/attachname.go`, `cmd/create/create.go` and others. Those
comments exist where the code looks arbitrary and the reasoning is long.
- **[docs/guarantees.md](../docs/guarantees.md) cites them 14 times**, and
load-bearingly: a guarantee's *status* is often justified by the plan that
set it ("`_plans/026` accepted it as the cost").
- **[docs/root-model.md](../docs/root-model.md) cites them 10 times**, and 22
- **[docs/root-model.md](../docs/root-model.md) cites them 5 times**, and 22
commit messages do too — those can never be fixed.

They are also versioned with the code they describe, which a notebook cannot
Expand Down
2 changes: 1 addition & 1 deletion cmd/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ func reserveOne(
// fail here is a server or network condition -- or a local read the converter
// never made: SyncAttachments opens every asset to checksum and upload it, so
// an image that Lstat'd fine in preflight can still be unreadable now. Those
// are the residuals S7 (no-partial-create) stays Partial for.
// are the residuals S7 (no-partial-create) accepts.
func publishOne(
r record, res *createResult, pageID string, version int,
c *client.ConfluenceClient, users *pagedoc.UserCache,
Expand Down
12 changes: 6 additions & 6 deletions cmd/create/parent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ func TestResolveParentNoneGiven(t *testing.T) {
}
}

// TestResolveParentIgnoresDirectoryNesting is guarantee L8 (no-layout-inference,
// docs/guarantees.md): hierarchy is never inferred from disk layout. The
// directory shape here is deliberately the one most tempting to "helpfully"
// infer from -- a file named after its parent directory, one level down from a
// same-named sibling file -- and it must still resolve to no parent at all
// when nothing said so.
// TestResolveParentIgnoresDirectoryNesting is principle L8
// (no-layout-inference, docs/design-principles.md): hierarchy is never inferred
// from disk layout. The directory shape here is deliberately the one most
// tempting to "helpfully" infer from -- a file named after its parent
// directory, one level down from a same-named sibling file -- and it must still
// resolve to no parent at all when nothing said so.
func TestResolveParentIgnoresDirectoryNesting(t *testing.T) {
root := rootFor(t, t.TempDir())
if err := os.MkdirAll(filepath.Join(root.Dir, "section"), 0o755); err != nil {
Expand Down
12 changes: 6 additions & 6 deletions cmd/create/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,12 +514,12 @@ func TestCreateAllStubIsEmptyThenPublished(t *testing.T) {
}
}

// TestCreateBatchIgnoresDirectoryNesting is guarantee L8 (no-layout-inference,
// docs/guarantees.md) at the batch level: a file nested several directories
// deep, alongside files at shallower levels with names that could plausibly
// read as its ancestors, must still be created as a top-level page unless a
// parent: field or --parent said otherwise. Nothing about the tree shape may
// contribute to the decision.
// TestCreateBatchIgnoresDirectoryNesting is principle L8 (no-layout-inference,
// docs/design-principles.md) at the batch level: a file nested several
// directories deep, alongside files at shallower levels with names that could
// plausibly read as its ancestors, must still be created as a top-level page
// unless a parent: field or --parent said otherwise. Nothing about the tree
// shape may contribute to the decision.
func TestCreateBatchIgnoresDirectoryNesting(t *testing.T) {
resetOpts(t)
dir := t.TempDir()
Expand Down
2 changes: 1 addition & 1 deletion cmd/export/actionlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (rec *recorder) recordWalk(res *result) {
//
// It is self-consistent in the way that matters. The pass hashes *our own
// render*, not a comparison against the page, so it does not depend on
// round-trip fidelity at all -- L5/L6 being Partial is irrelevant here, because
// round-trip fidelity at all -- L5/L6's storage churn is irrelevant here, because
// the value recorded is precisely the one a later `update` recomputes from the
// same file. What it does have to match is update's own inputs, which is why
// the title comes from the file's frontmatter and the space key from the live
Expand Down
16 changes: 16 additions & 0 deletions docs/confluence/storage-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ would churn ids forever.
So the guarantee is **semantic**, not byte-for-byte, which is also the
converter's stated design target.

### A page from the editor does not survive an export and republish byte-for-byte

**Verified 2026-09-05**, exporting a live page the editor had written and
publishing the Markdown back. The stored storage changed in two ways, neither of
which changes what renders:

- The editor writes a list item as `<li><p>text</p></li>`; the converter writes
`<li>text</li>`.
- The editor's TOC macro carries `ac:local-id`, `ac:macro-id` and `data-layout`
attributes; the converter's canonical form has none of them.

This is why L5 (`roundtrip-from-confluence`, [../design-principles.md](../design-principles.md))
promises the page keeps its *meaning* rather than its bytes. The Markdown side
is stricter: after one cycle it stops changing, which
`TestRoundTripMarkdownIsAFixedPoint` checks over every `storage2md` case.

### Confluence strips HTML comments on write

**Verified 2026-09-13.** A comment does not survive the write at all — it is not
Expand Down
Loading
Loading