How it's made
One person, one repository, four products.
The IDE you design in, the runtime underneath it, the player that is the executable of every application you build, the server and the client that pair with it — and this website, the manual and the example projects. All of it out of one folder, written with an AI and decided by a person. This page is the inside story: how it fits together, how that arrangement is kept honest, and two afternoons where the numbers moved.
Everything is in one place
There is no separate team for the server, none for the website, none for the docs. The repository holds one Xcode project and one Swift package, and everything else is a small tool beside them — the manual is generated, the example projects are generated, the website's pictures are generated, even the application icon is drawn by a program.
That is a constraint, not a boast. One person cannot maintain two of anything. So there is one list component and the data browser draws it too; one printing engine and the quick print from a list goes through it; one shell behind find, sort, import and export. Every place where a second version of something could have appeared, it did not — because there would have been nobody to keep the two in step.
| Part | What it is |
|---|---|
| Runtime | SQLite, schema, migration, the JavaScript bridge, forms, reports, transfer, the editors |
| IDE | the designer, the debugger, the build pipeline |
| Player | a small host — the binary of every application you build |
| Server + Client | the same payload, one holding the data |
| Website | 16 pages, checked by a script |
| Manual | generated into the site and into the IDE |
| Examples | generated, never hand-edited |
There is no compiler in the build
This is the decision the whole shape rests on. A finished application is our precompiled player — one binary, built and signed by us once — with your project beside it as an encrypted payload. Building copies, encrypts and signs.
A test build of a real project finishes in about two seconds. The two minutes a release build takes are almost entirely Apple's notarization, not ours.
The runtime is linked by the IDE and by the player. A bug fixed there is fixed in the designer, in Run, in every application rebuilt afterwards, and in the server — at the same moment.
Anything a running application needs lives in the runtime, never in the IDE. That is why the find window, the import window and the report editor exist inside the applications you ship, not just in the tool.
Written with an AI, decided by a person
Most of this code was written by an AI, working from decisions a person made and under review by that person. That is a plain fact about how Neuridion is built, and it is written here rather than left to be guessed at.
It is also the honest answer to the obvious question — how does one person build an IDE, a database runtime, a client/server layer, a website and a manual? This is how. The interesting half is not that an AI writes code; it is what has to be true for that to be safe.
The person directing it has been building database applications for over forty years. That is the half of this arrangement nobody talks about, and it is the half that decides whether it works. An AI writes quickly and will build, precisely and confidently, whatever it is asked for — including the wrong thing, in a shape that looks entirely reasonable until a customer’s data is in it. What keeps that from happening is not the model. It is knowing, before a line is written, what a data model costs when it is wrong, where a schema migration goes wrong three years later, and why a list must never quietly stop at a thousand rows.
So the instructions are exact, and they are the product. Money is an integer count of minor units, never a decimal. The list reads two hundred rows at a time, however many there are. A rule that only some writers obey is not a rule, so validation lives on the table and not in the form. A built application never checks a licence — not once, not anywhere. Every one of those is a decision made by a person who has seen what the alternative does, handed over as a requirement and not as a suggestion. The machine’s job is to carry it out and to be caught when it does not.
The project's own notes say how many tests there are, how many warnings the build has, how many German strings are allowed to remain in an English product. A script compares each number against reality and fails when they differ. A number nobody verifies drifts — three of them did, on one day, before this existed.
After a fix, the fix is deliberately broken again to watch the new test go red, then put back. Twice a test written after a bug would have passed with the bug still in place. Once — this week — a test looked right and proved nothing, because the two names it compared happened to sort correctly anyway.
Decisions are written down with their date and their reason. If something turns out to be impossible, the work stops and the person is told — instead of a slightly different thing being built and never mentioned.
When a diagnosis is a guess, the answer is a spike: the smallest possible program that settles it. Both stories below started that way, and one of them overturned what everybody assumed the problem was.
And this is the instrument the reading is done with
Reviewing a machine that writes hundreds of lines an hour has one real difficulty, and it is not reading code. It is knowing what changed — which types are new, which function quietly grew a parameter, which file was touched for a reason nobody wrote down. So the same person wrote a second program for it. Sextant parses this repository — 506 files, 1,111 types, 4,534 functions, in under three seconds — and watches it while the agent works. It reads Swift, which is what Neuridion is written in.
That screenshot is not a mock-up and not a re-enactment: it is
this project, that evening, while the buttons beside a list were learning to sit down the
side of it. RowActionEdge, railGroup,
rowActionRailWidth, setActions(_:edge:handler:) — the
review starts from that list rather than from a diff of four hundred lines, and the
difference is whether a person can actually read a day’s work or only claim to.
Six lenses ask six questions, and each answers with places in the source rather than with a verdict — the rule for that window is that every row must be openable at a line, and what cannot be pointed at does not belong in it. Four of them matter to anybody who has to trust what came out of this workshop:
Every network address, every keychain call, every settings key, every folder, the pasteboard — and credentials, recognised by a provider’s token format, by the name they are assigned to, or by shape alone, and always shown masked. What is not in that list, the application does not talk to. It is the check behind our privacy page, and that claim is worthless unless something can enumerate it.
An invented symbol name, an image that is not in the asset catalog, a text key with no entry — all of them compile, and all of them fail on screen. With a rule we would happily steal for anything: no catalog means no claim. A kind that could not be checked is listed as unchecked, because a missing catalog must never look like a clean result.
Timers, observers, file watchers, sensors, audio — and tasks whose handle nobody kept, so nothing can ever cancel them. Balanced per kind rather than per function, so a timer begun when a window opens and ended when it closes is not a complaint. This is the family of fault that costs nothing at all on the machine it was written on.
Functions with the same control-flow shape and the same call order, whatever they are named — exactly what copy-and-rename leaves behind. A fact written twice is a fact that will be updated once, which is this project’s most persistent bug; here it is countable instead of remembered.
And the part that is easiest to miss is the one that matters most: a review is a state, and it expires by itself. Every symbol carries a human verdict — under discussion, needs redesign, verified, signed off — and that verdict is pinned to the shape the symbol had when it was given. Change the shape and the sign-off flips back to outdated on its own. That is the difference between a review and the memory of one: nobody has to remember which of four and a half thousand functions was read before the last rewrite went through it.
What Sextant does not do is decide. It reports; the person decides, and the person is the one who then tells the agent it got something wrong. That order matters more than any single check in the list: a review tool that quietly fixes things is a second author, not a review.
A story
The attachment nobody was looking at — and how a database became two files
Neuridion stores a scanned invoice in the record itself: no media folder beside the data, nothing to lose when somebody moves a directory. The question is what that costs when a list shows two hundred rows and nobody wants to see a single scan.
The answer, for a long time, was everything. Reading records compiled to
SELECT table.*, so every row arrived with its documents attached, whether or not
anybody looked. The laziness everyone believed in was one floor too high: it sat at the
bridge — a script was carefully handed an object without the bytes — while the bytes
had already been read from disk, decrypted, and put in memory to build that object.
A spike settled it: three hundred rows, a five-megabyte scan on each, a database of 1.5 gigabytes, read once the old way and once with file columns left out of the query.
| Reading 300 records | Before | After | Faster by |
|---|---|---|---|
| Plain database | 0.376 s | 0.001 s | 376× |
| Encrypted (SQLCipher) | 1.725 s | 0.002 s | 862× |
Why the database is now two files
Making the read lazy raised the next question: where do those bytes live? Leaving
them in the row means every page SQLite touches drags megabytes past the disk head. So the
documents moved into a sibling database — Customers.files.sqlite beside
Customers.sqlite — attached to the same connection with the same key.
Attached to the same connection, a write that touches a record and its document is one atomic transaction. Two files, one promise.
The sibling uses the same key. There is no second thing to configure and no way to end up with an encrypted database and plaintext scans.
The record's own column keeps the envelope — name, kind, dates, size — so a list draws “Contract.pdf · 2.4 MB” without touching the second file at all.
A project without documents has exactly one database, as it always did.
Another story
We tortured the server until it stopped answering
A MacBook Air as the server, a MacBook Pro loading it over a gigabit cable with real paired clients — lists of 50 and 200 rows, counts, a sum, a sum grouped over a relationship, and a create·save·delete in every ten. Each client offers about ten requests a second: a busy desk, not a stopwatch. Twenty steps, up to four hundred clients at once, against Harbour Supply’s 7,600 invoices.
| Concurrent clients | Requests / second | Typical answer |
|---|---|---|
| 1 | 9 | 8.6 ms |
| 20 | 189 | 5 ms |
| 60 | 569 | 4 ms |
| 100 | 932 | 6 ms |
| 200 | 1,198 | 66 ms |
| 240 | 1,225 | 95 ms |
| 300 | 1,204 | 148 ms |
| 400 | 1,166 | 242 ms |
Not one error, and not one refused connection, anywhere in that climb — over the cable, and again over Wi-Fi. It gets slow and it does not fall over, which is the property that matters: a server that goes sluggish under overload is one you may overload. One that throws errors is downtime.
And the database was never the reason
The server answers every command on one serial lane, by design: SQLite has a single writer, and the order of table events is business logic. The obvious guess is that the lane is what fills up. We measured it, from inside the lane itself — and it is not. It reaches 80 % at 140 clients and then stops climbing, while the wait quadruples. At 1,225 requests a second that is 0.6 milliseconds of database work per request, against the 242 milliseconds a client waits at four hundred. The network costs 1.3. Everything else is queueing above the database.
That is worth more than the headline number, because it says what to build next — and it is not a faster database.
Over Wi-Fi, one user is slower than two hundred
The same run over Wi-Fi 6 gave the result nobody expects. A single client waited 83 milliseconds where the cable took 8.6 — ten times worse. At eighty clients it was 38 milliseconds, and at four hundred Wi-Fi beat the cable outright. The radio sleeps between packets, and every wake costs tens of milliseconds; a continuous stream keeps it awake. The worst Wi-Fi load is a quiet office, not a full one. The link was a good one — channel 36, 33 dB of headroom — which is what makes this a fact about power saving rather than about somebody’s flat.
And then it stopped accepting anybody, for a reason nothing could see
The run ended badly: past four hundred clients the server began refusing connections, and it did not come back on its own — the clean-up pass minutes later could not get in either. The obvious reading was that four hundred is simply the limit. It was not. The process was holding 8,169 TCP sockets — 7,871 of them already closed — against roughly 8,200 connections it had ever accepted. Every single one.
Everything visible had been right. When a client hangs up, the server reaps the bookkeeping: the connection table, the statistics, the locks it held. What nobody ever did was tell the networking layer it was finished with the connection, and until you do, it keeps the file descriptor. In ordinary use that is one lost descriptor per client that ever connects — every restart, every wake from sleep — until a server that has been up for weeks quietly admits nobody — and in a load test it looks exactly like a capacity limit, which is how it nearly became one in this very story.
It is one line. The test that keeps it away counts the sockets on the transport’s own port, because the connection table was correct the whole time the sockets piled up: anything that asked the server how many clients it had would have passed with the bug in place.
What the whole exercise bought, with the leak repaired: zero failed requests and zero refused connections through four hundred clients, on cable and over Wi-Fi. And the behavior that matters more than any number — it slows down, and it does not fall over.
Measured, then decided
Five decisions, and the numbers behind them
None of these were taste. Each one started as a measurement, and two of them ended somewhere other than where they were heading.
A million rows, and only what is on screen is read
A list used to fetch the first thousand records and say nothing about the rest — the quiet kind of wrong answer, because it looks like a complete one. The list is an AppKit table now, and it reads a page of 200 rows at a time, keeping at most eight of them. Scroll to row 47,312 and that page is fetched; the pages you scrolled past are dropped once they are more than four away. At the very most about 1,600 records are in memory, whatever the table holds.
The row count itself is not a fetch at all — it is a count(*), which SQLite
answers without reading a single row. The stress-test project has 1.01 million rows across
160 tables; opening it takes 0.11 s, and scrolling its biggest table is the same work as
scrolling a table of fifty.
A footer under a money column sums the whole set — all 7,628 invoices — not the rows that happen to be loaded. A limit nobody can see is a wrong answer wearing the face of a right one, so the sum is a query and never a loop.
Grouping for a chart is done in SQL — the example's dashboard says so out loud: “Grouped in SQL — no records are loaded.” Six bars over seven thousand invoices cost one statement.
Until somebody treats it as a list. invoice.lines.count() counts in the
database; asking a customer with four thousand invoices how many they have must not load
four thousand records.
The story further up this page: a row's scans are not read until something asks for them. That is the same rule one floor down — read what is shown, not what exists.
And the decision that came with it: the old list was deleted, not kept beside the new one. One list component, used by the forms you design, by the data browser, and by the preview inside the find and import windows — because a second one would be a second set of bugs and nobody here to keep the two in step.
Money is an integer
In floating point, 1.005 × 100 is not 100.5. It is
100.49999999999999, and rounding that gives 1.00 € where 1.01 € was
owed.
While amounts are only typed in, that is a curiosity. The moment a line total, a tax amount and an invoice total are computed, every line of every invoice runs through that one rounding — and a curiosity becomes an accounting error that is very hard to argue with afterwards.
An Amount is a whole number of minor units — cents — from the moment it is typed to the moment it is printed. Never a floating-point number, anywhere, at any step.
The field says how many, not the currency's habits: two for the euro, three for a dinar, none for the yen. And a money field carries its currency wherever it is drawn, so a list cannot show a bare number where a form shows money.
Totals are added by SQLite, over whole numbers. Which is why the total on a form and the total on the printed report cannot disagree — they are the same statement, not two additions that happen to be near each other.
1.005 and 2.675 are the two classic cases where floating point quietly rounds the wrong way. Both are in a test, and the test was watched failing against the old code before the fix was believed.
Three more, in short
Before a line of the server existed, a spike measured the protocol — and encoding cost more than the network did. Decision: a hand-written binary format for rows, and JSON never for row data. Today a 500-row list is one trip at 5.8 ms; one record is 0.13 ms; a ping is 0.09 ms.
Scans rarely change, so the attachment store counts its own writes: two backups carrying the same number carry the same documents. Decision: clone on APFS rather than copy — a genuinely separate file that shares its blocks. A hard link was rejected: SQLite writes to a database merely by opening it, so one backup could damage another.
Over the wire each save is its own round trip. Measured: 4,811 against 25,550 rows a second, 5.3× and 86 % of what is possible with no network at all. Decision: wait. Three questions are unanswered — a refusal would arrive after the loop, a read inside the transaction would not see its own unwritten rows, and deleting has no bulk command. The number is on the list beside the question, so the decision is made with it rather than without it.
And at the end
What the editor sees before anything runs
A script that has not been run yet is still a script that can be read. The editor knows the data model, so a field name that does not exist is underlined while it is typed — and the window it belongs to carries a mark in the sidebar, so you find it without opening every file.
Writing to a field that does not exist used to be accepted without a word: the value went into an ordinary JavaScript property and was saved nowhere. A crash is seen the same afternoon; that is seen in three months, by somebody asking why a column is empty. It is refused now — and underlined before it is ever run.
Every place the checker complains about carries its file and its line, and every line in the console is a button that opens the script there. The reason is unglamorous: the alternative is somebody hunting through tabs, and a warning that costs a hunt gets ignored the third time.
Fix the name and the underline is gone; the sidebar’s triangle goes with it. That sounds obvious and was not: the mark used to survive every edit until the script happened to run again, so a repaired fault looked exactly like an unrepaired one.
.where(…) calls higher up the same script are left alone,
because somebody has already thought about those..all(), hit nine places in our own example project — and every one of
them was right: two hundred customers are loaded whole precisely so that twenty-six
thousand invoice lines are not. From the source alone, “read 200” and
“read 200,000” are the same three words. A checker that underlines correct
code is one nobody reads twice, so it came out again the same day. It speaks now only
when it has measured, above five thousand rows, with the real number in the
message — and the way out it recommends is one the product actually has. An
earlier draft of that very sentence suggested a command Neuridion has never had.
The tests, and the commands that keep the story honest
Not as a rule somebody remembers — as the way the work is done. Sound arrived with ten tests, the update check with thirteen, computed fields with six suites. The tests are what makes the next change safe, which is the only thing that matters to somebody already using the version before it.
After a bug is fixed, the fix is deliberately broken again to watch the new test go red. Twice a test written after a fix would have passed with the bug still in place — which is a test that proves nothing and reassures everybody.
The designer canvas and the running window are separate renderers. A test counts the settings each one reads: anything the window honors and the canvas ignores fails the build. Four rounds of reading the two files side by side had missed what counting found in seconds.
One command runs all ~85 scripts of the shipped projects through the real engine: does it parse, does every table and field it names exist, can the debugger rewrite it. The first time it ran it found fifty-five complaints — every one of them a bug in the checker, under correct code.
Some faults exist only while the program is running — a keystroke that never reaches its handler, a sheet that swallows every click. Those are found by a test that opens the application and uses it, not by reading the source.
They regenerate with one command, so a picture cannot quietly go on showing a version that no longer exists — which is exactly what had happened to the set before them. A second command checks that every picture a page names is actually there: it found a chapter of the manual pointing at a file nobody copied, in the shipped product, with nothing failing over it.
The lesson that cost the most time
A test that measures the machine is worse than no test. Four of them wobbled under load — and three had one cause, which was not the clock: the thing they waited for is delivered on the main queue, which an application services constantly and a test process services only by luck. They passed on an idle Mac and failed while something else was compiling. The cure was an injectable delivery queue and waiting for a state instead of a duration; the proof is the suite run three times with four processes deliberately burning CPU.
The point of all of it
This tool is not made fast by a compiler. It is made fast by the shape of the data.
Every number on this page comes from the same kind of decision, and none of them came from tuning the language. They came from asking the program to do less.
| What made it faster | Factor | Where it came from |
|---|---|---|
| Compiling the hot code | 20× | JavaScriptCore, for free, while your application runs |
| Not reading documents nobody asked for | 376× | a decision about what a query names |
| The same, on an encrypted database | 862× | the same decision — every byte not read is also a byte not decrypted |
| Sending a batch instead of a round trip | 5.3× | measured, and deliberately still on the list |
A compiler gives you a constant factor, and we take it gladly: SQLite is compiled C, JavaScriptCore turns your hot scripts into machine code, the runtime underneath is compiled Swift. But the compiler was never the interesting part. Twenty times faster is what a compiler buys. Eight hundred times faster is what it buys you to not read three hundred scans off a disk because somebody opened a list.
That is why the work went where it did: a page of 200 rows instead of a thousand-row cap,
a count(*) instead of a fetch, a sum computed by the database instead of a loop,
a grouped chart that loads no records at all, documents in a sibling file that a list never
opens, and a wire that sends five hundred rows in one trip. None of it is clever code. All of
it is a decision about what is not done.