We kept building the same parts, so now we keep them
Millwright fixed the order we build in. It did nothing about what we had already built. Castings is the list of parts we stop writing twice.
On 24 October we went through everything Wayside Foundry has built and wrote down every part in it. That covered fifteen code repositories, the project folders on our machines, the agent skills we build with, and the workspace where we track the company. It came to 370 parts.
This is the second note about how we build. The first, An assembly line for applications, is about Millwright, the fixed order of stations every app goes through. This one is about what goes into those stations.
We built Castings for ourselves first. It feeds Millwright, so that every app starts from parts we have already proven, and it makes us more efficient builders. We also want to be part of the community of people who build, however they build, and to give something back to it. So where a part could help someone else, we want to find a way to put it out there for anyone who wants to use it. Writing down how it works, including what we got wrong, is part of that.
How the inventory was built
A part is one reusable unit of work: a component on a screen, a logic module, an integration with an outside service, a build or deploy script, a test, a template, or an agent skill. The rule for drawing the line was that a part is something another builder would recognize as a single thing and could lift on its own. A function is too small to be a part. Where several small pieces only ever travel together, they were recorded as one.
Every part was recorded against the same fields, so the list can be sorted, filtered and checked rather than remembered:
| Field | What it records |
|---|---|
| Location | Repository, file, and line range where the part is a section of a larger file |
| State | One of six: runs in a live app, behind a feature flag, present in the code but never loaded, a tool or script, a document, or unresolved. Each carries the evidence it was judged on, such as the flag and its value |
| Portability | Drop in, needs light adapting, or tied to the app it came from |
| To remove | Names, identifiers, URLs and copy specific to us that would have to come out before the part went anywhere else |
| Credentials | Whether the file holds any key or credential. Recorded by file only. No value is ever copied into the inventory |
| Lesson | What failed or what we learned that made the part the way it is, taken from the code's own comments and our records |
The list was then checked against the code. It cites 797 file paths, and every one was looked up in the repositories: all 797 exist. A part whose location cannot be found is not counted.
| Kind | Parts |
|---|---|
| Logic modules | 80 |
| Interface components | 76 |
| Templates and documents | 35 |
| Build and deploy tools | 35 |
| Integrations with outside services | 33 |
| Data and content | 25 |
| Test and quality tools | 22 |
| Agent skills and prompts | 22 |
| Whole apps | 21 |
| Design assets | 21 |
By state: 225 run in a live app, 78 are tools and scripts, 33 are documents, 7 sit behind a flag that is off, 21 are in the code with nothing loading them, and 6 could not be settled from the code alone, so they are marked unresolved rather than guessed. 334 of the 370 carry a recorded lesson.
The count is a starting figure, not a final one. Where to draw the line around a part is a judgment, and the number will come down once duplicates are merged.
What the inventory found: four service workers
A service worker is a script the browser runs alongside a web app. It decides what is stored on the device, what is fetched from the network, and when a new version of the app replaces the old one. It is what lets a web app open with no signal, and it is also the most common reason a person keeps seeing an old version after an update.
Four of our apps each have their own, written separately, and they do not agree:
| App | Pages | Versioning | Lines |
|---|---|---|---|
| Advanced Golf Address | Network first, cache as fallback | Cache name carries the build stamp, set automatically on deploy | 71 |
| Let's Shoot This | Network first with a 6 second timeout | Cache name bumped by hand | 117 |
| Bulldog Buddy | Network first, cached shell offline | Bumped by hand. Install fails if a core file fails, so the last working version stays | 122 |
| withe | Cache first | Bumped by hand. Reloads open windows on an update, never on a first install | 65 |
Each of those choices has a reason recorded beside it in the code, and most of the reasons are good. The problem is that they were made four times, separately, so a lesson learned in one app never reached the other three. A fifth set of sites, the course builds made from the golf app, is the one place where we did reuse: its build script takes the golf app's service worker and rewrites it for each course.
The deploy scripts show the same pattern. There are three, of 134 to 331 lines, and each solves the same problems in its own way: running the tests first, publishing only an approved list of files, and refusing to ship when something has not been updated. The withe script, for example, refuses to publish if the app changed and its service worker cache name did not, because a cache that is not renamed keeps serving the old app.
None of this was a decision. Each app was started in a new working session, and nothing in that session said what we had already built. Building with AI makes writing a part cheap enough that nobody stops to ask whether it already exists. Cheap is not the same as free: four versions are four sets of behavior to keep straight, and a fix to one does not reach the others.
What a casting is
The name comes from the work. A foundry makes castings: parts made from a pattern that already exists, so the same part comes out the same way every time.
A casting is one part taken out of the app it was built in, kept on its own, and written up well enough to go into the next app without anyone having to work out again why it is the way it is. Each one is a folder:
castings/
INDEX.md one line per casting, read first
last-resort-render/
README.md what it does, how to fit it, the lesson
index.js plain JavaScript
index.d.ts types for TypeScript
test.js the checks that prove it behaves
SOURCE.md the app and commit it came from
network-first-worker/
...
The core of every casting is plain JavaScript, because plain JavaScript runs inside every framework and inside none. Type definitions sit beside it so it works natively in a TypeScript project. A visual part gets a React version as well, only where that is worth maintaining a second copy. Scripts, skills and templates are used as they are, whatever the project is written in. Every casting carries its own tests, and SOURCE.md records exactly which app and which commit it was taken from, so any copy of it can be traced back.
Why we copy castings instead of sharing them
The usual alternative is a shared package: one copy of the code, installed into every app as a dependency. We are deliberately not doing that.
A shared package couples every app to it. Change the package and every app that installs it changes on its next build, whether or not anyone has tested that app against the change. Our apps ship separately, to separate places, on separate days, and an app that is live should only change when someone decides to change it and checks it. So a casting is copied into an app, and from that moment the app owns that copy.
The cost of copying is drift, which is exactly the problem the inventory found. What is different now is the record. Each copy is traced to a casting and a commit, so when a casting improves, we can see every app holding an older copy and decide, app by app, whether to bring it forward. Drift becomes a list we can work, rather than something we discover.
None of this is a new idea, and we are not presenting it as one. Builders have shared reusable code on GitHub for as long as GitHub has existed: snippet collections, starter templates, and component libraries built on exactly this copy and own model, where you copy a component into your project and it is yours from then on. Teams inside larger companies keep internal libraries of their own for the same reason. What we are doing is keeping our own, in the same form, for the apps we build.
How Millwright will use it
This is the plan, and it is not built yet. At the station where code gets written, Millwright reads INDEX.md before anything else. If a casting covers what the app needs, it copies the casting and records which one and which version. If nothing covers it, the part gets written, and once it is running in one of our live web apps and has passed its tests, it comes back to the inventory as a candidate casting. A part becomes a casting only after it has run in a live, tested app, not before.
One casting, in full
withe is our safety plan app. A safety plan is used on a bad day, which is the one day the app cannot come up as a blank screen. So the app has a last resort. If anything throws an error while a screen is being drawn, it replaces the page with the person's own plan as plain text and a button to call or text 988, the crisis line.
let failed = false;
function lastResort(err) {
if (failed) return; // never let the fallback loop
failed = true;
let txt = '';
try { txt = asText(); } // the plan, read straight from the object
catch (e) { try { txt = JSON.stringify(plan, null, 1); } catch (e2) {} }
const d = document.createElement('div');
d.id = 'lastresort'; // its own styles, nothing from the app
d.innerHTML = '<h1>Something in the app broke</h1>' +
'<p><a class="big" href="tel:988">Call or text 988</a></p><pre></pre>';
d.querySelector('pre').textContent = txt; // textContent, never innerHTML
document.body.innerHTML = '';
document.body.appendChild(d);
}
addEventListener('error', e => lastResort(e && e.error));
addEventListener('unhandledrejection', e => lastResort(e && e.reason));
function render() { try { return renderInner(); } catch (err) { lastResort(err); } }
This is a shortened version of withe's own source. Four decisions in it are what make it a casting rather than a snippet.
It catches every path that can stop a screen. The render function is wrapped, and so are the two ways an error escapes on its own: one thrown anywhere on the page, and a promise that is rejected with nothing handling it.
It depends on nothing the failure could have broken. No app styles, no app fonts, and no app functions except the one that reads the plan, which has its own fallback to the raw data. If the stylesheet is what failed, the last resort still renders correctly.
It inserts the plan with textContent, not innerHTML. The plan is the person's own words. Inserting user text as HTML would let that text be interpreted as markup, which is an injection risk, so it goes in as plain text.
It runs once. If the fallback itself fails and is called again, the second call returns immediately, so it can never loop.
The crisis line is specific to withe. Taken into another app, the casting keeps the structure and changes the message and the button to whatever that app's user needs.
What stays inside the company
Contributing does not mean publishing everything, and the inventory is built to keep the two apart. Every part carries a proposed view: 219 look suitable to share, 92 might be with work, and 59 stay internal. Those are proposals, and we decide part by part.
What stays internal is fixed: the calculation behind where to stand in Advanced Golf Address, our brand, anything that touches a person's data, and anything that carries pricing or commercial terms. Before any casting is shared, it goes through four checks: every file is scanned for keys and credentials, every identifier and address specific to us is removed, anything that could carry a person's data is removed, and one of the founders reads the whole thing before it goes anywhere. A casting that fails any of the four does not go out.
Questions we expect
Is AI-written code safe to reuse? Code is not safe or unsafe because of who wrote it. It is safe when it has been tested and has run under real use. That is why a part only becomes a casting after it has run in a live web app and passed its tests, and why every casting carries those tests and the record of where it ran.
Is copying code not just duplication? It is duplication with a record. The problem with copied code is losing track of the copies. Each copy of a casting is traced to its source and version, which is the part that was missing before.
Why not publish all of it? Some of it is what makes our apps worth using, some of it touches data that belongs to the people who use them, and none of it is published until it has been checked and read. What we can share, we want to share. That is the point of doing this.
Where this sits
11 October: Millwright is named and the line gets its stations. 23 October: the first note about it. 24 October: the inventory, and the finding that we had built the same parts more than once. Next is the merge, then the index, then the first castings, then Millwright reading the index before it writes anything.
If you build too, we would like to know what would actually be useful to you, so the first parts we share are the ones people need. We are asking on X.