Skip to content

Releases

Every released version, generated from CHANGELOG.md. Edit the changesets, then run pnpm release-notes:render; the test suite fails when this page and the changelog drift apart. A release with a hand-written note links to it rather than repeating it here.

0.16.3

Patch Changes

  • #162 62d8a2a Thanks @E1i! - An observation: a true output read for the wrong relation, and the refusal that ended it

    A false premise entered a task brief from the reviewing session, survived the owner re-running the command it rested on, and was stopped at the implementer by a measurement. git tag | tail -3 was read as the three newest versions; it answers the last three lexicographically, and here those are v0.8.0, v0.9.0, v0.9.1, because 9 sorts after 1. From that reading followed a count of untagged releases, a claim about what the release action pushes, and a claim that the header comment in release.yml is false — two briefed pull requests, and a correction drafted for a changeset. git tag --sort=v:refname ended it: 31 tags against 32 published versions, one gap at 0.1.0, which is the version release.yml already documents as published by hand before trusted publishing could work.

    What makes the specimen worth recording is that the command succeeded. Its exit status was zero, its output was real, every tag it printed exists, and re-running it reproduced the same three lines. Only the relation between that output and the question was wrong. A failure would have announced itself; this did not, and the second run — by a second person — read as confirmation rather than as a repetition of the same reading.

    The entry states two readings and promotes neither. It is a third instance of a valid result read as evidence for a different relation, after git ls-files in decision 0014 and a commit range read for a temporal question; all three are git, in each case the default output answered an adjacent question — what is listed rather than what is tracked, ancestry rather than time, lexicographic rather than version order — and in each case a flag existed that would have answered the question asked. Three instances on one mechanism are material for the entry and are not grounds to extend anything, the trigger for that being a third instance on a different mechanism. And the carrier that stopped it was neither a check nor a rule but the implementer declining to write code from a brief that did not match the tree: the fourth refusal to build recorded here, and the first where the refusal kept a false statement out of a published record rather than out of an unbuilt artifact.

    No remedy is proposed and nothing is promoted to a decision or a rule. The boundary is one premise, one night, one project and three parties — a form, not a rate, and nothing about how often a true output is read for the wrong relation.

0.16.2

Patch Changes

  • #160 9016086 Thanks @E1i! - Two open questions: a check that can never be green, and a path that left no evidence

    doctor exits 1 on this repository for two conditions that are legitimate and permanent — tsconfig.base.json recorded in the manifest's sync branch and absent from the tree, and fifteen baseline files modified since init, which is what a repository that edits its own construct files looks like. Neither will become green, and a third condition — a real one — would arrive in the same exit code and go unread. A check that can never be green reports as much as one that can never fail. The question recorded is whether an owner can declare a construct-owned path they do not want, and files they maintain themselves, so that doctor can tell a declared deviation from an unknown one.

    The second question comes from the first one's evidence. Measured: the recorded hash 040735e3… is byte-identical to templates/harness/tsconfig.base.json, and git log --all --follow over the path returns nothing, so git holds no evidence either way about whether the file was ever on disk here. Stated, not measured: the message of commit 9c00c33 says --apply wrote the path and that it was then deleted; the owner does not recall deleting it. A path sync --apply writes that git does not track leaves no evidence of having existed, so what the run did is recoverable only from prose — and the two kinds of claim above are why the entry keeps them apart rather than reading the message as a record of the action.

    Both are named and neither is answered. No design, no code, and nothing repaired: the sync record is a record of the past.

  • #155 9428ade Thanks @E1i! - Why a marker with no recorded provenance is not backfilled, said where the reader meets the gap

    doctor now reports markers that carry no recorded provenance, and the obvious next move — compute a sha over each body and write it down — is the one move that must not be made. A sha asserts that the body it hashes is what that run wrote. Computing one over a body nobody recorded asserts authorship of text whose author is exactly what is unknown, turning never looked into checked and matching: rule 2 in the direction that manufactures support.

    docs/cli.md says this under the table of readings, so a reader who meets unrecorded there finds the reason rather than an apparent backlog. Provenance is written only by the run that writes the body: a discovery run records it for the markers it fills, a marker it did not fill keeps the entry it had, and unrecorded stays until a run rewrites that marker.

    This repository's own open-questions marker carries the question that leaves open, with its shape stated rather than a design proposed: the step that knows what body it wrote is a hand-written L0 step, and any command that records provenance after the fact is indistinguishable at the moment it runs from the laundering above. So an answer cannot be a command the owner runs afterwards — either the write happens inside the same act that authors the body, or it does not happen.

    No behaviour changes and no code changes; the nine markers stay unrecorded.

  • #157 ed97251 Thanks @E1i! - Two pointers in this repository's own markers named a file that is not here

    AGENTS.md pointed at docs/PLAN.md twice — once in module-map, listing it among the directories the layout table does not cover, once in high-effort-areas, citing a section of it beside the discovery protocol. The file does not exist and is not tracked. Both pointers are removed and neither is replaced: what that file held now appears to live across architecture/decisions/, architecture/observations.md and docs/guide/, but that is an inference, and a marker is not the place for one. The sentences around them stand without a destination.

    Nothing else changes. Both markers carry no recorded provenance and still do.

  • #158 8d21eac Thanks @E1i! - Two source files imported across the dependency policy with no boundary to hold them to it

    AGENTS.md says dependencies inside src/ point one way and that eslint.config.mjs enforces it. ALLOWED_INTERNAL_IMPORTS keyed a directory or file per module, and two files that import other modules had no key at all, so nothing was enforced for them: src/failure.ts, which reaches the vocabulary in src/ui, and src/cli.ts, which composes everything.

    The acceptance is the general property rather than the two files: every source file that names an internal import is covered by a boundary. It was run red first and named both — the second was not known before it ran. A file that names no internal import is asked for nothing, so src/record-ahead.ts and src/version.ts carry no entry and are not made to carry an empty one; that case ships as a test beside the criterion.

    The entries say what each file imports today, not what it might: src/failure.ts may reach ui and nothing that holds a record, and src/cli.ts may reach the commands, the presets, the vocabulary, the version and detect, but not the manifest, the model, the materializer or sync behind them. Both cases are in tests/dependency-policy.test.ts.

    No behaviour changes: nothing in src/ moved, and the new rules are satisfied by the tree as it stands.

  • #159 6b22a07 Thanks @E1i! - An open question: the machine-readable output declares nothing and refuses nobody

    construct.json declares manifestVersion and construct.model.json declares modelVersion, and each refuses a record written by a later build through one shared error. doctor --json declares nothing: src/cli.ts serialises DoctorResult as it stands, so the output carries no statement of what it is and there is nothing for a reader to check or for the tool to refuse. A consumer matching a value that has since changed — authorship: "unknown", which 0.16.1 no longer emits — receives no error; its branch simply stops firing.

    The question is recorded in this repository's open-questions marker with the measurement behind it: nothing the construct materializes calls doctor --json — not the templates, not the workflows, not construct-discover.md — and every match outside the source is built documentation or release-note prose. The consumer count is zero today, and that is what makes an answer cheap now rather than what makes it unnecessary.

    No design is proposed and no code changes.

0.16.1

Patch Changes

  • #151 2cf0662 Thanks @E1i! - Release verification says which of three states left the version absent, and derives the third

    mikoshi-construct@0.15.0 was versioned and never reached the registry. Release verification caught it, which is what it is for. Its message then named two causes — a staged publish awaiting approval, or a publish that failed while reporting success — and prescribed the repair for the first: npm stage approve, then re-run. The real cause was neither. The release action had found an unconsumed changeset in .changeset/ and updated the version pull request instead of publishing, so there was nothing staged to approve and nothing to re-run. Every fact in the line was true and it pointed the reader at a repair that does not exist for the state they were in.

    Three states produce the identical 404, and they take three different repairs: approve the staged version, read the failed publish's log, or consume the changesets and release again. The message now names all three and prescribes none of them while the state is undetermined — the reader is told what tells them apart, not what to do before they know which one they have.

    The third is not one of three guesses, because it is readable. Which branch the release action takes is decided by the tree it runs on: unconsumed changesets in .changeset/ mean it versions rather than publishes. The verification checks out that tree to read the version it is verifying, so it reads the directory from the same checkout and states the cause, with the count and the filenames that carry it. A tree with nothing pending excludes that state instead, and two are named rather than three.

    Boundary. The reading is of the tree the verification checked out, and it says so in those terms. A changeset merged after a successful publish, while the registry is still catching up, would be read as versioning; the window is the poll's two and a half minutes and the claim stays scoped to what was seen. Nothing about the exit codes changes: absent is still 1, unreachable still 2, and unreachable still claims nothing.

  • #154 424df7f Thanks @E1i! - Discovery provenance reports every state it computes, and the three causes of "unknown" become three states

    doctor derived four things about a discovery marker and printed one. markerAuthorship answered unknown for three different causes — the marker was never construct-authored, no sha was ever recorded, and the body could not be read — and the report then kept only the markers still reading back what discovery wrote and returned early when that list was empty. So owner, the one state the sha exists to surface, was computed on every run and never printed, and a marker whose provenance was never recorded was indistinguishable from one the owner had rewritten.

    Measured on this repository before anything changed: nine of ten markers recorded as unknown with no sha, one recorded as construct-authored whose body no longer hashes to its recorded sha. doctor printed no provenance section at all.

    The incoherent record is now unconstructable rather than handled. MarkerProvenance is a union of the two combinations that can occur — construct with the sha of the body that run wrote, or unknown with null — so construct with no sha cannot be expressed. upgradeManifest is the one door such a record can arrive through, and it reads it back as unknown with null. That removes one of the three causes at the type level and leaves two real states, which is why authorship has four members and no flag:

    readingwhat it means
    constructrecorded, and the body still hashes to the recorded sha
    ownerrecorded, and the body no longer matches: edited since, and read as the owner's
    unrecordednothing was recorded, so there is nothing to compare a body against
    unreadablerecorded, and the body could not be read here

    The report renders each of them from one table keyed by the reading, so a state added later has nowhere to be silently dropped, and the section is omitted only when there is no marker at all. On the repository above it now names the edited marker as the owner's and says that nine carry no recorded provenance.

    authorship in doctor --json no longer emits unknown; it emits unrecorded or unreadable, whichever the reading was. Nothing else about the output changes, and provenance still never changes the exit code.

    The nine markers are left without shas: recording them is a separate change, and doing it here would have removed the state this was measured against.

  • #152 c4411fb Thanks @E1i! - Two observations: the cells of a blind discovery run, written before it, and what the run produced

    A copy of this repository was taken before either record existed, to ask whether the discovery protocol surfaces a form stated nowhere in the tree: an assertion that cannot be subjected to a meaningful attempt to refute it is a declaration, not a check. The tree carries an instance of that form — decision 0027, on acceptance criteria, rendered in three further files — and an adjacent generalisation in decision 0024 and in observations.md. The prescribed reading surface of .claude/commands/construct-discover.md names none of them.

    The first entry fixes the four cells before the run: what a hypothesis matching the form would mean with the carriers unread or opened, and what its absence would mean in each case. Two readings are stated plainly there because both were got wrong on the way to writing them — that a protocol which does not prescribe a file is not an agent failing to read it, which is why the run asks for the list of files actually opened, and that a hypothesis matching the form is attributable to independent discovery only if that list shows every carrier unread.

    The second entry records the outcome. In one blind run the protocol produced four new instances of the form — three in the enforcement plane, one in verification — and did not state the form as a general property; no carrier of it was opened. The cells were binary on whether a hypothesis appeared and had nowhere to put four instances written without the generalisation, which is recorded rather than adjusted. No mechanism is offered for why the form was not stated. A second run by a different tool is excluded as invalid rather than counted as a negative result, and one of the four instances — nine of ten discovery markers carrying no recorded provenance — is left unrepaired on purpose, because repairing it would have changed the specimen.

    Neither entry is promoted to a decision or a rule. The boundaries are one run, one specimen, one protocol, and an agent of the same lineage as the one with which the form was first stated.

  • #151 2cf0662 Thanks @E1i! - 0.15.0 and 0.16.0 each carry a dated notice naming the other

    The changelog asserts that 0.15.0 is a release. It is not: no tag, nothing on the registry. Under 0021 the entry is not corrected — it records what was versioned, and that did happen — so a dated notice is added beside it instead, and the entry is left as it was written.

    The notice is written in both directions, and the more important one is on 0.16.0. A reader arrives at the version they installed, not at the one that does not exist: someone on 0.16.0 sees an entry naming a single pull request and has no way to learn that two more shipped inside it. So 0.16.0 says it carries what is listed under 0.15.0, and 0.15.0 says its changes went out as 0.16.0. This is 0021's own remedy for a stale record — a pointer from the old statement to the new one — applied to a pair rather than to one file.

    A notice is parsed out of the changelog rather than kept in a second list, and a test fails when one points at a version that answers nothing back, so the pair cannot be half-written. The rendered release-notes page carries both, because it is generated from the changelog.

0.16.0

Recorded 2026-09-22 — this release also carries the work versioned as 0.15.0. The entry below names only what 0.16.0 versioned itself. Everything listed under 0.15.0 is in this release too: that version was versioned and never published, so if you installed 0.16.0, read the 0.15.0 section as part of what you have.

Minor Changes

  • #149 fda1950 Thanks @E1i! - /plan in Claude Code requires a criterion to be seen failing before the change

    The plan command asked for acceptance criteria a harness run or a test can confirm, and for each criterion to be verified by what the task changes itself. Both are satisfied in full by a criterion that has never been shown failing, so a task could carry four criteria, all of them green from the first moment, none of them able to tell a change that worked from one that was never needed. The command now asks for the criterion to be run and seen failing first.

    It is scoped, and the scope is the point. The requirement reads where a criterion can fail against the repository as it stands. A criterion for a defect or for absent behaviour can; one written to forbid a wrong implementation cannot, and the command says nothing about that case in either direction. Stating the wide form would have a planning agent reject a legitimate guard; stating the narrow form as universal would do the same. Silence here is the honest state of a question that is open.

    This is not enforcement, and the distinction is worth reading slowly. What changes is that the rule reaches the planning command in Claude Code — not planning in general, and not Cursor, which this tool gives no /plan equivalent. The instruction is prose an agent may or may not follow — L0 on the scale this tool reports, exactly like the discovery protocol, and nothing reports a violation of it. The carrier is the evidence a run leaves behind, and it is not built here. Anyone reporting this release as the rule now works is making a claim wider than what was done.

0.15.0

Recorded 2026-09-22 — versioned, never released. No v0.15.0 tag exists and the registry has never served this version. The release run on the version commit found an unconsumed changeset in .changeset/ and updated the next version pull request instead of publishing — nothing was staged and nothing failed, the publish never ran. The changes below reached the registry in 0.16.0. The entry itself is left as it was written.

Minor Changes

  • #146 4dd1b57 Thanks @E1i! - A construct.model.json from a later build is a named state, not a schema failure

    A model declaring a modelVersion this binary does not understand produced a generic parse failure — the same path as a missing field or a malformed array. It is now reported as what it is, with the record, the field and both versions, and the command exits 1 without reading or writing anything. doctor, graph, sync and init all inherit it, because all four read the model.

    The state was not built twice. One already existed for construct.json and everything about it fitted except that the error and its line named that file and that field in their own text. The carrier now takes the record and the field as values and both readers raise it, so there is one mechanism rather than two of the same shape. A manifest from a later build reports exactly the text it reported before.

    The check also runs before the unknown-property check, since a model from a later build will usually carry fields this binary has never seen and the symptom would otherwise be reported instead of the cause.

    What it closes. readModel returns null when the file is absent, and null already means no model, which doctor explains by saying construct init writes one. Had a version-ahead model collapsed into that reading, doctor would have told someone holding a newer model to run init — proposing to overwrite the file it had just failed to read.

    Nothing migrates an older model, no fact kind is added, and MODEL_VERSION does not move.

Patch Changes

  • #148 70f8c07 Thanks @E1i! - An observation: an acceptance played a second role at 0027's first use

    Decision 0027 requires an acceptance to be red on the current tree before the implementation exists. Its first application — decision 0028 — carried two axes, and only one behaved that way. The second was green and could not have been red, because the defect it describes does not exist on that tree; it becomes red only against a specifically named wrong implementation, which is what it was run against.

    So an acceptance has two legitimate roles and the rule describes one: detecting an existing defect, which is red on the current tree, and forbidding a named wrong fix, which never is. The second needs a clause the first does not, or it is satisfiable by construction — the named wrong implementation must be one a reasonable implementer would actually reach for. Here it was the one the task brief itself described as the current state.

    The entry does not extend 0027. Whether "red on the current tree" should become "red on the current tree, or against a plausible named wrong implementation" is named and left open on one application and one gap, with the trigger stated: a second acceptance that turns out to be a guard against a wrong fix, in an unrelated task.

0.14.1

Patch Changes

  • #145 71ff5d2 Thanks @E1i! - Decision 0027: an acceptance is red before the implementation exists

    The lifecycle had an unnamed stage between the brief and the implementation, and it had been doing real work. It is acceptance discipline, and it is not the reasoning ladder: one answers whether we may proceed to reasoning at all, the other how much reasoning to give once the harness says the solution does not work. A new normative area takes a new number rather than widening the ladder into a job it never had.

    The rule is one mechanical test. A criterion that cannot be shown failing is a statement of intent however specific its wording, and the discriminator for every checkpoint in the stage is that it is met by producing something a second party can check rather than by an answer about one's own work. Two stops come with it — a baseline that cannot be reproduced is reported rather than assumed, and a criterion is never adjusted to make the test green — and one condition, that the party writing the acceptance is not the party measuring it.

    the-cycle.md §3 keeps both of its existing rules, which the record names individually: the first bounds what a criterion may depend on and survives untouched, the second orders tasks and is not about criteria at all. Neither provides red-before-implementation, so the third statement lives in the decision and §3 points at it.

    The record carries L1 and says why: nothing today can report a violation, so by this repository's own observation it is a rule carried by memory. What would make it L3 is named and not built.

    The record carries two things a later reader would otherwise have to rediscover. The chain of carriers has a stated end — it stops at the first level where enforcement can fail without a human deciding it has failed, and where that failure has been demonstrated on a real case the carrier was expected to catch — with both clauses shown against examples already here: the dependency audit under continue-on-error fails the first, and the post-publish smoke satisfies the second by having been run red on 0.12.2 and green on 0.13.0. And the path to enforcement is written as three rungs rather than one, because the middle rung — the planning agent reading the rule — is necessary, is still L0, and is the one that will be reported as completion.

  • #143 f7c9da5 Thanks @E1i! - Three observations: rules without carriers, plans that ended in not building, and six stops on a false premise

    A rule with no independently checkable carrier is carried by memory. Two rules stated clearly and observed by whoever remembered them: the documentation claim that stood ten minor versions, and the reasoning ladder, whose only artifact is a ledger line recorded for 29 of 65 runs. What they share is not that they were broken but that breaking them produces nothing, so the absence of complaints is evidence about the reporting rather than about the rule. The rule was considered for promotion to a numbered epistemic rule and was not promoted, because it fails its own requirement — nothing would exist if it were broken.

    Three plans that ended in not building, each with its reason recorded. The discovery plan skill, whose hand-written plan came out empty; the trace of an unmade claim, which 0020 left open and 0024 answered by refusing to record one; and the rule above, which declined its own promotion on the standard it proposes. None was dropped quietly, each reason is specific to the thing, and in each case the output was a record rather than the artifact.

    Six stops on a false premise, in one night. Each premise as stated, and what measurement showed. Five were the reviewing session's and one the implementing session's. The condition that makes it work is that in all six the party who wrote the criterion was not the party who measured it.

    All three carry their boundary: a form, not a rate, and none of them says how often any of it happens.

0.14.0

Minor Changes

  • #140 f1b1728 Thanks @E1i! - The workspace policy is recorded, not derived again on every run

    Decision 0026, implemented. allowedWorkspaceImports answered two questions at once: which packages exist, which is a fact about the tree, and what each may import, which is a decision. A second init re-derived both, so a leaf recorded as importing nothing came back allowed to import the app, and sync --apply then wrote that looser policy into eslint.config.mjs — a dependency policy loosening on a repository nobody edited.

    Which packages exist is still derived on every run, so a package added since the last one becomes a new key. What each may import is now kept once recorded. A new key is given the preset's default for a package of its kind: a package under apps/ may import every other workspace package, anything else may import nothing. The run names the keys it added, what each may import, and that eslint.config.mjs is not rewritten there so a later sync --apply would write the new policy into it — the consequence, not only the delta.

    Adopting a monorepo whose packages already import each other will now fail lint until you widen the policy deliberately. The default for a first init on a monorepo that already carries packages used to be permissive — every package allowed to import every other — because the packages were detected rather than created. It is now the same narrow default as everywhere else: packages/* starts at importing nothing. Widening it is a one-line edit to eslint.config.mjs, and from then on the record keeps what you chose. The old default blessed whatever the repository already did without anyone deciding to, and under this release that unchosen policy would have been recorded and kept.

    construct.json carries the policy as structure under policy and declares manifestVersion 5. The rendered form is derived from the structure and is never read back to recover it, so a formatting function is not the authority on what was decided. The rendered entries are sorted by package directory, so the policy no longer depends on the order the packages were enumerated in.

Patch Changes

  • #142 341ff2c Thanks @E1i! - The CLI reference carries the line the picture has carried since 0.11.1

    docs/cli.md describes the picture's legend and the state on each entry, and stopped before the sentence the page prints under that legend — that colour carries the derived state and not the enforcement level, so the same green covers an L0 claim nobody is obliged to read and an L3 claim that fails the build. Nothing in the reference was false; it was incomplete about the one thing the page goes out of its way to say.

    The reference now repeats that line rather than restating it, and a test holds both against the same constant in src/model/svg.ts, so a change to the sentence cannot leave the document behind.

  • #139 c4b8c6a Thanks @E1i! - The written count says which of two questions it answers

    init printed "Written: 4 files" and no next step in the same run. Both were right and they counted different things: the count was applied write operations, the next step was derived from the operations whose content actually differed from what was on disk. On a second run the merges and appends reproduce what is already there, so four operations are applied and nothing changes — and the reader was left reconciling two numbers that answer different questions under one word.

    The row now names both: 59 files, 59 changed on a first run, 4 files, 0 changed on a second, 5 files, 1 changed where one file was restored. The count is not removed, because the four operations did happen. The changed set is now computed once and read by both the count and the next step, so they cannot drift apart again.

    The three cases of the closing line are unchanged.

0.13.0

Minor Changes

  • #136 d640935 Thanks @E1i! - A second init reads the record instead of asking the directory, and instead of asking you

    Three places where init derived an answer from the state of the directory when construct.json already held it. All three are only wrong from the second run onward, which is why the rollout is what triggers them.

    A second init deleted the lint-policy claim from construct.model.json. Whether the repository carries the preset's sample was computed from whether the directory is empty — false on every re-run — so the model was rebuilt without that claim and mergeModel dropped it, silently, while the sample sources were still on disk and every fact under the claim still held. It is now answered by whether the construct ever materialized the sample here, which the manifest records and sync already computed the same way; the reading is one function both commands call. Three runs on a tree materialized from empty now leave construct.model.json byte-identical from the second run on, carrying the same claims the first run wrote.

    doctor follows the same reading, so the two cannot disagree. Where the construct did materialize the sample and the owner deleted the claim by hand, doctor now reads every-fact-holds — a run here really would record it — instead of promising the opposite.

    A second init asked again for what it had already been told. The preset, the agent target, the project name and the code-review provider are all recorded, and a re-run now reads them and names them in the configuration block rather than putting the same four questions. A flag still overrides any of them. The confirmation before writing stays, because it authorises this run rather than restating a value. The recorded review model is kept too, instead of falling back to the default.

    init against a manifestVersion from a later build writes nothing, which docs/cli.md has asserted all along and nothing held. It is now a test.

  • #134 e2db78c Thanks @E1i! - Three output lines stop claiming a case they are only true in

    Found by taking a live adopted monorepo through sync, sync --apply, the harness, init and doctor. The model was written and five claims recorded; the output made it read as though nothing had happened.

    doctor told an adopter that lint-policy stood on facts that all hold and that construct init would record it. It never will. The claim comes with the preset's sample sources, and init materializes those only into an empty directory — which the tree doctor inspects never is, because it carries a construct.json. The every-fact-holds reading now splits: it keeps that name and that promise only where a run in this repository really would write the claim, and reads sources-omitted where it would not, saying so instead. --json carries the fourth value under reading.

    init reported how many records it carried over and how many it added after the list of paths, so a second run read as a full re-materialization until the last line. The count, and the variables this run changed, now print before the list. What the run changed is reported before what it looked at.

    init's closing line named pnpm install && pnpm run quality whatever the run did. It now names install and the harness where the run wrote a package manifest, the harness alone where it changed other files, and nothing at all where it changed no file — which is what a third init on the same tree does.

    init chose the AGENTS.md and CLAUDE.md form from whether the file exists, when the question is which form the construct wrote. By the second run the file always exists, so a second init replaced the full document it had written itself with the short form meant for a repository that already had one — on a real adopted monorepo that silently removed the baseline command list. The form now comes from variants in construct.json, which records it, and appendBlock no longer counts the heading inside its own block as a document heading it must demote. A second and a third init on a tree the construct materialized from empty now leave both files exactly as the first run wrote them.

Patch Changes

  • #138 8b977cf Thanks @E1i! - Decision 0026: which packages exist is derived, what each package may import is recorded

    A second init on the monorepo preset re-derived allowedWorkspaceImports from the packages the first run created, recording 'packages/shared': ['@x/api'] where the first run recorded []. eslint.config.mjs is skipped on a re-run, so the file kept the strict policy and the record no longer matched it; sync reads that path as update, and sync --apply closes the gap by writing the looser policy into the file. Confirmed by running it.

    The variable answers two questions at once. Which packages exist is a fact about the tree. What each may import is a decision, and after the first run it is the owner's — re-deriving it is the construct overwriting what it does not own, which is the shape 0013 and 0006 already settled for the record.

    The record decides: the key set is derived every run, so a package the owner added is picked up; the allowances of a key already recorded are never re-derived; a new key gets the default the preset applies to a package of its kind, measured as apps/* may import every other workspace package and anything else may import nothing. Widening and narrowing stop being separate cases because a recorded value is not touched.

    The decision is recorded; its implementation and test are not, and the record says so and carries L0 rather than a level it does not have.

    The record carries two named constraints rather than leaving them to whoever implements it. A run that changes a policy variable names both values and what the change will do — naming both values has held since 0013, and the defect was read and not understood rather than invisible, so the consequence is the requirement and the delta is not. And the recorded allowances are kept by recording the structure beside the rendered form, never by parsing the map back out of the rendered source, which would make a formatting function the authority on what was decided.

  • #137 be73f5d Thanks @E1i! - An observation: the claim that a second init was fixed, from the day it was made to the day it failed

    Decision 0013 measured a second init on a 43-path tree, made construct.json additive and named variants among the branches that must survive. docs/guide/upgrading.md then concluded "That is fixed: the record is additive now." The second clause was true and tested; the first read as the second init is fixed, when what was fixed was its record layer. It shipped in v0.3.0 and stood through v0.12.2.

    The defect rode in on 0013's own sentence — the branches carry the previous entries, "then the entries this run wrote". AGENTS.md is written by every run, so the freshly computed variant always replaced the carried one, and the record handed the right answer to the caller that computed the wrong one. The tests held the record's self-consistency and not its stability: the carry-over assertion was guarded by a clause excluding every path the run wrote, and a case named is idempotent asserted four properties the replacement satisfies. Nothing compared what init rendered across two runs until this week.

    The entry is kept because it is the one claim in this corpus with a complete lifespan: when it was made, what it was measured on, the layer that evidence covered, the layer the sentence claimed, when it was falsified and by what. It is one claim in one repository and carries no rate.

0.12.2

Patch Changes

  • #132 f8f32a7 Thanks @E1i! - The discovery plan skill was not built, and an open question on interpretation freshness

    Written out by hand against this repository's model before any code, the plan came out empty: seven hypotheses, twenty-eight facts, all holding, zero items under each of the three decay kinds the skill was scoped to name. The emptiness is the result. Had anything decayed, doctor would already say so through the same projection, so the skill as scoped was a second rendering of what the report already carries.

    The observation also records the procedure, on its second use in a day: a plan written by hand before any code, as a check of necessity rather than a preparation for implementation. It sent one change to repairing its inputs and stopped this one from being built.

    A new open question sits beside the enforcement-capability one and answers something different: whether an interpretation has been reconsidered since the tree under its facts moved. Nothing is claimed to be stale — what is unknown is whether a reconsideration is owed. The obstacle is named too: answering it means reading what changed since a baseSha, which means git, and doctor executes nothing.

0.12.1

Patch Changes

  • #129 7fed773 Thanks @E1i! - Observation: a command that did not run, read as a measurement that did

    Two cases on one machine in one day, under the same broken shim: a scan reported pull request bodies clean from a run where the tool was never found, and an evidenceClean computation would have written true for five hypotheses from a git that never executed.

    Recorded as one shape rather than as two tool problems — a command fails, returns empty, and the empty is read as a successful measurement. The entry keeps the part that makes it worth recording: the failure was visible only because an unrelated expectation happened to contradict the value, and on a clean tree the failed measurement and the correct answer coincide exactly. Catching it was luck.

    The requirement it leaves is procedural, not a check for a missing binary: a result is not a measurement merely because it has the expected shape; the measurement must also evidence that it was performed. Where a hypothesis depends on one that cannot show it ran, no hypothesis is written.

  • #128 77f7db8 Thanks @E1i! - Discovery runs on this repository, and the open questions name what holds them

    The tool had interpreted a specimen, a corporate site and two adopted trees, and never its own repository: the model carried zero hypotheses. Running the materialized protocol here writes seven, each authoredBy: discovery with a computed baseSha and a computed evidenceClean, and doctor now reports hypotheses where it reported none.

    Three of the four open questions asserted something false about this repository and nothing could re-check them. Decision 0025 states why: a hypothesis stands on facts and is re-derived on every read, so it cannot go stale silently; a question stands on nothing and therefore can. Premises that fit the two fact kinds are now hypotheses the questions cite by id; the judgment halves stay prose and the marker says they age.

    The elision in the rendered picture now keeps the tail of a long label. Two facts on one long path rendered as the same visible line — the latent property recorded a few hours earlier, arriving on the first new data. The demonstration moves with it: the blind spot is now the middle of a line, not its end.

  • #131 18c435c Thanks @E1i! - The reading-that-never-happened observation no longer rests on one broken toolchain

    The entry recorded two cases under one broken shim, which left it readable as one machine's misconfiguration. The same reading arose in that session from a second, unrelated cause — a 403 from a proxy on the GitHub API — which has no shim under it at all. Zero matches distinguishes neither "the tool was not found" nor "the API refused": two sufficient causes, one empty result, and the result names neither.

    Recorded as reported by the reviewing session and not verified here. Its weight is that the form does not depend on the shim, so the opening no longer offers the shim as the explanation.

0.12.0

Minor Changes

  • #126 806dd4a Thanks @E1i! - doctor names the claims this preset can make and this repository does not carry

    Since the birth gate, a claim whose evidence does not hold is not written — which left doctor showing a short list and explaining nothing. It now names each claim the preset can make and the model does not carry, with the first fact that does not hold:

    Not claimed here: this preset can make these and this repository does not carry them. They have no
    level, because nothing is enforced by a claim that was never made.
      no-committed-secret — .github/workflows/security.yml does not carry what it would stand on.

    Nothing about this is stored. The expected set is rebuilt on every read from the preset and vars already in construct.json, compared with the model, and evaluated against the tree by the same machinery that evaluates the claims the model carries. No new entry, no schema change, no MODEL_VERSION bump, nothing written by init. A trace would state what was true at init and go stale in silence; a derivation stops being reported the moment it stops being true.

    The expected set is what the preset can claim, not what one init materialized: node-library ships no sample and therefore cannot make lint-policy at all, so its absence is a fact about the preset and is never reported. An absent claim carries no level, sits in its own block, and changes no exit code. Where the tree carries every claim its preset can make, nothing is printed.

    Decision 0024 records it, and closes 0020's open question as incorrectly posed: the answer was not to record the absence but that the absence needs no recording.

Patch Changes

  • #126 806dd4a Thanks @E1i! - An absent claim distinguishes evidence that fails from evidence that could not be read

    The first cut of the not-carried block tested evaluation !== 'holds', which merged two different states — this is false here and we could not look — and left the choice between them to the declaration order of supportedBy.

    Each absent claim now carries its reading: a fact that does not hold, a fact that could not be read, or every fact holding. Where both a failing and an unreadable fact are present the failing one is reported, because it is the one a reader can act on, and that preference is stated rather than inherited from list order.

0.11.1

Patch Changes

  • #124 eeeb89c Thanks @E1i! - The picture stops implying that colour carries enforcement strength

    On a default-preset tree an L0 claim nobody is obliged to read and an L3 claim that fails the build are the same green — true of this repository's own picture, where vulnerable-dependencies-are-visible is L0 and no-committed-secret is L3. The level is written inside each claim, but colour is read first and text second, so the page implied that green means fine when it means the facts named under it hold.

    One sentence now renders with the legend, because the legend is what explains the colour: colour carries the derived state and not the enforcement level, and each claim's level is written inside it. Nothing else on the page moves — no new channel, no new colour, no layout change — and stdout is untouched.

    Decision 0023 records what this does not do: the picture does not show enforcement strength and is not intended to, and the sentence prevents a wrong reading rather than supplying a right one. A visual channel of its own for strength was costed and set aside, deferred on the same condition 0023 already sets for interaction — built when there is a recorded reading of the picture, not before.

0.11.0

Minor Changes

  • #118 3ef5abf Thanks @E1i! - A construct.json from a later build is reported, not silently normalised

    upgradeManifest read any manifestVersion whatever its value: a manifest from a later build was treated as one from an earlier build, its unfamiliar branches discarded and manifestVersion rewritten down. Decision 0009 settled the backward direction and left this one open.

    Now a manifestVersion above what the binary understands throws a named error carrying both numbers, and every command that reads the manifest — doctor, sync, init, and cost where the environment does not already name the runtime — reports one line naming the version found, the version understood, and that a newer CLI is needed. Nothing is read and nothing is written.

    This does not help anyone already running an older binary. A published 0.1.1 will keep throwing on a v4 manifest; nothing here reaches it. The change is prospective: it makes the next shape change a reportable state instead of a second stack trace, and decision 0022 says so rather than reading as a repair of the crash that prompted it.

  • #122 cb8683c Thanks @E1i! - construct graph --out writes a picture you can open

    graph put Mermaid on stdout, which is the right machine-readable artifact and is not something a person can open: it needs a viewer that lives somewhere else. --out <path> now also writes one self-contained HTML file — inline SVG, inline styles, a few kilobytes, and nothing fetched when you open it: no CDN, no script, no network from a file:// URL or anywhere else.

    stdout is untouched and stays the default. The Mermaid comes out byte for byte as before, pinned by a fixture captured from the renderer as it stood before this change.

    The renderer is ours rather than an inlined Mermaid, on numbers taken before any code was written: the published package is 0.28 MB and Mermaid's minified bundle alone is 3.4 MB, so inlining it would grow the package roughly thirteenfold, weigh every rendered file at 3.4 MB, and make a tool whose invariant is that it executes no code it did not ship start shipping 3.4 MB of third-party JavaScript. Paying that before any person has read the picture inverts the order decision 0017 sets.

    Both outputs are serialized from one structure, so they can differ in layout and cannot differ about what is in the picture — a test holds them to each other. Decision 0023 records that, and records before the fact what would count as evidence the picture is used: one observable signal, and a plain statement that the others cannot be measured by a CLI that emits no telemetry.

  • #116 165fd6a Thanks @E1i! - harness-steps names every step of the quality script the construct writes

    The templates have always written pnpm composition:check into the quality script, and no fact named it. The claim said the harness runs lint, typecheck and tests, and stood on three needles — so it under-reported the script it was standing on, and a composition check silently dropped from that script would not have moved the claim.

    A file-contains fact for pnpm composition:check now sits under the claim, and its statement and mechanism name the step alongside the others.

    The needles are deliberately not widened to also match pnpm run …. A literal substring cannot tell one invocation from the other, and one that tried would be guessing at a script the construct did not write. The needle is the construct's signature on its own script; decision 0020 is what keeps it honest, by checking the facts before the claim is made.

    tests/harness-steps-facts.test.ts holds the template and the facts to each other in both directions: every needle must be a substring of the quality script each preset renders, and every step of that script must be named by a needle. The second direction is what the missing fact failed.

  • #115 4d74a48 Thanks @E1i! - A construct claim is written only where its evidence holds on the tree init just wrote

    init used to write facts it never evaluated. On a repository whose owner had written their own quality script, the harness-steps claim was grounded in needles looking for the construct's spelling of the harness steps — false at the moment they were written, and reported by doctor as unsupported from the first run. That is a finding about what the preset shipped dressed as a finding about the repository, which is the defect hook was removed for.

    Now a construct-authored claim is made only when every fact it declares holds on the tree, the facts nothing else stands on are not written, and init names each withheld claim with the evidence that failed. A claim already in the record is kept whatever its state, so drift still reads unsupported instead of disappearing; a claim whose evidence is merely unknown is still written, because not having looked is not evidence of absence.

    On an adopted repository this withdraws four claims, not one. A tree already carrying its own ci.yml and security.yml keeps only the claims standing on files the construct wrote. The report is shorter than it was — not because less is checked, but because less of it was pretending, which is the sentence 0.5.0 shipped under and now covers a larger set. Decision 0020 records what the construct stops asserting, that discovery is what may legitimately claim over the owner's own files, and the asymmetry this accepts: a withheld security claim and an absent security practice both read as silence.

Patch Changes

  • #117 b1a634a Thanks @E1i! - Decision 0021: a record of what a past version said is not edited

    0019 exempted a frozen fixture from the rewriting it otherwise requires, and gave the reason: the fixture states what a past version wrote. That reason was written as a property of one kind of file, and it is not one. A published release note listing an older version's harness steps was left alone for the same reason, and that file carries no specimen and no address — so the case cannot be a carve-out from a rule about how specimens are described.

    0021 states the class. A frozen fixture, a published release note, a dated entry in observations.md: the test is not the file's location but its tense. If an artifact's job is to state what was true then, it is not corrected when that stops being true, and what replaces the correction is a new dated record saying what changed.

    0019 keeps its fixture paragraph and gains a pointer, with no scope of its own — what may be named and what may be rewritten are two axes, and by this repository's own convention new scope takes a new number rather than widening an existing entry. The record reopens nothing: it states the rule those decisions were already following.

  • #112 e39ee77 Thanks @E1i! - Records describe a specimen by structure, never by address

    architecture/decisions/0019 promotes a rule that had been living as a clause inside one observation, where it governed nothing: a repository used as a specimen is described by its layout, role, package manager, relation to this tool and the artifacts the finding turns on — never by name, npm scope, owner, URL, identifying commit or problem domain. Where the address sits inside quoted tool output, the quotation is either dropped for a description or marked redacted, never silently edited.

    Nine sites that named specimens by address were corrected and one stale citation to a test that does not exist was fixed. Frozen fixtures under tests/fixtures/ are exempt by the record: they are what a past version wrote, not what this repository is still authoring.

    The record also states the cost rather than softening it. The three runs those entries describe are no longer reproducible: their addresses are not held anywhere this repository can cite, and 0001's findings corpus is a decision rather than a repository that exists. Addresses already published remain in git history and in pull request bodies; the rule governs records written from now on.

  • #119 2899345 Thanks @E1i! - doctor says what writes a construct.model.json

    On a repository without one, doctor reported the absence three times — in Enforcement, in Hypotheses and in YOU ARE HERE — and never named the command that creates the file. All three readings were correct and together they were a dead end: the sentence that resolves it existed only in the 0.5.0 release note, which is not where a person meets this.

    The Enforcement section now carries one further line, once, saying the file is written by construct init, that init is additive and overwrites nothing it does not own, and that nothing forces you to have one. The three existing readings are unchanged and the sentence is added beside them: an absent model is a state to explain, not a fault to repair, and it still does not move the exit code.

  • #123 56e289f Thanks @E1i! - Edge labels in the rendered picture cannot sit on top of each other

    The picture's stage labels were separated only where two edges ran between the same pair of nodes. Labels belonging to different claims were not touched, and on this repository's own model they stood 9 pixels apart in one column — the same illegible overprint the parallel-edge fix was meant to end, arriving by a route that fix did not cover.

    Label placement now excludes the collision by construction rather than detecting it: an anchor that would land within one line height of an already-placed one is pushed clear before it is written, so no rendered file can contain the forbidden state.

    tests/edge-labels-do-not-collide.test.ts holds the property, taking the threshold from the renderer's own constant rather than repeating a number, and its own description says what it holds: distance, not readability. Readability was found by a person opening the file, and a green suite here is not a claim that the picture reads well — only that no two labels are closer than a line.

  • #120 7114467 Thanks @E1i! - The add population is named, before anything is done about it

    A live run against an adopted repository left two construct-written artifacts that do not fit it. Neither is a conflict — the construct added them and the owner never touched them — so they read as ours and sit there inert or wrong.

    architecture/observations.md now records what add actually tests: a path absent from the tree, with no recorded sha, that the template groups produced. There is no notion of applicability in the classification at all. The only two conditionalities in the tool are onlyWhenEmpty mounts and the omittedGroups they produce, and both key on the tree being empty rather than on what the tree is.

    Every path the four presets produce is partitioned rather than sampled: 87 distinct paths, 37 reached only in an empty directory, and the remaining 50 across four kinds. Of the five kinds two are already conditional and one cannot misfit, so the population where a misfit can occur is exactly 23 paths plus the keys merged into package.json — and the observed pair fell one in each of the two.

    tests/add-population.test.ts checks the partition in both directions, so a new template either moves the record or fails the build. No fix ships here: nothing under templates/, src/sync/ or src/materialize/ changes.

  • #113 2ce9dcf Thanks @E1i! - Epistemic rule 10: true when written is not true when merged

    A sentence whose truth-maker is changed by the same diff that contains it is false on arrival. The rule names where such sentences concentrate — the passages that explain the change, and the artifact's own account of itself — and requires both to be reread against the finished diff before the change is done.

    It caught its own introduction. This file's first line counted the rules; adding rule 10 falsified that count, inside rule 10's own diff. Fixed in the same change, which is the rule's first application and its acceptance test.

    Evidence is two sentences written an hour apart in one afternoon, each inside a passage explaining the change it sat in. Two occurrences, one author, one file: a shape, not a rate. No mechanical check is proposed — the set of such sentences is given by meaning rather than form.

  • #121 c4140ee Thanks @E1i! - The upgrading guide knows about the model, and about a manifest it cannot read

    docs/guide/upgrading.md described a four-step loop that has never written a construct.model.json, so a repository carried forward from before 0.5.0 followed the page exactly and still had none. The page now carries the one upgrade case that needs init, why it is safe — the record is additive, and a construct claim is written only where its evidence holds, so the run cannot invent enforcement the tree does not have — and the fact that doctor says all of this itself.

    It also warns that the list of claims will be shorter than before rather than longer, in 0.5.0's own words: shorter not because less is checked, but because less of it was pretending. And it carries the one failure upgrading produces on its own, a stale CLI meeting a manifest from a later build, which now stops with a named line instead of a stack trace.

    Written from a run of the sequence, in order, against a tree materialized by an early 0.1.x release and carried forward with no model — structurally that tree and no other. sync, sync --apply, doctor before and after, init, and the later-manifest case were each run and their output is what the page quotes. pnpm run quality is the one step in the page not exercised there, because that tree has no installed toolchain; it is unchanged from before.

    No change to sync, init or any template.

0.10.2

Patch Changes

  • #110 02e15b0 Thanks @E1i! - construct graph shipped in 0.10.0 and was documented in the CLI reference and the README command table, and mentioned zero times in the walkthrough a new user actually reads. Reachable is not discoverable — the same gap the release sidebar had. Getting started now ends on the picture, which is the payoff of the walkthrough: init writes the files, doctor reports on them, and the graph shows what those reports are read out of.

    The example is the real rendering of a repository straight after init, not a sketch, and the documentation site now renders Mermaid fences as diagrams rather than as source.

    It also states the two things somebody meeting the model for the first time would otherwise discover by surprise: a repository with no construct.model.json draws nothing and says so, and init is what creates one.

    Why a plugin and not a build-time render, decided rather than defaulted. vitepress-plugin-mermaid works outside its declared support — it names vitepress: ^1.0.0 against this site's 2.0.0-alpha.20, and mermaid: 10 || 11 against mermaid 12, pinned here to 11. That is a real upgrade risk. The alternative, rendering to SVG at build time, removes it and ships no renderer to the client, but @mermaid-js/mermaid-cli peer-requires puppeteer, which puts a headless browser in every CI run, and the SVG becomes a generated artifact needing a writer and a staleness check.

    The deciding fact is that docs:build runs inside pnpm run quality, so a VitePress upgrade that breaks the plugin turns the harness red on the pull request that bumps it — the most visible moment rather than an unpredictable one. The client cost is lazy: mermaid is code-split across chunks loaded only when a diagram of that type renders, so pages without one pay nothing.

    The trigger for revisiting is written down: if a VitePress upgrade breaks the plugin, or a second diagram type pulls in chunks that are not lazy, render to SVG at build time instead. Migrating later costs roughly one edit per diagram page, which is why the trigger is recorded now rather than left to be rediscovered.

0.10.1

Patch Changes

  • #108 e6a214c Thanks @E1i! - The release index asserts that versions run newest first, and the next release is the first with a two-digit minor — the point at which a string comparison puts 0.10.0 below 0.9.0. The ordering was already numeric and the index takes its sequence from CHANGELOG.md rather than sorting it, so the assertion and the data cannot agree on one wrong comparison; both properties are now pinned by tests instead of being true by accident, with a mutation to a character comparison failing them.

    Checked before the release rather than by it, and of the same family as an assertion that pinned the newest version as a literal: code written while every minor was a single digit, correct up to the day it is not.

    Predicted, so it is not read as a defect. 0.10.0 is also the first version whose index anchor carries a two-digit minor. The link is built as #_0-10-0, by the rule observed in rendered output for single-digit minors, and nothing asserts that VitePress slugs the two-digit case the same way — deliberately, because reasoning about the slugifier is what docs:anchors exists to replace. So the first real check of that anchor happens in the version pull request that introduces 0.10.0. If it turns red there, docs:anchors is doing its job on the render rather than on an assumption, and the fix is the anchor, not the gate.

0.10.0

Minor Changes

  • #106 0cc12d0 Thanks @E1i! - The model has a picture you can look at: construct graph. The renderer that draws construct.model.json as a Mermaid flowchart was reachable only from this repository's own scripts; now every installation has it. The diagram goes to stdout so it pipes into a file or a viewer, and the states in it are derived on read by the same code doctor reports from — the command decides none of them itself.

    Absence stays a reading of its own: a repository with no construct.model.json gets a line on stderr and an empty diagram, a model that parses and names no entry gets a different line, and both exit 0, because nothing to draw is not a failure.

0.9.1

Patch Changes

  • #102 1d1a639 Thanks @E1i! - The documentation sidebar listed 0.5.0, 0.4.0 and 0.3.0 under Releases — the three versions that happen to have hand-written pages — so a visitor saw 0.5.0 as the highest number in the navigation and concluded that was where the project stood, while the index below listed everything through the current release. The gate added earlier was not at fault: it required every version to be reachable, and every version was. Reachable and prominent are different properties, and only the first had been asserted.

    The sublist is now computed at config time from the same functions the index renders from, so there is no generated artifact that can fall behind and no second writer to enumerate. A version with a hand-written page links to that page and stays named however old it gets; a version without one links to its own section in the index. pnpm docs:anchors checks those section links against the rendered HTML, because an anchor that misses still lands on the page and says nothing. It runs inside pnpm run quality rather than only in the docs deployment, which fires on pushes to the default branch and never on a pull request — and whose path filter did not cover the sources that generate the anchors.

0.9.0

Minor Changes

  • #95 905533b Thanks @E1i! - cli+templates: Every construct cost report names the version of the CLI that produced it — before the numbers in the text register, as version in --json — and the /implement instructions now require every figure in the closing usage line to name what measured it: the Workflow tool's own accounting, or construct cost at the version that command reports. A whole session of published cost figures came from a binary that reported 0.1.1 and, on inspection of the bundle itself, predates the response-deduplication fix — while the sources they were quoted against are at 0.8.0. Nothing in any of those numbers said so, and the version the binary reports turned out not to be enough on its own to place it. A hypothesis already records what tree it was read from; a cost figure recorded nothing, and that asymmetry is what this closes.

    No figure changes: the arithmetic is untouched and pinned by a test, and the two counting methods that are known to disagree remain unreconciled — while they are, provenance is what lets a reader see which of them a number came from.

  • #93 c727686 Thanks @E1i! - templates: A hypothesis records whether its own evidence was committed, not whether the tree was clean. baseClean becomes evidenceClean and speaks only of the files the facts under that hypothesis name. The first live discovery run on an adopted repository recorded false on every hypothesis and could not have recorded anything else — init writes forty-two files into the repository it adopts before discovery reads a line — so a required field had one reachable value and distinguished nothing. Scoped to the evidence it answers both ways on that same path: a hypothesis standing on the repository's own committed files reads true, one standing on a file init just wrote reads false. The discovery protocol now carries the command that computes it, git status --porcelain -- over the paths of that hypothesis's facts, under the same compute-it-never-estimate-it instruction as the sha256 one-liners. doctor's annotation says the evidence under the hypothesis was not committed when it was read, which is neither a doubt about the hypothesis nor a refutation of it. The schema is closed, so a model written with baseClean is rejected by name rather than ignored; nothing in the wild carries a hypothesis yet. The reasoning is in architecture/decisions/0018-evidence-clean-scopes-to-the-evidence.md.

  • #97 50f7978 Thanks @E1i! - cli: construct.model.json gets its third projection — a Mermaid picture, rendered through the same mechanism that already renders architecture/composition/*.yaml. pnpm model:render writes the graph into architecture/model.md, and pnpm model:check — wired into pnpm run quality — reports the committed block as stale when the model moves without it. Claims and hypotheses are nodes, the facts under them are nodes, and a fact several entries stand on is drawn once with one edge from each of them: that fan-in is the shape a list cannot show and the reason the projection exists. A claim's edges carry the stage they come from, so its two supportedBy lists stay apart.

    The renderer holds nothing of its own. Every state in it comes from deriveModelState, and the gate that guards doctor is repeated here: a renderer that decides a state from the model — from how many dependents a fact carries, from the level a claim declares — fails the test. A repository with no construct.model.json renders a sentence saying so rather than an empty diagram, and a model that parses and names nothing renders a different one: absence is a third reading, not emptiness.

Patch Changes

  • #96 a9a4674 Thanks @E1i! - The documentation site stops at a version that no longer exists. docs/release-notes/ carried three hand-written pages and the Releases nav link pointed at 0.5.0, while the changelog had already recorded nine more releases — the content existed and was simply never rendered, on the page a reader lands on when the tool did not work for them.

    A generated index at docs/release-notes/ now lists every version CHANGELOG.md carries, newest first, rendered by pnpm release-notes:render and committed the way the composition diagrams are. A release with a hand-written note — 0.5.0 and its upgrade sequence, which no changeset roll-up would produce — is linked rather than repeated, so hand-written notes stay the better thing where a release needs one.

    The floor is held by two tests rather than by remembering: one fails when the committed index drifts from the changelog, the other reads the versions from CHANGELOG.md and the wiring from the real VitePress config and fails in both directions — a version added to the changelog and wired nowhere, and wiring removed for a version that exists.

  • #99 fd8c76f Thanks @E1i! - templates: The discovery protocol's hypothesis step now tells the run to regenerate a rendered model where the repository has one, the way its composition step already says to run composition:render. Writing construct.model.json and leaving the artifact rendered from it behind turns the next harness run red for a reason nobody connects to the step that caused it.

    Found by sweeping every gate over a generated artifact for all the writers of its source, after a gate tested in both directions still shipped a release-blocking defect: both of its mutations had been performed by one writer, and a second one — the version bot — wrote the source and called no renderer.

  • #100 c103628 Thanks @E1i! - A test guarding the generated release index asserted order[0] was 0.8.0 — the newest version on the day it was written. Every release moves that value, so the check failed on the release after it shipped, for a reason that had nothing to do with what it was guarding. It now asserts the property it meant: the list is in descending version order, with more than one entry and a guard against the assertion holding vacuously.

    The same defect the repository keeps recording in other forms — a statement true of the present standing in for the property — this time inside a test written to enforce a property.

  • #98 c407022 Thanks @E1i! - The release index is generated from CHANGELOG.md, and changeset version writes CHANGELOG.md — so every version pull request bumped the changelog, left the generated page behind, and failed its own harness on the gate added to keep that page current. The gate was right and the pipeline was missing a step: version-packages now runs the renderer after changeset version, so the page is regenerated by the same step that invalidates it.

    A test resolves the version script the release workflow names, follows it through package.json, and fails when that chain no longer reaches the renderer — the state every release was in until now.

0.8.0

Minor Changes

  • #89 dfd6598 Thanks @E1i! - templates: discovery writes hypotheses into construct.model.json. The protocol gains a step of its own after the markers — not a reading of them: a marker is prose answering what is where, a hypothesis is a structural record answering what this is, standing on the same two fact kinds and no third, carrying the commit the run read from and whether that tree was clean. Everything it writes is authored by discovery, so the next init leaves it alone. An interpretation those two fact kinds cannot support stays prose in a marker, and one that looks as though it needs a third kind is recorded as an open question rather than inventing a way of knowing. The step where the run records what it wrote now names both addressees: provenance goes to construct.json and nowhere else, interpretation to construct.model.json and nowhere else.

    The step is instructions to an agent, which nothing enforces — L0. What ships proven is that the template materializes, that its worked example parses against the schema, and that a discovery-written hypothesis survives a second init while the construct's own half is rewritten byte for byte. That the netrunner actually comes back from the model with something written in it is shown by a live run on a real repository, and that run has not happened yet.

0.7.0

Minor Changes

  • #86 cb99c0c Thanks @E1i! - doctor reads back what the construct was taken to be. The result gains a knowledge-family hypotheses field: one entry per hypothesis in construct.model.json, carrying its statement, the base it was read from, and the state derived from the facts named under it — held, unsupported with the paths that no longer match, or unknown. The two ways of being unknown stay apart on the wire and in the report: a hypothesis whose facts could not be read says so and names them, and a hypothesis with nothing named under it says that instead of reading like a reading that failed. An empty list never passes for a repository that was looked at and found standing, and a hypothesis recorded against a tree with uncommitted changes is reported as one.

    Two defects on the hypothesis path go with it: the derivation kept only the state and dropped the reason, and it resolved with no facts in hand, so a fact that stopped holding was named by its id instead of the path it points at.

    The rule that a reading may not be worded as a verdict now covers hypotheses as well as claims, and lives in one place both read from rather than in the test beside one renderer. It matters more here than it did for claims: unsupported on a claim is a statement about a mechanism, while on a hypothesis it is a statement about what the repository is, so the false reading — you are not that — sounds more confident than the true one, which is only that the ground under the interpretation stopped matching. Both registers are held to it at the strings, because a rendered line in a test resolves to the plain register whatever theme it asks for.

0.6.0

Minor Changes

  • #85 6e56f8a Thanks @E1i! - A hypothesis now records the tree it was read from, not just the commit. Beside baseSha, construct.model.json requires baseClean: whether the working tree the run began reading carried no uncommitted change, before the run had written anything of its own. A construct that engrams an interpretation off a dirty deck should say so on the record, so a SHA in the model can no longer stand for bytes the interpretation was never formed from.

    Both fields are required and neither constrains the other — a repository with files and no commit is baseSha: null with baseClean: false. baseClean is a claim discovery writes about its own run, never a measurement anything can confirm later, and hypotheses carrying different bases coexist by design: the base is how a fresh interpretation is told from a stale one.

0.5.4

Patch Changes

  • #81 5916b79 Thanks @E1i! - The identifier scan classifies every JSON block in the documentation instead of selecting the ones it recognises.

    Selection caught the set narrowing — rename a table heading and it failed — and was blind to a block of a new shape never joining the scan at all. Nothing was missing, so nothing could be missed. A construct.model.json example added to the guide would have gone unscanned in silence, and its claim ids are exactly the identifiers at issue.

    Every block is now one of four kinds: a doctor result and a repository model, both scanned; a manifest and a sync report, both deliberately not, because their keys belong to other vocabularies. A block of any other shape fails the test and is named. Adding one forces a decision rather than slipping past.

    A model block has its claim ids and checkIds read while its structural keys — modelVersion, facts — are not treated as identifiers, so the second scanned kind needed no allowlist either.

    This is the same move as the field classification that admits a mixed value and the type that makes an unclassified field a compile error: name the forbidden state so it can be prohibited, rather than arranging for it not to arise.

0.5.3

Patch Changes

  • #78 62a317b Thanks @E1i! - cli: When red-gate, hook, construct-tests and weakestLink left doctor, the documentation went on naming them and nothing failed; a reader would have found out before the build did.

    The code now owns two lists instead of one. Current is derived and never written by hand — the keys of DOCTOR_FIELD_FAMILY plus every claim id and check id the model builder produces. Retired is the new exported list of identifiers this tool has published and no longer uses. Every identifier the docs and templates name must sit in one of them, so a removal can be described freely while a name in neither list fails the build. The two are asserted disjoint, which is the list's second and larger job: a retired name may never come back meaning something else, or every past mention would retroactively start saying something false.

    A mention is a token in code formatting — a key or an id value in a fenced JSON result, a single-backticked cell in the tables that list fields and verdicts — never a word in prose. The English word "hook" in the L2 level description is not an identifier, and a rule that flagged it would be demanding edits that make the documentation worse.

    What the scan deliberately does not read. Only blocks whose shape is a doctor result, and the field and verdict tables. The manifest and sync-report examples are left alone: their keys — manifestVersion, strategy, counts — belong to other vocabularies and are in neither list, so scanning them would have forced exactly the allowlist this design exists to avoid. A test states that exclusion rather than leaving it to be inferred from a regex.

    The gap that leaves is tracked rather than merely named. The scan selects the blocks it reads, and selection catches narrowing — a reworded heading fails — while being blind to a block of a new shape never joining the set at all. Replacing the selection with a partition, so that every JSON block must be classified and an unclassified one fails, is issue #79.

  • #77 8084a24 Thanks @E1i! - What it refuses to claim described a version that no longer exists, which is an uncomfortable thing for a page about not overstating.

    It taught the old three states — present, absent, unknown — which were the check vocabulary before verdicts became projections of the model. The states are held, unsupported and unknown now, and the page gives each one the reading it is not: held is not proven, because the facts under a claim are necessary and never sufficient; unsupported is not not enforced, because a named fact stopped matching and the report says which; unknown is not absent.

    It also still said the harness question is "always unknown, because proving it means running it". That verdict is gone. A blind spot is represented by a stated boundary now, not by a verdict manufactured to fill the space — an answer nobody can act on is not a smaller finding than no answer, it is a worse one, because it looks like a finding.

    The levels table carries the precondition every level above L0 was already assuming: the mechanism must be able to report a failure. And a new refusal joins the list — that a level it reports is proven — with the note that whether a mechanism could fail at all is an open question rather than something assumed either way.

    getting-started lists construct.model.json among the files a new repository gets, since it is committed and a reader meets it in their tree on the first run.

0.5.2

Patch Changes

  • #75 1d04d5b Thanks @E1i! - The baseline fix in 0.5.1 repaired two things and only one was tested. Files compared against stale init hashes were reported modified, which is what prompted the work; a path recorded only by sync was never examined at all, which produced no symptom and so appeared in no test. It shipped repaired and unheld, free to regress as quietly as it arrived.

    It is held now: a path the init record never contained is reported missing when it is deleted and modified when it is edited. Reverting the fix fails all three cases.

    The case that asserts silence while the path matches passes under the defect as well, because never looking is also silent. Only the cases demanding a positive report tell the two apart — which is why they are the ones that matter here.

0.5.1

Patch Changes

  • #71 211da72 Thanks @E1i! - doctor compared every path against the frozen init record and ignored everything sync had recorded since, so on any repository that has run sync --apply it reported the files sync had just written as modified. Found on a real tree, not a fixture: seven fabricated entries sitting beside nineteen genuine ones, with nothing in the output telling them apart, and one more added by every future sync.

    construct.json holds two records on purpose — the init record is frozen by decision 0006, and the sync record carries what has been written since. The latest recorded state for a path is the first overlaid by the second, which is what recordedShas has always returned and what sync itself reads. doctor simply did not use it. That was visible in the output before it was visible in the code: versionGap reached the sync record through replay while modifiedFiles did not, one sibling backed and the other bare.

    The same defect was in two more places. uncollectedTests looked for the runner config and enumerated recorded test files in the init record alone, so anything sync added was invisible to it. And init counted the records it carried over from an existing construct.json without the sync half, under-reporting what it kept and over-reporting what it added.

    Three occurrences make it structural rather than a bug to fix again, so the shared boundary is now enforced: reading manifest.files directly anywhere under src/ fails lint, with src/manifest.ts the single exemption, since it is the file that defines what the two records mean.

  • #71 211da72 Thanks @E1i! - The model every repository gets from init claimed vulnerable-dependencies-are-visible at L3, and the job behind it ships with continue-on-error: true in templates/base. A job with that flag is marked successful even when its step fails, so the check is green whether or not a vulnerability was found. The claim asserted a level the mechanism cannot reach, in the release that shipped the model, in every repository materialized by it.

    The level is now L0 and the mechanism says why: the audit runs on a schedule and on pull requests, reports into the log, and can never fail a check, so nobody is obliged to act on it.

    The scale reads L3 as "CI that does not block a merge", which superficially fits — but that wording presumes a check able to report a failure at all, and distinguishes L3 from L4 by whether the failure blocks. A check that is green in both worlds carries no information and sits below the scale.

    The mechanism was left as it is rather than made to fail. A dependency audit reads an external advisory database, so making it block would fail on news rather than on the change, which is presumably why the flag was set. Lowering the claim to the truth is the repair; raising the mechanism is a separate question with its own costs.

    This is rule 8 applied to the tool itself — the presence of a command is not the level at which it is enforced — and the first case where a claim was held on facts that were all true while the mechanism it named could not fail. supportedBy gives necessary conditions, never sufficient ones.

  • #73 73f4d54 Thanks @E1i! - The enforcement scale now states the assumption every level above L0 was already making: the mechanism must be able to report a failure.

    L3 read as "CI that does not block a merge", which literally describes a job carrying continue-on-error — it is CI, and it does not block. The wording presumed a check capable of failing and distinguished L3 from L4 by whether the failure blocks, without ever saying so. The distinction between L0 and L3 is whether a failure can be raised at all, and that half was never written down.

    A check that is green whether or not the invariant holds reports nothing and is L0, however much machinery stands behind it.

    This is a precondition being written out, not scope being added: it is what the levels already assumed, and exactly one record was affected by the gap — the dependency-audit claim corrected in this same release. Writing it now, while that single case is known and already repaired, means the sentence reclassifies nothing retroactively. Left for later it would silently move an unknown number of past records, and nobody would be able to tell a clarification from a change of scope.

0.5.0

0.5.0 — the tool stops asserting what it cannot show

0.4.0

0.4.0 — a number is worth what its counting method is worth

0.3.1

Patch Changes

  • 0070441 Thanks @E1i! - A green release run now has to mean the version is installable.

    Publishing 0.3.0 ended green — Successfully published, a git tag, a GitHub release — with nothing on the registry. The version had gone into npm's staged-publish state, which a stage-only trusted publisher produces by design and which no CI token can approve; the next run exposed it with 409 Cannot publish over previously staged version. The pipeline had reported a success the world did not contain.

    A separate Release verification workflow now asks the registry about the version in package.json after every release, and can be re-run on its own once a human approves a staged version — re-running the release itself would only publish again and fail on the 409.

    It answers with three outcomes rather than a boolean, because the rule this release is built on applies to its own guards: installable, absent, and unreachable for a request that could not be made. A network error is never read as a missing version. And when a version is absent the message names both worlds it could mean — a staged publish awaiting approval, or a publish that failed while reporting success — because CI cannot tell them apart and picking one would be the same defect again.

  • #42 5b20037 Thanks @E1i! - The documentation link comes first, where a reader on npm actually sees it.

    The link to the site existed but sat below the install snippet and the version note, which on the npm package page is under the fold. It is now the line directly beneath the description, with the four destinations worth naming: the site, getting started, the development cycle and the CLI reference.

    A test keeps it that way and keeps it true: every e1i.github.io link in the README must resolve to a page this repository builds, and the documentation must be named before the install snippet. The README travels to npm, where nothing checks it and a dead link stays dead until the next release.

0.3.0

0.3.0 — a claim is worth what its enforcement is worth

0.2.0

Minor Changes

  • #13 04ba003 Thanks @E1i! - construct cost stops reporting two different facts as one. A missing project directory meant both "this runtime does not expose per-run usage" and "nothing has been run here yet", and the command printed the more damning reading of the two — so a Cursor user was told nothing was recorded when the truth was that their runtime never records it. Behind a CostSource interface, the command now resolves the runtime it is actually running under and answers ok, empty, unsupported, mismatch or unknown, each with its own exit code and its own line. --json is an object carrying the status, the runtime and the project key that was looked up. A key that misses because the repository was reached through a worktree, a symlink or another path is named as such instead of being reported as absence, and where the evidence does not settle it the answer is unknown rather than a guess.

  • #17 7e8ca83 Thanks @E1i! - Discovery fills the markers in AGENTS.md and the invariants table, and once filled nothing distinguished what the tool wrote from what the repository's owner stands behind. construct.json now records it: discovery.baseSha, discovery.filledAt, and per marker the file it lives in, who authored it and the sha256 of the body discovery wrote. The marker itself stays a document a person reads — provenance in the prose would spoil the document and would put the record in the one place most likely to be edited.

    Authorship by the owner is never declared, only derived: a marker whose body no longer matches its recorded sha reads as theirs, with no command to run and nothing written back. Editing it by hand is the only evidence needed. doctor names the markers that still read back, word for word, what the tool wrote — the places where the repository is quoting the construct at itself — and reports it without making it a gate or changing an exit code.

    construct.json also carries an integer manifestVersion, separate from construct, which is the CLI version; conflating a schema version with a product version is what makes a later migration undecidable. Manifests written by 0.1.x are normalised on read by a pure upgrade, so a repository initialised before this change gets a report instead of a crash. Their markers read as unknown, never as the construct's: a legacy manifest carries no provenance, and claiming otherwise would have this feature produce exactly the lie it exists to prevent.

  • #12 39f8569 Thanks @E1i! - cli: doctor reports an enforcement level instead of matching substrings. Five checks — lint-policy, construct-tests, ci, hook, red-gate — each return {id, level, state, evidence}, with level from L0 (text, or a command nobody is obliged to run) to L3 (CI), state one of present, absent or unknown, and evidence naming the file or key that was read. The report ends with one line naming the weakest link: the lowest level among the gates the repository claims. --json gains warnings, checks and weakestLink after the fields it already emitted, which keep their names and meaning; the exit code is unchanged, so a low level is information, not a failure.

    doctor executes nothing from the repository it inspects — no child process, no dynamic import of a path inside it, no require into its node_modules, no call into its ESLint or Vitest APIs — because it is run through npx in a clone nobody has decided to trust yet, and a flat ESLint config is a module. The lint policy forbids those forms under src/** and tests/dependency-policy.test.ts lints one sample per form. Because branch protection lives in the GitHub API and not in a file, doctor never claims L4, ci is never absent, and the red gate is always unknown and says so.

  • #15 26e449a Thanks @E1i! - construct cost now reads the run ledger and reconciles it against what the runtime exposes, joining on the runtime's own run identifier — which the /implement skill step records from here on. Both directions are reported and counted: an entry whose run has no session, and a session with no entry. Neither is an error; they are the two ways a record and a reality drift apart, and seeing the drift is the point. Pairing entries to sessions by time is deliberately not done — that is a guess presented as a finding. Entries written before the key existed are counted as unjoinable, lines that do not parse or lack a declared field are reported with their line number and the exact field path, and a token value of unknown is never read as zero, because zero is a number and it would be a lie.

    The ledger's declared schema is what the writer can actually produce and no more: the workflow returns aggregate accounting for a run, never a row per agent, so no per-agent field is declared. Declaring a field nobody writes is the same defect as claiming an enforcement nobody performs. docs/cli.md says plainly that the ledger is written by a step of a skill and is therefore L0 — a record nobody is obliged to keep.

  • #19 7664355 Thanks @E1i! - The lint policy checks never reached a real repository. They lived in each preset's sample group, and a sample is materialized only into an empty directory — so init against a repository that already has code, the case this tool exists for, wrote the policy and skipped the test that proves it fires. doctor had been reporting lint-policy L0 absent and was right. The tests now ship in baseline, and arrive whether the directory is empty or not.

    node-frontend declared three restrictions and shipped no test that any of them fires; it now has one, and the restrictions cover their class — a computed member reaches the same method, and binding an element's classList or style to a local name steps around a selector matched on the member expression. Reverting any of them to its narrow form fails the new tests.

    node-library is deliberately untouched and still reports lint-policy L0 absent. It declares no syntax policy — its groups are the base and the harness, and the harness config carries no restriction of the construct's — so absent is a true reading rather than a missing file, and docs/cli.md now says so where the check is documented. Manufacturing a policy so that a report turns green is the defect this tool exists to find.

  • #7 f22bcdf Thanks @E1i! - The policies the presets ship were shape matches on one spelling each, and the same operation written another way walked past: await import('@scope/shared'), const { env } = process, globalThis.process.env, const { body } = req, and — worst of the set — sql.raw`select 1` , the tagged form that NO_RAW_SQL's own message tells you to use. Each restriction in the monorepo and node-backend presets now covers its class, including binding process or req to a local name, while keeping every role's exemptions exactly as they were.

    The durable half is the test. syntax-policy.test.ts compared resolved selector strings against the same strings restated in the test — proof that a restriction is attached, never that it fires. Both presets now lint real source per role from a single per-role table: one sample per restricted form expecting a report, each role's exempt forms expecting none.

    Named limit: in the monorepo, a package that binds process to a local name and reads .env off it is still not reported. That restriction is env-specific by design, and widening it would change what the rule means rather than what it catches.

Patch Changes

  • #8 7f07169 Thanks @E1i! - Acceptance now runs the artifact that actually ships. Each leg installs the packed tarball into a directory outside the repository and invokes the installed construct binary for init and doctor, instead of node dist/cli.js out of the working tree. That is what publishing exercises: externals tsup leaves out must resolve from the installed package's own dependencies, files must carry templates/ or the first template read fails, and bin must point at the built entry — running from the workspace proves none of the three, which is why nine legs went red at once. A drift guard asserts no bare import under src/ resolves to a devDependency; it is green today and stays that way on purpose.

  • #18 045efca Thanks @E1i! - The README's cost paragraph is rewritten from thirteen measured runs instead of three, and it now says something different. The old text argued that the reasoning class predicts the price. It does not: nine medium runs spanned 847k to 5.9M. What the measurements show is that almost the entire cost of a run is each agent's entry into the repository — a fresh exploration, paid in full before anything is produced and paid again by every agent that starts. Two low runs cost 557k and 740k with two agents each; two high runs cost 14.19M and 14.14M with three. The class decides how deep an entry goes; the ladder decides how many entries there are, which is the claim the tool should be making.

  • #10 728efc7 Thanks @E1i! - Six fixtures for doctor under tests/fixtures/doctor/, written before the checks that will read them and asserting the wrong answer on purpose. Five repositories are objectively broken — an eslint config that never loads the construct policy, construct tests outside the runner's globs, a quality script that is red on a clean checkout, a quality script no workflow runs, a command with no hook to run it — and today doctor calls all five healthy. Each test says so in its title. A fixture written after the check can only confirm what the check already does; a fixture written first has to reproduce the lie. The expectations sit in one table keyed by the fixture directory, and a cross-check fails when a fixture has no row or a row has no fixture, so a check can never arrive without something that proves it can fail.

  • #14 21b65d3 Thanks @E1i! - The ladder's output contract was declared twice and the duplicate was the weaker of the two. The runtime validates against a schema; the agent files then restated the same contract in prose and closed with a fenced JSON example, which reads to an agent as "format your answer as text that looks like this" — the likely cause of a run where five answers in a row came back invalid, and a contradiction of decision 0005, which this repository had already taken. The fenced block is gone from the agent files here and in the templates; what remains is a list of the fields and what each one means. Alongside it: a schema-rejected response now retries with the validator's complaint in the prompt instead of a bare "the previous attempt failed", the retry limit is a parameter rather than a literal, and every failed attempt is recorded with a reason that tells a bad shape apart from a red harness and from a blocked report.

  • #5 3116836 Thanks @E1i! - Black ICE on the package boundary: pnpm run quality now runs a privacy guard over templates/, docs/ and README.md. Domains are permitted by an explicit allowlist — a denylist in a public repository names the very thing it hides — and any host in a URL or an email address is checked whatever its top-level domain. Home directory paths (/Users/<name>, /home/<name>, ~/<name>) are refused outright, one such path is gone from the sample transcript in docs/cli.md, and a test asserts the published file list carries nothing under .construct/, findings/ or runs/.

  • #16 27cf209 Thanks @E1i! - The ladder no longer re-asks an agent whose response the schema rejected: retryLimit defaults to 0, and the run stops with the validator's error where a person can read it. This default has a measured price tag, which is rarer than it should be.

    A run was killed when the architect failed structured-output validation five times and hit the runtime's own retry cap. Re-running the identical task after the duplicated output contract was removed produced a valid spec on the first attempt. Comparing the two: the failed architect cost 3,658,281 billable tokens for nothing, the successful one 3,118,576 — so the four extra schema attempts inside a single call were worth about 135k each, not the millions they looked like. That cap belongs to the runtime and is not ours to set.

    What is ours is retryLimit, and it counts whole additional agent calls. Each one repeats the agent's exploration of the repository from scratch — about three million tokens for an architect — and it cannot fix a contradiction in the brief, because the agent is not allowed to change the brief. The first attempt already carries the runtime's five internal tries; a second full call buys a rerun of the same misunderstanding at a thousand times the price of one schema retry. Stopping and showing a human the validator's complaint costs nothing and took about a minute to act on when it happened.

  • #9 51921a5 Thanks @E1i! - Two claims that were made but never checked are now tests. Materialization is deterministic: for every preset and every AI target, two init runs into empty directories with fully pinned variables produce identical file lists and identical hashes, so a future template that reaches for a Date, an unordered Set or an unsorted walk fails the gate instead of shipping. And the claim that this repository runs on its own construct is now a replay compared against the repository root, with every difference declared in architecture/self-hosting-drift.yaml with a reason — ten of them, measured, not assumed — failing in both directions so the list cannot rot into decoration.

    Deliberately not done: the replay is never compared against the sha256 map in construct.json. That manifest was written by 0.1.0; comparing today's templates to it compares two versions and calls the result reproducibility. Decision 0006 freezes what init wrote and scopes the freeze to that branch only, since provenance will write the discovery branch later. The boundary is stated in the test itself: what reproduces is materialization; discovery does not, and pretending otherwise would be the defect this work exists to catch.

  • #6 7cbf62d Thanks @E1i! - The ICE was painted on, not wired: the invariant "the CLI spawns exactly one child process" was enforced by a single selector matching a static import of node:child_process, so await import('node:child_process') and createRequire(target)('eslint') walked straight through. The spawnPolicy block now fails the build on the whole class — any dynamic import() whatever its specifier, require calls and require.* access, node:module and createRequire — anywhere under src/ except the pnpm version probe, and tests/dependency-policy.test.ts lints one real source sample per form instead of comparing selector strings. The invariants table now names the mechanism rather than the word lint.

0.1.3

Patch Changes

  • #3 b3051b6 Thanks @E1i! - cli: a pnpm-workspace.yaml or workspaces field counts as a monorepo only when it lists packages. Since pnpm 10 that file also carries settings such as minimumReleaseAge and allowBuilds, and the construct ships one in every preset, so a generated single-package project reported itself as a monorepo and a second init suggested the wrong preset.

0.1.2

Patch Changes

  • 1cf5709 Thanks @E1i! - cli: document the commands. The README now opens with a real first run and a Usage section covering a new project, an existing repository, agent targets and the read-only commands; docs/cli.md is the full reference with every flag, worked examples and exit codes.

0.1.1

Patch Changes

  • b34f3a8 Thanks @E1i! - templates: gitleaks allowlists construct.json

Names are a tribute to Cyberpunk 2077. Not affiliated with CD Projekt Red.