Skip to content

The tree

shipped 1.0.0

The tree is the centre. Every site is a spoke. Direction is measured from the tree, never from the machine.

The tree is your repository as it stands on disk: the committed working copy. A site is a WordPress install: a database and a filesystem. The DDEV container on your machine is one of those, and so is dev, and so is prod.

The tree is the only participant with history. It is the only one you can read a diff on and the only one a review can happen to. So it is the only sensible origin for the word direction.

What that makes pull and push mean

pull reads into the tree. push writes out of it. The environment argument names which site, and nothing else.

cwp pull dev      # dev      → the tree
cwp push dev      # the tree → dev

One verb each way, carrying everything under management. Tidiness is not the reason. A partial read would leave the baseline meaning nothing in particular: the items in it would carry baselines from different days. --only <artifact> narrows a single run without pretending the rest moved.

The three sites differ in distance and in what a guard lets you do to them. They do not differ in kind.

What the tree holds

Six artifacts. cwp pull writes all of them and cwp push sends all of them. --only names one.

In the tree--onlyRead as
content/contentone YAML file per post, plus terms, menus and the authors
the design system directorybricksa tree of JSON, one file per item, plus cwp-options.json
settings/settingsthe options a group declares
widgets/widgetsone file per widget area
roles.ymlrolesroles and the capabilities they grant
inventory.ymlinventorycore, locale, plugins, themes and versions
the tracked paths—the code you write, sent by the same push

The tracked paths are the row that is not like the others. They are code, they have no per-item identity and no baseline, and git is their history. They travel with the push because an operator shipping a change means the files as much as the content.

Everything in that table is text. You can diff it and review it the way you review code. The artifacts exist in those formats rather than as a database dump for that one reason.

A family adds no row. It is one feature of a plugin or theme declared as a unit. It expands into the rows above: a settings group, a content group, meta rules on the other groups. What lands in the tree is the same files a group you typed by hand would produce.

What the tree deliberately does not hold

This half matters as much as the first. A tree that quietly lags the site is worse than no tree at all.

  • Images, unless you say which ones. cwp media pull and pull.uploads bring a library to your machine, and nothing reaches the tree by that route.

    What reaches the tree is media.tracked, and it has three answers. none is the default. referenced is the files your pulled content points at, enough to rebuild the site’s pages without a database. library is every attachment the site holds. Whichever you pick, the capture runs under a file and byte budget and names the files it left when the budget stopped.

    Only originals travel. WordPress generates the derived sizes from them, and wp media regenerate rebuilds the set after a push. The budget exists for the reason none is the default: git stores every version of a binary whole and forever, and nobody can repair a history afterwards.

  • Customer accounts. No subscriber, no buyer, no order, no address. The scrub anonymises everyone the project did not ask to keep, and nothing puts them in a file.

    The people who wrote the content are the exception, and the exception is deliberate. An item records the login that wrote it, and content/authors.yml records those people: display name, address, roles. A login the other side cannot resolve leaves the item with no author, no archive page and the wrong permissions. They keep their real data through the scrub for the same reason.

    So a repository carrying content carries its editorial staff by name. Make that decision consciously: git does not forget. An environment that should not have them at all takes author: it writes every item there under one account.

  • Third-party plugins and themes. .gitignore excludes wp-content/plugins/* with your own plugin exempted, and wp-content/themes/*. What travels instead is the list, inventory.yml, so a checkout reproduces the set without vendoring it.

  • WordPress core. The image or the host provides it, the inventory records it, and nothing commits it.

  • Secrets. The pull leaves out the design system’s api-keys and custom-code tabs unless you ask for them, and a settings group refuses to carry a key it recognises as a credential.

  • Everything nothing describes yet. Custom tables, post types a plugin registers at runtime, options nobody declared. This is the largest category and the least visible one.

The tree is not the site, and one command measures the gap

cwp coverage counts what WordPress holds, area by area, against what the tree describes. It gives each area one of four verdicts: tracked, excluded, untracked, not-reproducible.

It exists for the failure the section above sets up. A second machine can check out the repository, build it and produce a site that is not the same one, with every column of cwp status reading green. status compares what an artifact describes; coverage asks what no artifact describes at all.

Measuring is not moving. cwp coverage names a site and reads the tree, and it transfers nothing between them. So its page carries no direction glyph, where every pull and push page does.

Where to go next