0.5.0 — the tool stops asserting what it cannot show
Two things arrive together. init now writes construct.model.json, a record of what this tool holds to be true about your repository and on what grounds. And doctor stops assembling its own picture of enforcement: every verdict it prints is now read out of that record.
Some of what doctor printed was never about your repository. hook reported the absence of a git hook that no preset installs. red-gate always said unknown, because doctor runs nothing it could prove a harness with. Both are gone. A third, lint-policy, reported a missing policy test that the construct writes only into a fresh repository — it now speaks only where the construct wrote the test it stands on, and is silent elsewhere. So the report is shorter than it was — not because it checks less, but because less of it was pretending.
If you are upgrading, do this first
- Run
construct doctor. You will probably see that there is noconstruct.model.json. - That is a normal state, not a fault. The file is written by
initand has never been written bysync, so every repository carried forward from an earlier version arrives without one.doctorwill say so in as many words, report everything it always reported about your files, and exit zero. It is telling you what it does not know, not that something is broken. - If you want one, run
construct init. It is additive: it writes the model, and it will not overwrite anything it does not own. Nothing forces you to — a repository with no model keeps working exactly as it does today.
The new file
construct.json records file provenance: what init and sync wrote, and what has changed since. construct.model.json records something different — what is claimed about your repository and how each claim is held. Facts the tool can point at, claims the construct makes, how each is enforced and verified, and, once discovery runs, the hypotheses it wrote with the facts supporting them.
Neither file reads state from the other, and that separation is checked by the build rather than agreed by convention.
It is committed, not generated on the fly. It belongs in your repository and in your diffs, and it is meant to be read there: when a claim about your repository changes, you should see it change in review, the same way you would see a change to a workflow file.
You can read one before you decide to generate one: this repository keeps its own at construct.model.json, written by the same code that would write yours.
The construct-authored half will not churn. Two runs over the same repository produce a byte-identical file for the entries init owns, which the test suite asserts on every preset: there are no timestamps in them, no generated identifiers, and they sit in the order the preset declares them rather than the order anything was read off disk. A diff there appears when something the construct describes has actually changed. The rest of the file — what discovery wrote, what you wrote — is carried over unchanged rather than rebuilt, so it moves only when its author moves it.
You may edit it by hand, under one rule
Every entry carries an authoredBy, and that is the whole of the ownership rule:
initmay replace only entries authored byconstruct;- entries with any other author are carried over unchanged.
So a second init will not overwrite what the construct does not own. A hypothesis discovery wrote survives; a claim you added yourself survives. What init rewrites is its own entries.
The corollary surprises people, so it is worth stating plainly: a hand-edited entry still authored by construct is overwritten by the next init. Change its authoredBy to discovery or unknown and your edit survives. There is no force flag and no backup file; ownership is the only thing that decides.
That matters most when a fact the construct wrote stops matching — a workflow renamed, a script rewritten into an equivalent form. doctor reports the claim as unsupported, which means this fact no longer matches, not the enforcement is gone. The way out is to change that fact's authoredBy away from construct and edit it to match. Editing it while leaving it authored by construct is not a fix — the next init overwrites it. architecture/model.md has the full rule and what it costs you.
What changed in doctor --json
Six changes, listed together because six discoveries in six diffs is worse than one list. The two removals come first among them, since a field that vanishes breaks a script quietly while a renamed one breaks it loudly.
checks is now every claim, not a selection. It used to be five fixed verdicts. It is now one entry per claim in the model, in the model's declaration order, each carrying claimId alongside id. The two names existing consumers read — ci and lint-policy — survive as the id of their claims, so find(c => c.id === 'ci') keeps working.
Be aware that id therefore holds two kinds of name: the historical check name where a claim has one, and the claim's own id where it does not. claimId is always the claim's id and never anything else, so key on claimId if you are iterating or aggregating rather than looking one entry up.
red-gate is gone. It was always unknown, for one reason: doctor executes nothing from the repository it inspects, so it cannot know whether your harness passes. That is a statement about doctor's own limits rather than a fact about your repository, and the report now says it in one line instead of dressing it as a verdict.
hook is gone. It reported that no git hook manager was installed. No preset installs one — the construct enforces the harness in CI on purpose, because a local hook is bypassable with --no-verify — so the verdict announced the absence of something nobody required. If your repository does have a hook, discovery records it with the facts behind it and doctor will speak about it, because then there is a claim to speak about.
weakestLink is replaced by youAreHere. The old field was computed by doctor over its own checks. The new one is the point on a claim's chain where it stops being held — claim, stage, state, and the facts that stopped matching — taken from the model rather than recomputed.
construct-tests is now uncollectedTests. Same question, different place in the report: are the test files construct.json recorded still collected by your runner config? That can only change when the construct's own files change, so it sits with the file report rather than among claims. It says nothing at all where the construct did not write your runner config — there it never wrote that end, and has no business pronouncing on a file you own.
harnessProblems now carries only the construct's own record. A missing package.json, a missing script, a recorded contract path that is gone. The assertions about what your harness command actually runs — lint, typecheck, tests — moved into the model as a claim, because a package.json you own can stop calling the right command without any construct file being touched.
One field is added
unreadableFiles lists paths the record names that exist but could not be read, with the cause beside each. They are neither missing nor modified, and reporting them as either would be a claim about a file doctor never opened. Previously such a path crashed the command.
If you script on the exit code, read this
No field was renamed or removed in a way that shows here, so this is invisible in the list above — but the meaning of the exit code moved in two places.
ok answers whether the construct's own installation is intact and fully inspectable. It says nothing about what is claimed of your repository.
- A repository where
doctorcould not read part of what it was asked about is no longer reported as successful. It exits non-zero and names the file, where before it crashed. - A harness that stopped running
lint,typecheckortestused to exit non-zero and no longer does. It is an unsupported claim now, reported inchecksunderharness-steps, which names the fact that stopped matching. If you relied on the exit code for this, read that instead.
A repository with no model at all exits zero. Nothing was inspected, so nothing is claimed — that is absence of a subject, not a failure.
Two things the model refuses
No stored state. A state property is rejected anywhere in the file. Whether a claim is held is computed from its facts on every read — so a claim whose workflow you deleted stops being held without anything having to notice the deletion.
No confidence number, under any name. confidence, strength, score and support are each rejected. A number beside a claim gets read as a probability, and it says nothing about whether anything was actually checked. Whoever wants one has to change the schema and defend it.
For the same reason an enforcement carries the facts that hold its level up rather than a bare level. A claimed L3 whose workflow has been deleted becomes unsupported; a bare "level": "L3" could never rot.
Where the reasoning lives
- decision 0015 — interpretation stays with the agent; the CLI records facts and checks them.
- decision 0016 — the model is the source;
doctorand every report are projections of it. - decision 0017 — this work adds no new way of knowing, on purpose.
- architecture/model.md — the vocabulary, member by member.
- docs/cli.md — the full
doctor --jsonshape.