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
62d8a2aThanks @E1i! - An observation: a true output read for the wrong relation, and the refusal that ended itA 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 -3was read as the three newest versions; it answers the last three lexicographically, and here those arev0.8.0,v0.9.0,v0.9.1, because9sorts after1. From that reading followed a count of untagged releases, a claim about what the release action pushes, and a claim that the header comment inrelease.ymlis false — two briefed pull requests, and a correction drafted for a changeset.git tag --sort=v:refnameended it: 31 tags against 32 published versions, one gap at0.1.0, which is the versionrelease.ymlalready 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-filesin 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
9016086Thanks @E1i! - Two open questions: a check that can never be green, and a path that left no evidencedoctorexits 1 on this repository for two conditions that are legitimate and permanent —tsconfig.base.jsonrecorded in the manifest'ssyncbranch and absent from the tree, and fifteen baseline files modified sinceinit, 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 thatdoctorcan 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 totemplates/harness/tsconfig.base.json, andgit log --all --followover 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 commit9c00c33says--applywrote the path and that it was then deleted; the owner does not recall deleting it. A pathsync --applywrites 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
syncrecord is a record of the past.#155
9428adeThanks @E1i! - Why a marker with no recorded provenance is not backfilled, said where the reader meets the gapdoctornow 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.mdsays this under the table of readings, so a reader who meetsunrecordedthere 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, andunrecordedstays until a run rewrites that marker.This repository's own
open-questionsmarker 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
ed97251Thanks @E1i! - Two pointers in this repository's own markers named a file that is not hereAGENTS.mdpointed atdocs/PLAN.mdtwice — once inmodule-map, listing it among the directories the layout table does not cover, once inhigh-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 acrossarchitecture/decisions/,architecture/observations.mdanddocs/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
8d21eacThanks @E1i! - Two source files imported across the dependency policy with no boundary to hold them to itAGENTS.mdsays dependencies insidesrc/point one way and thateslint.config.mjsenforces it.ALLOWED_INTERNAL_IMPORTSkeyed 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 insrc/ui, andsrc/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.tsandsrc/version.tscarry 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.tsmay reachuiand nothing that holds a record, andsrc/cli.tsmay reach the commands, the presets, the vocabulary, the version anddetect, but not the manifest, the model, the materializer orsyncbehind them. Both cases are intests/dependency-policy.test.ts.No behaviour changes: nothing in
src/moved, and the new rules are satisfied by the tree as it stands.#159
6b22a07Thanks @E1i! - An open question: the machine-readable output declares nothing and refuses nobodyconstruct.jsondeclaresmanifestVersionandconstruct.model.jsondeclaresmodelVersion, and each refuses a record written by a later build through one shared error.doctor --jsondeclares nothing:src/cli.tsserialisesDoctorResultas 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-questionsmarker with the measurement behind it: nothing the construct materializes callsdoctor --json— not the templates, not the workflows, notconstruct-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
2cf0662Thanks @E1i! - Release verification says which of three states left the version absent, and derives the thirdmikoshi-construct@0.15.0was 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
424df7fThanks @E1i! - Discovery provenance reports every state it computes, and the three causes of "unknown" become three statesdoctorderived four things about a discovery marker and printed one.markerAuthorshipansweredunknownfor 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. Soowner, 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
unknownwith no sha, one recorded as construct-authored whose body no longer hashes to its recorded sha.doctorprinted no provenance section at all.The incoherent record is now unconstructable rather than handled.
MarkerProvenanceis a union of the two combinations that can occur —constructwith the sha of the body that run wrote, orunknownwithnull— soconstructwith no sha cannot be expressed.upgradeManifestis the one door such a record can arrive through, and it reads it back asunknownwithnull. That removes one of the three causes at the type level and leaves two real states, which is whyauthorshiphas four members and no flag:reading what 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.
authorshipindoctor --jsonno longer emitsunknown; it emitsunrecordedorunreadable, 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
c4411fbThanks @E1i! - Two observations: the cells of a blind discovery run, written before it, and what the run producedA 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.mdnames 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
2cf0662Thanks @E1i! - 0.15.0 and 0.16.0 each carry a dated notice naming the otherThe 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
fda1950Thanks @E1i! -/planin Claude Code requires a criterion to be seen failing before the changeThe 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
/planequivalent. 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.0tag 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
4dd1b57Thanks @E1i! - A construct.model.json from a later build is a named state, not a schema failureA model declaring a
modelVersionthis 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 exits1without reading or writing anything.doctor,graph,syncandinitall inherit it, because all four read the model.The state was not built twice. One already existed for
construct.jsonand 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.
readModelreturnsnullwhen the file is absent, andnullalready means no model, whichdoctorexplains by sayingconstruct initwrites one. Had a version-ahead model collapsed into that reading,doctorwould have told someone holding a newer model to runinit— proposing to overwrite the file it had just failed to read.Nothing migrates an older model, no fact kind is added, and
MODEL_VERSIONdoes not move.
Patch Changes
#148
70f8c07Thanks @E1i! - An observation: an acceptance played a second role at 0027's first useDecision 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
71ff5d2Thanks @E1i! - Decision 0027: an acceptance is red before the implementation existsThe 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-errorfails 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
f7c9da5Thanks @E1i! - Three observations: rules without carriers, plans that ended in not building, and six stops on a false premiseA 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
f1b1728Thanks @E1i! - The workspace policy is recorded, not derived again on every runDecision 0026, implemented.
allowedWorkspaceImportsanswered two questions at once: which packages exist, which is a fact about the tree, and what each may import, which is a decision. A secondinitre-derived both, so a leaf recorded as importing nothing came back allowed to import the app, andsync --applythen wrote that looser policy intoeslint.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 thateslint.config.mjsis not rewritten there so a latersync --applywould 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
initon 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 toeslint.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.jsoncarries the policy as structure underpolicyand declaresmanifestVersion5. 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
341ff2cThanks @E1i! - The CLI reference carries the line the picture has carried since 0.11.1docs/cli.mddescribes 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
c4b8c6aThanks @E1i! - The written count says which of two questions it answersinitprinted "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 changedon a first run,4 files, 0 changedon a second,5 files, 1 changedwhere 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
d640935Thanks @E1i! - A second init reads the record instead of asking the directory, and instead of asking youThree places where
initderived an answer from the state of the directory whenconstruct.jsonalready held it. All three are only wrong from the second run onward, which is why the rollout is what triggers them.A second
initdeleted thelint-policyclaim fromconstruct.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 andmergeModeldropped 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 andsyncalready computed the same way; the reading is one function both commands call. Three runs on a tree materialized from empty now leaveconstruct.model.jsonbyte-identical from the second run on, carrying the same claims the first run wrote.doctorfollows the same reading, so the two cannot disagree. Where the construct did materialize the sample and the owner deleted the claim by hand,doctornow readsevery-fact-holds— a run here really would record it — instead of promising the opposite.A second
initasked 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.initagainst amanifestVersionfrom a later build writes nothing, whichdocs/cli.mdhas asserted all along and nothing held. It is now a test.#134
e2db78cThanks @E1i! - Three output lines stop claiming a case they are only true inFound by taking a live adopted monorepo through
sync,sync --apply, the harness,initanddoctor. The model was written and five claims recorded; the output made it read as though nothing had happened.doctortold an adopter thatlint-policystood on facts that all hold and thatconstruct initwould record it. It never will. The claim comes with the preset's sample sources, andinitmaterializes those only into an empty directory — which the treedoctorinspects never is, because it carries aconstruct.json. Theevery-fact-holdsreading now splits: it keeps that name and that promise only where a run in this repository really would write the claim, and readssources-omittedwhere it would not, saying so instead.--jsoncarries the fourth value underreading.initreported 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 namedpnpm install && pnpm run qualitywhatever 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 thirdiniton the same tree does.initchose theAGENTS.mdandCLAUDE.mdform from whether the file exists, when the question is which form the construct wrote. By the second run the file always exists, so a secondinitreplaced 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 fromvariantsinconstruct.json, which records it, andappendBlockno longer counts the heading inside its own block as a document heading it must demote. A second and a thirdiniton a tree the construct materialized from empty now leave both files exactly as the first run wrote them.
Patch Changes
#138
8b977cfThanks @E1i! - Decision 0026: which packages exist is derived, what each package may import is recordedA second
initon the monorepo preset re-derivedallowedWorkspaceImportsfrom the packages the first run created, recording'packages/shared': ['@x/api']where the first run recorded[].eslint.config.mjsis skipped on a re-run, so the file kept the strict policy and the record no longer matched it;syncreads that path asupdate, andsync --applycloses 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
be73f5dThanks @E1i! - An observation: the claim that a second init was fixed, from the day it was made to the day it failedDecision 0013 measured a second
initon a 43-path tree, madeconstruct.jsonadditive and namedvariantsamong the branches that must survive.docs/guide/upgrading.mdthen concluded "That is fixed: the record is additive now." The second clause was true and tested; the first read as the secondinitis 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.mdis 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 whatinitrendered 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
f8f32a7Thanks @E1i! - The discovery plan skill was not built, and an open question on interpretation freshnessWritten 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,
doctorwould 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 meansgit, anddoctorexecutes nothing.
0.12.1
Patch Changes
#129
7fed773Thanks @E1i! - Observation: a command that did not run, read as a measurement that didTwo 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
evidenceCleancomputation would have writtentruefor five hypotheses from agitthat 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
77f7db8Thanks @E1i! - Discovery runs on this repository, and the open questions name what holds themThe 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: discoverywith a computedbaseShaand a computedevidenceClean, anddoctornow 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
18c435cThanks @E1i! - The reading-that-never-happened observation no longer rests on one broken toolchainThe 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
806dd4aThanks @E1i! - doctor names the claims this preset can make and this repository does not carrySince the birth gate, a claim whose evidence does not hold is not written — which left
doctorshowing 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, noMODEL_VERSIONbump, nothing written byinit. A trace would state what was true atinitand 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
initmaterialized:node-libraryships no sample and therefore cannot makelint-policyat 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
806dd4aThanks @E1i! - An absent claim distinguishes evidence that fails from evidence that could not be readThe 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 ofsupportedBy.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
eeeb89cThanks @E1i! - The picture stops implying that colour carries enforcement strengthOn 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-visibleis L0 andno-committed-secretis 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
3ef5abfThanks @E1i! - A construct.json from a later build is reported, not silently normalisedupgradeManifestread anymanifestVersionwhatever its value: a manifest from a later build was treated as one from an earlier build, its unfamiliar branches discarded andmanifestVersionrewritten down. Decision 0009 settled the backward direction and left this one open.Now a
manifestVersionabove what the binary understands throws a named error carrying both numbers, and every command that reads the manifest —doctor,sync,init, andcostwhere 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
cb8683cThanks @E1i! - construct graph --out writes a picture you can opengraphput 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 afile://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
165fd6aThanks @E1i! - harness-steps names every step of the quality script the construct writesThe templates have always written
pnpm composition:checkinto thequalityscript, 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-containsfact forpnpm composition:checknow 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.tsholds 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
4d74a48Thanks @E1i! - A construct claim is written only where its evidence holds on the tree init just wroteinitused to write facts it never evaluated. On a repository whose owner had written their ownqualityscript, theharness-stepsclaim was grounded in needles looking for the construct's spelling of the harness steps — false at the moment they were written, and reported bydoctorasunsupportedfrom the first run. That is a finding about what the preset shipped dressed as a finding about the repository, which is the defecthookwas 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
initnames each withheld claim with the evidence that failed. A claim already in the record is kept whatever its state, so drift still readsunsupportedinstead 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.ymlandsecurity.ymlkeeps 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
b1a634aThanks @E1i! - Decision 0021: a record of what a past version said is not edited0019 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
e39ee77Thanks @E1i! - Records describe a specimen by structure, never by addressarchitecture/decisions/0019promotes 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
2899345Thanks @E1i! - doctor says what writes a construct.model.jsonOn a repository without one,
doctorreported the absence three times — in Enforcement, in Hypotheses and inYOU 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, thatinitis 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
56e289fThanks @E1i! - Edge labels in the rendered picture cannot sit on top of each otherThe 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.tsholds 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
7114467Thanks @E1i! - The add population is named, before anything is done about itA 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.mdnow records whataddactually 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 areonlyWhenEmptymounts and theomittedGroupsthey 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.tschecks the partition in both directions, so a new template either moves the record or fails the build. No fix ships here: nothing undertemplates/,src/sync/orsrc/materialize/changes.#113
2ce9dcfThanks @E1i! - Epistemic rule 10: true when written is not true when mergedA 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
c4140eeThanks @E1i! - The upgrading guide knows about the model, and about a manifest it cannot readdocs/guide/upgrading.mddescribed a four-step loop that has never written aconstruct.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 needsinit, 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 thatdoctorsays 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,doctorbefore and after,init, and the later-manifest case were each run and their output is what the page quotes.pnpm run qualityis 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,initor any template.
0.10.2
Patch Changes
#110
02e15b0Thanks @E1i! -construct graphshipped 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:initwrites the files,doctorreports 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.jsondraws nothing and says so, andinitis what creates one.Why a plugin and not a build-time render, decided rather than defaulted.
vitepress-plugin-mermaidworks outside its declared support — it namesvitepress: ^1.0.0against this site's2.0.0-alpha.20, andmermaid: 10 || 11against 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-clipeer-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:buildruns insidepnpm 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
e6a214cThanks @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 puts0.10.0below0.9.0. The ordering was already numeric and the index takes its sequence fromCHANGELOG.mdrather 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.0is 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 whatdocs:anchorsexists to replace. So the first real check of that anchor happens in the version pull request that introduces0.10.0. If it turns red there,docs:anchorsis 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
0cc12d0Thanks @E1i! - The model has a picture you can look at:construct graph. The renderer that drawsconstruct.model.jsonas 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 codedoctorreports from — the command decides none of them itself.Absence stays a reading of its own: a repository with no
construct.model.jsongets a line on stderr and an empty diagram, a model that parses and names no entry gets a different line, and both exit0, because nothing to draw is not a failure.
0.9.1
Patch Changes
#102
1d1a639Thanks @E1i! - The documentation sidebar listed0.5.0,0.4.0and0.3.0under Releases — the three versions that happen to have hand-written pages — so a visitor saw0.5.0as 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:anchorschecks those section links against the rendered HTML, because an anchor that misses still lands on the page and says nothing. It runs insidepnpm run qualityrather 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
905533bThanks @E1i! - cli+templates: Everyconstruct costreport names the version of the CLI that produced it — before the numbers in the text register, asversionin--json— and the/implementinstructions now require every figure in the closing usage line to name what measured it: the Workflow tool's own accounting, orconstruct costat the version that command reports. A whole session of published cost figures came from a binary that reported0.1.1and, on inspection of the bundle itself, predates the response-deduplication fix — while the sources they were quoted against are at0.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
c727686Thanks @E1i! - templates: A hypothesis records whether its own evidence was committed, not whether the tree was clean.baseCleanbecomesevidenceCleanand speaks only of the files the facts under that hypothesis name. The first live discovery run on an adopted repository recordedfalseon every hypothesis and could not have recorded anything else —initwrites 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 readstrue, one standing on a fileinitjust wrote readsfalse. 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 withbaseCleanis 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
50f7978Thanks @E1i! - cli:construct.model.jsongets its third projection — a Mermaid picture, rendered through the same mechanism that already rendersarchitecture/composition/*.yaml.pnpm model:renderwrites the graph intoarchitecture/model.md, andpnpm model:check— wired intopnpm 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 twosupportedBylists stay apart.The renderer holds nothing of its own. Every state in it comes from
deriveModelState, and the gate that guardsdoctoris 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 noconstruct.model.jsonrenders 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
a9a4674Thanks @E1i! - The documentation site stops at a version that no longer exists.docs/release-notes/carried three hand-written pages and theReleasesnav 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 versionCHANGELOG.mdcarries, newest first, rendered bypnpm release-notes:renderand 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.mdand 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
fd8c76fThanks @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 runcomposition:render. Writingconstruct.model.jsonand 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
c103628Thanks @E1i! - A test guarding the generated release index assertedorder[0]was0.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
c407022Thanks @E1i! - The release index is generated fromCHANGELOG.md, andchangeset versionwritesCHANGELOG.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-packagesnow runs the renderer afterchangeset 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
dfd6598Thanks @E1i! - templates: discovery writes hypotheses intoconstruct.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 bydiscovery, so the nextinitleaves 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 toconstruct.jsonand nowhere else, interpretation toconstruct.model.jsonand 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
initwhile 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
cb99c0cThanks @E1i! -doctorreads back what the construct was taken to be. The result gains a knowledge-familyhypothesesfield: one entry per hypothesis inconstruct.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:
unsupportedon 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
6e56f8aThanks @E1i! - A hypothesis now records the tree it was read from, not just the commit. BesidebaseSha,construct.model.jsonrequiresbaseClean: 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: nullwithbaseClean: false.baseCleanis 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
5916b79Thanks @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.jsonexample 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
mixedvalue 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
62a317bThanks @E1i! - cli: Whenred-gate,hook,construct-testsandweakestLinkleftdoctor, 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_FAMILYplus 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
idvalue 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
8084a24Thanks @E1i! -What it refuses to claimdescribed 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 areheld,unsupportedandunknownnow, and the page gives each one the reading it is not:heldis not proven, because the facts under a claim are necessary and never sufficient;unsupportedis not not enforced, because a named fact stopped matching and the report says which;unknownis 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
L0was 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-startedlistsconstruct.model.jsonamong 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
1d04d5bThanks @E1i! - The baseline fix in 0.5.1 repaired two things and only one was tested. Files compared against staleinithashes were reported modified, which is what prompted the work; a path recorded only bysyncwas 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
initrecord 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
211da72Thanks @E1i! -doctorcompared every path against the frozeninitrecord and ignored everythingsynchad recorded since, so on any repository that has runsync --applyit 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.jsonholds two records on purpose — theinitrecord 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 whatrecordedShashas always returned and whatsyncitself reads.doctorsimply did not use it. That was visible in the output before it was visible in the code:versionGapreached the sync record throughreplaywhilemodifiedFilesdid not, one sibling backed and the other bare.The same defect was in two more places.
uncollectedTestslooked for the runner config and enumerated recorded test files in the init record alone, so anything sync added was invisible to it. Andinitcounted the records it carried over from an existingconstruct.jsonwithout 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.filesdirectly anywhere undersrc/fails lint, withsrc/manifest.tsthe single exemption, since it is the file that defines what the two records mean.#71
211da72Thanks @E1i! - The model every repository gets frominitclaimedvulnerable-dependencies-are-visibleat L3, and the job behind it ships withcontinue-on-error: trueintemplates/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
L0and 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
L3as "CI that does not block a merge", which superficially fits — but that wording presumes a check able to report a failure at all, and distinguishesL3fromL4by 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
heldon facts that were all true while the mechanism it named could not fail.supportedBygives necessary conditions, never sufficient ones.#73
73f4d54Thanks @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
0070441Thanks @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 with409 Cannot publish over previously staged version. The pipeline had reported a success the world did not contain.A separate
Release verificationworkflow now asks the registry about the version inpackage.jsonafter 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, andunreachablefor a request that could not be made. A network error is never read as a missing version. And when a version isabsentthe 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
5b20037Thanks @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.iolink 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
04ba003Thanks @E1i! -construct coststops 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 aCostSourceinterface, the command now resolves the runtime it is actually running under and answersok,empty,unsupported,mismatchorunknown, each with its own exit code and its own line.--jsonis 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 isunknownrather than a guess.#17
7e8ca83Thanks @E1i! - Discovery fills the markers inAGENTS.mdand the invariants table, and once filled nothing distinguished what the tool wrote from what the repository's owner stands behind.construct.jsonnow 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.
doctornames 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.jsonalso carries an integermanifestVersion, separate fromconstruct, 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 asunknown, 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
39f8569Thanks @E1i! - cli:doctorreports an enforcement level instead of matching substrings. Five checks —lint-policy,construct-tests,ci,hook,red-gate— each return{id, level, state, evidence}, withlevelfromL0(text, or a command nobody is obliged to run) toL3(CI),stateone ofpresent,absentorunknown, 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.--jsongainswarnings,checksandweakestLinkafter 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.doctorexecutes nothing from the repository it inspects — no child process, no dynamic import of a path inside it, norequireinto itsnode_modules, no call into its ESLint or Vitest APIs — because it is run throughnpxin a clone nobody has decided to trust yet, and a flat ESLint config is a module. The lint policy forbids those forms undersrc/**andtests/dependency-policy.test.tslints one sample per form. Because branch protection lives in the GitHub API and not in a file,doctornever claimsL4,ciis neverabsent, and the red gate is alwaysunknownand says so.#15
26e449aThanks @E1i! -construct costnow reads the run ledger and reconciles it against what the runtime exposes, joining on the runtime's own run identifier — which the/implementskill 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 ofunknownis 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.mdsays plainly that the ledger is written by a step of a skill and is therefore L0 — a record nobody is obliged to keep.#19
7664355Thanks @E1i! - The lint policy checks never reached a real repository. They lived in each preset'ssamplegroup, and a sample is materialized only into an empty directory — soinitagainst a repository that already has code, the case this tool exists for, wrote the policy and skipped the test that proves it fires.doctorhad been reportinglint-policy L0 absentand was right. The tests now ship inbaseline, and arrive whether the directory is empty or not.node-frontenddeclared 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'sclassListorstyleto 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-libraryis deliberately untouched and still reportslint-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 — soabsentis a true reading rather than a missing file, anddocs/cli.mdnow 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
f22bcdfThanks @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 thatNO_RAW_SQL's own message tells you to use. Each restriction in the monorepo and node-backend presets now covers its class, including bindingprocessorreqto a local name, while keeping every role's exemptions exactly as they were.The durable half is the test.
syntax-policy.test.tscompared 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
processto a local name and reads.envoff 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
7f07169Thanks @E1i! - Acceptance now runs the artifact that actually ships. Each leg installs the packed tarball into a directory outside the repository and invokes the installedconstructbinary forinitanddoctor, instead ofnode dist/cli.jsout of the working tree. That is what publishing exercises: externals tsup leaves out must resolve from the installed package's own dependencies,filesmust carrytemplates/or the first template read fails, andbinmust 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 undersrc/resolves to a devDependency; it is green today and stays that way on purpose.#18
045efcaThanks @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: ninemediumruns 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. Twolowruns cost 557k and 740k with two agents each; twohighruns 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
728efc7Thanks @E1i! - Six fixtures fordoctorundertests/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 todaydoctorcalls 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
21b65d3Thanks @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
3116836Thanks @E1i! - Black ICE on the package boundary:pnpm run qualitynow runs a privacy guard overtemplates/,docs/andREADME.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 indocs/cli.md, and a test asserts the published file list carries nothing under.construct/,findings/orruns/.#16
27cf209Thanks @E1i! - The ladder no longer re-asks an agent whose response the schema rejected:retryLimitdefaults to0, 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
51921a5Thanks @E1i! - Two claims that were made but never checked are now tests. Materialization is deterministic: for every preset and every AI target, twoinitruns into empty directories with fully pinned variables produce identical file lists and identical hashes, so a future template that reaches for aDate, an unorderedSetor 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 inarchitecture/self-hosting-drift.yamlwith 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 whatinitwrote 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
7cbf62dThanks @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 staticimportofnode:child_process, soawait import('node:child_process')andcreateRequire(target)('eslint')walked straight through. ThespawnPolicyblock now fails the build on the whole class — any dynamicimport()whatever its specifier,requirecalls andrequire.*access,node:moduleandcreateRequire— anywhere undersrc/except the pnpm version probe, andtests/dependency-policy.test.tslints one real source sample per form instead of comparing selector strings. The invariants table now names the mechanism rather than the wordlint.
0.1.3
Patch Changes
- #3
b3051b6Thanks @E1i! - cli: apnpm-workspace.yamlorworkspacesfield counts as a monorepo only when it lists packages. Since pnpm 10 that file also carries settings such asminimumReleaseAgeandallowBuilds, and the construct ships one in every preset, so a generated single-package project reported itself as a monorepo and a secondinitsuggested the wrong preset.
0.1.2
Patch Changes
1cf5709Thanks @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.