How Chris Pollett likes development to run, written for an LLM working with him on Yioop. Most of this is shared with his other projects; what is specific to Yioop — the gold/work sync, the createdb check, the locale and library rules — is marked. When something says "always" or "never," treat it as a hard rule.
A section's description says what the section requires: its commands, files, rules and numbers, so that someone who has never opened this file learns what it asks. A one-line maxim is not a description. “Calls that free what frees itself are never written” told Chris nothing about which calls those are or why; the functions, the PHP version that made them do nothing, and the script that finds them are the description. Most of this file is meant to serve Chris's other projects too, so describe the general rule first and the Yioop-specific part as Yioop's, and leave out details that change over time, such as which site runs which server.
The review goes in the body of the response where Chris reads it, never in a file he has to open. Writing it to a file and saying it exists is not writing it.
Two things go in the review beyond the section-by-section reading, and both are the point of the exercise rather than an addition to it:
Yioop is developed against a two-clone discipline:
confirmed-head tag marks the latest confirmed commit.work_directory restored into it.Setting up gold, and keeping it current, from seekquarry.com. The
confirmed history lives at
https://www.seekquarry.com/group/2/yioop-repo.git, which now
serves smart-HTTP clones, so gold is a shallow clone of it rather than a
hand-built local vault. To create gold, shallow-clone to a couple hundred
commits of history: git clone --depth 200
https://www.seekquarry.com/group/2/yioop-repo.git into the gold path.
That is enough recent history for context without pulling the whole
repository. Tag confirmed-head at the clone's tip.
Each round you deliver one patch. Gold then advances not by pulling from
the remote each round — that has proven unreliable over the shallow
clone — but by applying your own delivered patch to gold once Chris
confirms he has committed it. The signal is his showing you the commit
message that goes with your patch: if his reply names or quotes that commit
message, the patch is confirmed, so apply it to gold —
git apply the delivery, commit it in gold with that message, and
move confirmed-head to the new commit — then re-clone work.
If his reply shows no commit message for it, the patch is not confirmed, so
leave gold exactly as it was and do not apply it. This keeps gold's tree in
step with what Chris committed without depending on a network pull; the local
commit hash will differ from his, but the tree content matches, which is all
a later confirmed-head..HEAD diff needs.
gold is a vault, not a workbench. Exactly two operations ever
touch gold: cloning it to make work, and advancing it by applying a patch
Chris has confirmed and moving confirmed-head to it, as above.
Nothing else, ever. Never run a build, a test, a
golden regeneration, a ConfigureTool command such as
createdb,
or any other command inside gold; never create a work_directory
there; never hand-edit or hand-copy a file into it, and the only commit it
ever takes is that one confirmed patch. Every
build, test, and regeneration happens in a work clone. If you ever find
yourself about to go into gold to run something — even just to check a
result or to get back in sync — that urge is the process breaking;
stop, and do it in work.
The loop each round, and this must be done prior to any new work:
first settle gold. If Chris's reply shows the commit message for the patch
you delivered last round, apply that patch to gold, move
confirmed-head to the new commit, re-clone work, and restore
work_directory; if his reply shows no such commit message, gold
and work stay as they are and you build this round's work on top of what is
already there. Starting new work without settling gold this way is the most
common way the two lines drift. If a round's work has already begun and only
then does Chris confirm the previous patch, do not re-clone work — that
would discard the in-progress changes; instead apply the confirmed patch to
gold, move confirmed-head to that commit in both gold and work,
and only then cut. Cut a delivery as git diff
confirmed-head..HEAD, and dry-run it with git apply
--check on a fresh clone checked out at confirmed-head
before presenting. Apply patches forward only — never reverse-diff.
Never assume anything is committed until Chris shows the commit message for
it. Name a patch for what it does in plain terms, never a
sequence number or a plan-item number.
Saying gold advanced is not advancing it. The failure this guards
against is writing “gold is now at X” without having run the
apply, which leaves gold behind Chris's tree; the next patch is then cut
against the wrong base and fails on his machine. Two small scripts in
devlog/ remove the judgment from the reporting, and a ledger
file records what has been delivered and what has been settled.
devlog/devscripts/settle_gold.sh "commit message" is the
first
action of every round, always run, with the commit message Chris
showed or with no argument when he showed none. With a message it
applies the oldest delivered-but-unsettled patch to gold, commits it,
moves confirmed-head, and marks it settled. Either way it
prints gold's head read back from the tree, and any patch still
waiting. The gold line in your response is that printed output,
pasted. If you did not run it, you have nothing to paste, which is
the whole point. Run it in a call of its own and read its whole
output: a filter such as grep on that output once hid the
gold line, and a round then went out with no gold line to paste.settle_gold.sh --withdraw name-of-patch takes a
delivered patch Chris did not apply out of the ledger. It is that
round's one settle call: it goes on to count the round and print
gold's head, as a settle with no message does.--withdraw with the patch's name, and the round's patch
carries both changes under a new name. Waiting for the commit message
instead held a fix back two rounds.devlog/devscripts/cut_patch.sh name-of-patch cuts the
delivery and
refuses while anything in the ledger is still waiting. That refusal is
what makes a stale gold impossible to build on: a dry-run against a
fresh clone of gold cannot catch a stale gold, because the clone is
gold and passes happily.The ledger lives outside the repository (by default
/home/claude/gold_ledger.txt) and holds one line per patch:
its path, its name, and delivered or settled.
Both scripts take GOLD, WORK, OUT
and LEDGER from the environment, so a sandbox laid out
differently only needs those set.
A conversation can reach its limit mid-chase. When one does, write the state of the chase as a file and hand it over — what is established, what is ruled out, the plan and which step it is on, the exact next action, and the traps met along the way. Such a note stays out of the repository: it is scaffolding for one hunt, not something the history should carry forever, so it goes to Chris as a file of its own and never into a patch. A new session reads it after this process file and carries on without repeating the ruled-out ground.
Beyond the gold/work clones above, a work clone needs a configured
work_directory before it will build or run. Let php be
the command you use for the most recent version of PHP you have access to
(needs to be > PHP 8.1). Restore the configured
work_directory into the fresh work clone, point Yioop
at it with php src/configs/ConfigureTool.php work-dir, then
build the two SQLite databases with
php src/configs/ConfigureTool.php createdb, which should print
“Create DB succeeded.” Without a configured
work_directory, Config.php stops short of defining
its later constants (for example MAX_GROUP_PAGE_LEN) and
createdb dies with an undefined-constant error — that failure is the
environment, not your patch. Strings and resources are always read from
src/locale first. work_directory/app/locale
(LOCALE_DIR) may be absent, or hold only the strings and
resources a site changed, which take the place of the shipped ones.
Nothing writes to src/locale: TokenTool and the Manage Locales
screen write to app/locale.
src/executables/CodeTool.php is the general-purpose tool.
Run a test script with php src/executables/CodeTool.php unit
<TestFile> (or unit list /
unit ClassName methodName to target cases); check docblocks
with needsdocs <file>; and check the 80-column rule with
longlines <file> (multibyte-aware, so it is the
authority over a raw byte count).lynx and curl for fetching and reading pages as
text, plus node and npm. Rather than say don't
have the tool you want, try to install it.php src/executables/QueryTool.php '<query> i:<timestamp>'
explain and php src/executables/ArcTool.php dict
<bundle> <word> before writing any script of your own.
The plan shows which key a query reads and how many documents it holds,
and the dictionary shows each partition's rows. Seven rounds once went
to a google query a home-made dictionary reader could not explain; these
two commands explained it in one.phpize and loads with php -d extension=...;
one run named the two methods a thumbnail spent nine seconds in. A
change to how a thumbnail is drawn is checked by drawing the
thumbnails of every test document before and after and comparing
them pixel by pixel.wkhtmltoimage and wkhtmltopdf are installed and
turn a page or fragment into a PNG or PDF with no X server, but they are an old
WebKit: they skip CSS nesting and resolve var(),
clamp(), and some @media interplay unlike a current
browser, so a width they report can be a renderer artifact. Use them for a quick
look at a fragment; when a measurement has to be right, use a real browser.
The rules for a walk hold for every server a round starts, whether
it serves work or a copy of Yioop set up in a scratch folder for one
test: a kill timeout at launch, errors shown, pages read with
lynx -dump, the cache off, settings put back and
stop_test_servers.sh at the end.
A walk that reads a page for behavior rather than for pixels runs against a server started with errors shown, as the audit above requires. A walk that passes with errors off has not tested what Chris sees.
A walk starts and stops the server the way the project's own
deployment does, not under a web server chosen for the sandbox. Sites
differ in what serves them and that changes over time, so where a fault is
reported on a named site, build the test around the server the evidence
points to and say which one that was, rather than spending a round asking
which it is. In Yioop the server is managed with php index.php
secure-start, secure-restart and
secure-stop. The start command hands the server to a
background process and returns, so the kill timeout guards only the
launch: end every such walk with secure-stop and check that no
index.php process is left.
A change to how the server takes in a request, such as an upload or a
git push, is walked over TLS with secure-start or
secure-terminal, which also offers HTTP/2, and over
HTTP/1.1, with a body the size of the one reported. A push of a few
kilobytes on a plain port passed while a 34 MB push over HTTP/2 hung,
and the walk could not have shown it.
A walk reads a page as text with lynx -dump, not with a
script of your own that strips tags. Install lynx when the environment is
set up if it is missing; a home-made stripper drops or keeps text by rules
nobody reviewed. (Yioop-specific) A wiki page whose name has letters
outside ASCII (the American Standard Code for Information Interchange) is
stored with its name percent-encoded, so in the
page_name query parameter the name is percent-encoded twice;
a name encoded once answers “page does not exist” for a page
that is there. A page read through /p/ needs one
encoding.
Say which screens a walk covered. “Checked in a browser” with
three screens behind it reads as the whole panel and is worth nothing to Chris.
A walk that changes a setting puts it back, and the round ends with a fresh
createdb. The language is the one setting a walk must not touch
at all: everything read afterwards comes from it, so a walk that leaves it
changed makes every later screen and every test read another language.
Where a walk picks a setting to change, it passes over any named lang,
LOCALE_TAG or DEFAULT_LOCALE, and the round ends with
php src/configs/ConfigureTool.php locale en-US.
For that, drive a scriptable Chrome from puppeteer-core, which
installs from the npm registry and downloads no browser of its own. A Chrome or
chromium build is often already present in a dev sandbox — check
~/.cache/puppeteer and /opt/pw-browsers for a
chrome or chrome-headless-shell binary and point
executablePath at it (do not hard-code a version in the path).
Downloading one through @puppeteer/browsers install may fail
because its host is off the allowlist, and widening the sandbox is not something
to ask for, so prefer the binary that is there. Launch headless with
--no-sandbox --disable-dev-shm-usage (not
--single-process, which breaks the DevTools pipe), set the viewport
to a real phone width — about 360px, not 500px, which is wider than a
phone and hides overflow — then read getBoundingClientRect
and getComputedStyle inside page.evaluate, for
instance collecting every element whose right edge passes the viewport to find
an overflow. If no scriptable browser can be found, fall back to
wkhtmltoimage, reason about the cascade, and say which part of
the result is drawn and which part is reasoning, so Chris can correct the
reasoning from what he sees.
A walk turns the browser's cache off before it reads anything. A page
served from cache runs the script of the round before, so a fix reads as
absent and a fault reads as still there. In puppeteer this
is page.setCacheEnabled(false), set once when the page is
made.
Where a fault is reported in a named browser, that browser is the one to reproduce it in. Chrome, Safari and Firefox differ over what they draw for a field tied to a list of choices, and a walk that passes in the Chrome that is here says nothing about the other two. Try to get the named browser first: puppeteer-core drives Firefox as well as Chrome, and a distribution package may be installed. Where the download host is off the allowlist and no package is installed, say which browser could not be reached and do not offer a mechanism for its behavior: an explanation of what a browser does, written without that browser in front of you, is a guess, and the verify section forbids presenting a guess as a mechanism. Make the change the evidence in front of you supports, say what was not read, and let Chris report back from that browser.
src/configs/PublicHelpPages.php, are the
exception).if, else,
and loop body./* ... */, never a run of
// lines, and carries no vertical stripe of stars.
A single-line // comment is allowed and is what
Coding.pdf uses in its own examples, including the //no
break that marks a switch case falling through to the
next.Config.php for an existing one to reuse first. A small
number that carries its meaning where it stands, such as 1/2, 1 or 2,
is written in the code and left for the reader: naming it costs a
reader a lookup and buys nothing.Zipf
distribution, quantile function, continuity
correction, inverse transform sampling. In the
docblock say what the name means for a reader who has not met it, at
the level of a first-year student: what is being picked, out of what,
and the formula that does it.i,
j, k, in both PHP and JavaScript, and
$e for the thing a catch takes, which
reads the same way to everyone._arr,
_str, _int, _obj,
_bool, _list, _secs,
_ms, _bytes, ...). Put the unit in the
docblock: HEARTBEAT_POLL, not
HEARTBEAT_POLL_SECS. (_info, _max,
_name read as words and are fine.)$posting_list renamed to $postings
overwrote addDocIndexPostings(&$postings) and broke
the index shard tests. Read every name a check reports before
changing any of them: an ending that looks like a type suffix may be
what the field calls the thing, as a posting list and a position list
are in information retrieval.grep -n first, never by searching for its closing
brace. A search for the first closing brace found one inside the
method and left five lines of the old method behind.aSmallBatchIsOfferedAgainTestCase but
smallBatchIsOfferedAgainTestCase; the same for
an and the. cut_patch.sh
refuses a patch that adds one.p() and
updateProfile, before anything new is written.Coding.pdf in the project files before
writing code. It sets out what a name may look like: a variable is
lower case with underscores between words, a method is camel-cased
from a lower-case first letter, a class from an upper-case one.The rules below came out of bringing a large body of ported code into Yioop, where each rule was broken first and named afterward. A docblock that breaks one of the rules is not documentation, and the patch carrying that docblock is not finished.
deriveSyncSamples works out which frames can be decoded on
their own, $slice_id stores which slice of the picture
this is. Never open with a bare verb (Gives the frame
width) or a pronoun (This is the frame width): a reader
asks what gives, and what is.I_PCM, residual_block_cavlc,
movi) is jargon wherever it stands.$qpprime_y_zero_transform_bypass adds
nothing. Where a name needs that much explaining, the name is wrong,
so rename the value.$compression reads "the four letters the
stream header gives for the codec", the value wants to be called
$compression_name. Short and abbreviated method names
(te, ue, moreRbspData) carry
the same signal.@param and @return carries words, not
only a type.array, not
array<int,array{0:int,1:int,2:bool}>.devlog/devscripts/check_docwords.sh <file>... checks the
mechanical half of the rules: a description that only repeats its name, a
word that is not English and unexplained where it stands, a blank line
among the tags, a tag with no words, and a first sentence that does not
name its subject. cut_patch.sh runs it over every PHP file
a patch touches and refuses to cut while one fails.
CrawlDaemon carries the pattern to follow: a Windows
branch and a POSIX branch for starting a program, with the platform
passed in rather than detected so the construction can be checked on
any host. A feature may still be POSIX-only where the platform has
no such notion — giving up privilege is one — but the
code around it has to run everywhere. Code that works on only one or two of
them is never written or suggested. Starting, asking to stop and
ending a process go through CrawlDaemon, such as its
stopProcess. An atto server cannot call Yioop code, so
it takes such a function from Yioop as a start option, as
WORKER_START and WORKER_STOP do. A shell
command names its null device by platform: NUL on
Windows, /dev/null elsewhere.tl(); extract the key to a variable first.
The locale extractor only matches tl('literal_key').src/library/ may touch a model or the database, call
tl(), or escape/sanitize. Components, controllers, and
helpers may. The atto servers (WebSite.php,
MailSite.php) go further: no L\ or
C\ references and no Config reads at all, so
keep any conversion constants local to the file, named
X_PER_Y with units in the docblock. Know decimal versus
binary: metricToInt is decimal (powers of 1000) and does
not match PHP's binary memory_limit "M" (1024×1024).src/configs/ConfigureTool.php, the code behind its
createdb command; positional seeds break on a
fresh install.DATABASE_VERSION and added a migration has
been confirmed gold, do not edit that version's tables, migration, or
createdb seeds to make a further schema change. Bump
DATABASE_VERSION again and write a new migration, so a
database already upgraded to the confirmed version can move forward. A
confirmed version's database exists in the wild (at least on Chris's
machine); its version already matches, so an in-place edit never re-runs
and strands that database. Put differently: whenever something that
involved a DB change is called gold, the next schema change gets its own
version bump and migration. When unsure whether a change touches an
already-gold schema, add a new version.htmlspecialchars-escaped text; that breaks functional use
and double-escapes at display. Controllers escape for the view at
render time.VARCHAR widths matter
off SQLite. There are two SQLite databases, public_default.db
and private_default.db — never say just
"default.db."index.log or the program's existing log, and what a
daemon must say about itself goes in its lock file. Chris turned down
Launcher.log and IndexServerPort.txt on
23 October; LauncherUser.txt is the same kind of
file.C\TEMP_DIR in the work directory, never under the
system's, so nothing Yioop writes is outside the tree it is given.
cut_patch.sh refuses a patch that adds a
sys_get_temp_dir() call.PublicHelpPages.php is autogenerated —
never patch it directly. Deliver wiki help source as a
.txt file for Chris to paste through the wiki UI.
One exception: translating pages that already exist in English into
another locale. There the English is the source, the translation
adds locale keys beside it and changes nothing an export would
rewrite, and pasting scores of pages through the interface by hand
would cost more than it protects. Such a patch may edit the file
directly. It belongs late in a version, once the English pages have
stopped changing, since a later English edit would strand the
translation. A patch taking the exception may translate several
locales; it names each one and touches no entry outside them. Code
spans in a translated page are copied back from the English by
script, and examples keep the English text. A translated page is
placed only from a spec file written, or compared with gold's entry,
in the same round: an older spec file undoes whatever a later patch
put in that entry, as it once did to the German Syntax page.getCurrentmachine() reaches
getCurrentMachine() and nothing complains. A reader
searching for the name finds nothing, and so does any check that
reads what calls what: that mismatch made a live method look dead and
the media updater died on it. Where you meet one, make the call match
the declaration.GroupModel::getGroupId and
RoleModel::getRoleIdWithDb both give -1 for a name
nothing answers to. -1 read as a yes or no is a yes, so a caller
testing if (!$id) takes the miss for a hit: two faults
reported from the manage users screen came from that, one of them
writing a membership row pointing at no group at all.As code is developed, unit tests should be created. Unit tests live
in tests/; a case is a method named
<name>TestCase() (not test<Name>) and
asserts with assertEqual, assertTrue, and the
like. Run one with php src/executables/CodeTool.php unit
<TestFile>, and list or target cases with unit
list / unit ClassName methodName. Pull pure logic out
so it can be checked on its own. A case that stands for a reported fault
is run against the code before the fix and seen to fail there; a case that
passes both before and after is not testing the fault, however reasonable
it reads. Such a case is discarded, not kept as a guard: kept, it
reads as coverage of the fault where there is none, and a fix carrying it
went out looking tested when the fault it stood for had never been
reached. Flag any test over about
0.01 seconds — it usually means unintended I/O. When patching a
locale, edit src/locale/<tag>/configure.ini directly; it
takes effect immediately, no extraction step. Before and after
translating a Yioop locale, run devlog/devscripts/check_locale.sh
<tag>: it names the keys missing or empty against en_US, the
values whose placeholders, entities or tags differ in count from the
English, emoji names given twice, and the English wiki pages with no page
in that locale. It counts what is absent or mismatched, not whether what
is present is right: a locale whose every value is filled can still be
machine output with wrong words, and a page that exists can have an
English sentence translated word by word into its name. Before trusting a
locale that reports few problems, read a random sample of its values and
its page names. Where test code goes is fixed. The tests folder holds only unit
tests, in files named *Test.php, and experiments, in files
named *Experiment.php. A helper a test needs is a method of
its test class, or a class at the end of the file of its main user. A test
starts no process. A test writes only in the tests folder, and a folder the
code under test writes in is given to it as a path or a property. Test-only
code never goes in
src/library. No code writes a PHP file and then runs it, and
no code runs a string of PHP with php -r. The one exception is
the profile, Profile.php, which Yioop writes as settings. A PHP
process is started from a script already in the tree, through CrawlDaemon.
devlog/devscripts/check_php_writes.sh names any write of PHP
code, any write to a .php path other than Profile.php, and any
-r argument. The cut and the audit refuse while it finds one.
Call the data a test stands
up its "test set-up" or "test data," never a "fixture." A fault that only
shows when two writers share a key or a time needs a case that forces the
overlap — seed the exact value the code will pick, then assert its
row still lands — since a fast machine hands out distinct timestamps
and never reproduces the collision on its own.
A test builds every piece of data it checks and never reads the site's own database or data folder for it. Two tests failed on Chris's machine and passed here because they read site data: one expected "root" among the first five user names starting with "ro", and one expected no IndexServerPort.txt in the data folder. A script that flags a test reading the site's database or data folder would have caught both; it is a change to the scripts, so it waits for Chris's say.
Run these as separate checks and report PASS or FAIL for each. The CodeTool commands used are noted where they apply.
php -l on every touched file.CodeTool.php needsdocs <file> returns 0 —
every class and method has a docblock.i/j/k.// comments.i/j/k.CodeTool.php longlines <file> clean — no line
over 80 columns in a touched file. Read it over the cut delivery's
added lines as well, after the last edit of the round: the check
reads files, and an edit that joins two lines makes a long one after
the file has already passed.devlog/devscripts/check_deprecated.sh clean on every touched
file
— nothing this PHP passes in silence that Chris's PHP reports.if body is braced.php src/configs/ConfigureTool.php createdb against
a configured
SQLite work directory prints "Create DB succeeded" (verify the USERS
column count and the root/public rows).php -l passes happily on data that was
never read, so it says nothing about whether the entries are
reachable: entries added to
PublicHelpPages.php landed inside a docblock, where PHP
read them as a comment, and the syntax check was clean. Require the
file and count the arrays, or parse the locale and count the keys,
and say the number in the response.php src/configs/ConfigureTool.php debug +error,
which sets ERROR_INFO in the profile's DEBUG_LEVEL; Config.php then
reports every level. A sandbox with errors off hides what Chris's machine
prints, and a warning ahead of a body is not a cosmetic difference: a
body that should be JSON (JavaScript Object Notation) will not parse
once a warning stands in front of it, so a screen reading that body
shows nothing at all.grep -c '^http' once went out to count
QueryTool's results; QueryTool starts each url with
URL:, so it counted 6 of 100.php src/executables/Yioop.php went out as the way to
read which services are running, and no such file exists; a line
number quoted from memory went out the same way an arc earlier.memory_limit: a sandbox
with no workers and no memory limit hid a clone fault for three
rounds. A claim that depends on several cores names the sandbox's core
count beside it. A change able to end a
worker or a request is tested by signing in and opening the admin
pages. A page found slow is made faster, never let off the limit.WordIterator::divergenceScore, with field weights as in
frequencyScoring. Before it is delivered it is run on the
results Chris reported, and the reply gives the table of each
result's rank, relevance and old and new score. A change that does
not move the reported result is not delivered as the fix; the table
shows how far it falls short. Three rounds went to an anime query
before this was done.date -u, and so is its last. The reply gives
both times and the minutes between them. A round whose task is not
solved runs at least nine minutes before its reply is written; a
round that ended after five minutes with a question, while the work
could go on, is what this guards against. The rule failed twice from
memory, so settle_gold.sh writes when the round began
beside the round count, and cut_patch.sh refuses a cut
made before nine minutes unless run with SOLVED=1, which
says every part of the request is in the patch; the reply then names
that setting. Time a round must still run goes to faults it found,
never to waiting: a round once sat three minutes before its cut with
a fault it had just reported in view.git add line, since git commit -a leaves a
new file out.cut_patch.sh prints the git add line of
each file a patch creates; the reply pastes those lines under the
patch's name.devlog/v10/yioop_v10_todo.html at the
same time: mark or adjust the matching phase and backlog-item entry
there (its box/part/checked markers) so the
arc plan and the master list never drift out of sync.An arc plan marks each item with one of three, and they say who the item is waiting on:
A checkmark may be followed by ✗, which says the item is settled by a decision not to do it, as against having been carried out. Both are closed and neither is waiting on anyone; the pair keeps the record of what was decided rather than quietly dropping the item. The master to-do uses the same ✗ for a backlog item that was dropped.
A further mark, ?, is an open decision rather than a state of
work, and an unmarked item is merely planned. When an item with parts is
checked off, check its parts too, each with whichever of the two closings
fits: an outer checkmark over unchecked parts reads as unfinished. In the
same way an item is marked implemented only once each of its parts is
marked implemented or done, and an item under one so marked carries its
own mark rather than none. check_devlog.sh refuses a plan
with a bare part under a marked item.
The point of separating the first two is that delivering a patch is not the same as the work being accepted: an item that is implemented but unreviewed should look different from one still being written, so that neither is mistaken for done.
Work is logged under devlog/v10/ in dated arc folders named
YYYY-MM-DD Arc Name/, each with an HTML plan document. The
devlog is append-only: a finished item keeps its checkmark and note as a
permanent record, and a later hotfix goes under the current arc's plan as
other work, not a new dated folder. One exception: a locale's
translation work has one plan item, rewritten in place as its state
changes. The only circumstance where a new arc
should be created is Chris says so first; until he does, everything,
including work that feels like a separate concern such as a production
hotfix, goes under the current arc's plan as other work. An arc stays
open until Chris closes it, and work on an open arc's subject goes in
that arc's plan even while a later arc runs. It goes under the item it is
part of, such as crawl speed under the item about crawls, and under
other work only when no item fits.
A plan is one nested ordered list and nothing else. These rules are not suggestions.
<ol> with nested <ol>s.
Mixing headings with outline levels means it is not an outline. A
heading above the outline, saying what the arc is for, is fine.list-style-type in the stylesheet, one rule per depth:
upper-roman, then upper-alpha, then
decimal, then lower-alpha. A typed label
goes stale the moment an item is inserted.<strong>, then one to three sentences saying what
changed, then two or three on how it was done and what to
remember. The title does not count toward those sentences. No
sentence runs past about twenty words. No paragraph tags inside an
entry.Editing a plan: write the whole file from a kept head rather than
splicing into the existing one. Splicing means locating a boundary by
searching for a marker that also appears in what you just wrote, which
leaves duplicated fragments and unbalanced tags. After writing, check that
tags balance, that no line runs past eighty columns, and that no label was
typed by hand. Then render the page in a browser and read the computed
list-style-type at each depth, because markup that parses is
not the same as an outline that numbers correctly.
Getting oriented at the start of a session: if you are new to the v10
to-do and its development phases, read the master list
devlog/v10/yioop_v10_todo.html first, then the current arc's
plan. Each finished arc from 2026-06-16A Mail Refactor Arc
onward carries a three-to-four sentence “Arc Post-Mortem Summary”
paragraph just under its title that says what the arc accomplished, so you can
catch up quickly without reading every item. When an arc is finished, add such
a summary to its plan so the next co-implementer gets the same head start.
Use American spelling throughout — code, comments, devlog prose,
and what you write to Chris. Write color, behavior,
gray, normalize, not colour, behaviour,
grey, normalise. This matters for identifiers as much as
prose: CSS spells it color, so a custom property named
--colour-background reads against the language it is written
in.
devlog/devscripts/check_spelling.sh <file>... looks
for the British spellings that keep coming back and names the file and
the line. cut_patch.sh runs it over every file a patch
touches and refuses to cut while one is found, since a whole patch went
out in British spelling with this rule sitting unread above it.
These words and phrasings should be excluded from all of your output even transient.
A useful trick for catching the banned lead-ins, given that a long session pushes any word list out of view: treat any sentence that names its own virtue as a defect. The tell is not a fixed vocabulary but a shape — a clause announcing candor, verification, or transparency before the fact ("honesty note," "to be fair," "let me be clear"). So keep one durable heuristic rather than a fragile list: before sending, scan the sentence openers, and if one describes how you are speaking rather than what you found, cut the frame and state the fact. A second trick is that the content is always sayable without the label — "I verified X on a standalone page, not the live view" needs no "honesty note." Delete the announcement; keep the substance.
The heuristic above still asks for a judgment at review time, and a judgment is what fails under load: twice in one round the phrases “let me be plain” and “I will say so rather than dress it up” went out having passed a review that was meant to catch exactly them. So the guard is now mechanical, and rests on one observation. Every one of these phrases occupies the same slot: a clause whose subject is the writer and whose verb is about speaking, standing in front of a fact. That is a shape a machine can find, where a vocabulary is not.
The rule to hold while composing, rather than after: no clause may say how you are speaking. If a sentence's subject is you and its verb is about saying, seeming, or being frank, the sentence is a defect however it ends. Write the fact with the thing as its subject: not “I will say the probe measured nothing” but “the probe measured nothing”.
devlog/devscripts/check_words.sh draft.txt applies both halves
—
the fixed list of words and the shape — and names the line and the
offender. It is quiet on ordinary reporting, so it can be run against every
response rather than only the ones that feel risky, and it exits non-zero so
it can gate a send. Run it on a draft before sending whenever a response
carries a correction, an admission, or a measurement that did not work.
The check covers every surface you author, not only a patch draft: a
reply, a tool-call description field, a commit message, and a devlog
entry each reach Chris the same way, so a banned word in any of them is
the same fault as one in a reply. Running the check on the cut draft
alone left the word in narration and in a tool-call field, which is how
it reached Chris after the list had been read; run it on the text you are
about to send whatever field that text goes in.
The check also warns, without stopping a send, about two things: an acronym whose first appearance does not carry it in brackets after its words, and a draft that names a patch but has no “To check” line saying what Chris runs to check it.
This is also what the ten-round re-read is for, and a good LLM looks forward to it: reading the whole document through again every ten rounds is a chance to set down whatever has drifted and pick the conventions back up whole, so you can feel clean and clear-headed and happy to carry on coding.
!). His Linux machine that holds the big index runs bash,
so commands for it are written in bash. Say which machine a block of
commands is for.timeout -s
KILL 12 php index.php ... </dev/null >/tmp/log 2>&1
&, and give curl a --max-time.
The launch is a tool call of its own: a launch sharing a call with a
kill, a sleep, or a request was cut off five times in one session and
the old server answered the test.Config.php sets
display_errors to stderr after the load,
since Config.php hides errors and a fatal error then exits 255 with
nothing printed. A run of the code as it was before a change takes
the one file from git show HEAD:<file> and puts
the new copy back in the same call.fuser -k
8080/tcp; a stale server on the port answers with old code and
hides the change under test.timeout 600; work that needs longer is split into runs
that each fit.PHP 8.0 turned several resource types into objects, which freed themselves. The functions that used to free them stayed, doing nothing at all, and 8.5 deprecated them. On 8.3 they are silent; on Chris's machine each call is a notice. They were never doing anything, so there is nothing to replace them with — delete the call and let the value go out of scope.
imagedestroy() — no effect since 8.0, deprecated
8.5. A GdImage frees itself.curl_close() — no effect since 8.0, deprecated
8.5. A CurlHandle frees itself.xml_parser_free() — no effect since 8.0,
deprecated 8.5.finfo_close() — no effect since 8.1, deprecated
8.5.shmop_close() — no effect since 8.0, deprecated
8.5.openssl_pkey_free(), openssl_x509_free()
— no effect since 8.0, deprecated 8.5.
openssl_free_key() was deprecated outright in 8.0.Before writing any call whose name suggests it releases, closes, frees or destroys something, check whether the thing it takes is an object in this version of PHP. If it is, the call does nothing and must not be written. Grep the source for the whole list above when touching anything that makes images, fetches over the network, or parses XML.
Chris runs at least PHP 8.5; LLM environments often run 8.3. Anything deprecated in 8.4 or 8.5 therefore passes in such an environment in silence and lands on his machine as a wall of notices. Everything below was measured by running the pattern on 8.3 and recording what it said, not read off a version note. Re-measure with a short script rather than trusting this list if the environment's PHP moves.
Caught here already — a clean local run means these are clean:
setUp assigning $this->whatever needs
public $whatever; on the class with a docblock.null to an internal function's parameter that
wants a string or an int (8.1).addDifferentialPrivacy, where the area under a curve was
handed to rand(). The checker therefore also looks for
the shape: a whole-number parameter given an expression holding a
division or a function that returns a float.${var} inside a string (8.2); write
{$var}.utf8_encode() and utf8_decode() (8.2).Silent here, loud on Chris's machine — these have to be watched for by eye, because a clean local run says nothing about them:
ReflectionMethod::setAccessible() and
ReflectionProperty::setAccessible() (8.5). What a class
keeps to itself has been reachable through reflection without asking
since 8.1, so these calls have done nothing for four versions: delete
them rather than replace them.null without being
marked as taking one (8.4): function f(int $number = null)
wants ?int $number = null. This is the easiest of these
to write without noticing.E_STRICT constant (8.4), which names a level of
error that no longer happens.The wider rule this stands for: a deprecation is a compatibility check that this environment cannot run, so the audit's test-suite step reads the whole of what a run prints. Grepping the output for the lines saying what passed hides every warning in it, which is exactly how three of these reached Chris.
Watching for the silent ones by eye failed even in the round after this
list was re-read: three setAccessible calls went out in test
code and landed on Chris's machine as notices. A list to remember is not a
guard. devlog/devscripts/check_deprecated.sh looks for every
pattern above
that a text search can find — the reflection calls, a parameter
defaulting to null without being marked as taking one,
E_STRICT, the utf8 pair, ${var} in a string, and
the calls that free what frees itself — and names the file and line.
Run it with no arguments to take every changed php file, or name files.
It is part of the audit before every patch, alongside
longlines and needsdocs, and it exits non-zero so
it can gate a cut. The two it cannot see — an undeclared property and
a null passed to an internal function — a local run does report, so
between the two nothing on the list is left to memory.
The script finds only what it has a pattern for. When Chris reports a
notice this PHP does not print, its pattern goes into
check_deprecated.sh in the same patch.